From a88d6f2eeba2b3355c33fac6d736cf6086342f47 Mon Sep 17 00:00:00 2001 From: hisham Date: Wed, 1 Apr 2009 17:11:57 +0000 Subject: Import latest revision from CVS at luaforge.net git-svn-id: http://luarocks.org/svn/luarocks/trunk@1 9ca3f7c1-7366-0410-b1a3-b5c78f85698c --- COPYING | 32 +++ Makefile | 123 ++++++++ configure | 362 ++++++++++++++++++++++++ install.bat | 312 +++++++++++++++++++++ makedist | 68 +++++ src/bin/luarocks | 19 ++ src/bin/luarocks-admin | 14 + src/luarocks/build.lua | 276 ++++++++++++++++++ src/luarocks/build/builtin.lua | 152 ++++++++++ src/luarocks/build/cmake.lua | 54 ++++ src/luarocks/build/command.lua | 30 ++ src/luarocks/build/make.lua | 84 ++++++ src/luarocks/cfg.lua | 242 ++++++++++++++++ src/luarocks/command_line.lua | 133 +++++++++ src/luarocks/deps.lua | 618 +++++++++++++++++++++++++++++++++++++++++ src/luarocks/fetch.lua | 284 +++++++++++++++++++ src/luarocks/fetch/cvs.lua | 43 +++ src/luarocks/fetch/git.lua | 53 ++++ src/luarocks/fetch/sscm.lua | 40 +++ src/luarocks/fs.lua | 43 +++ src/luarocks/fs/unix.lua | 519 ++++++++++++++++++++++++++++++++++ src/luarocks/fs/win32.lua | 369 ++++++++++++++++++++++++ src/luarocks/help.lua | 69 +++++ src/luarocks/install.lua | 115 ++++++++ src/luarocks/list.lua | 35 +++ src/luarocks/make.lua | 54 ++++ src/luarocks/make_manifest.lua | 32 +++ src/luarocks/manif.lua | 439 +++++++++++++++++++++++++++++ src/luarocks/pack.lua | 116 ++++++++ src/luarocks/path.lua | 224 +++++++++++++++ src/luarocks/persist.lua | 91 ++++++ src/luarocks/remove.lua | 163 +++++++++++ src/luarocks/rep.lua | 187 +++++++++++++ src/luarocks/require.lua | 263 ++++++++++++++++++ src/luarocks/search.lua | 394 ++++++++++++++++++++++++++ src/luarocks/type_check.lua | 233 ++++++++++++++++ src/luarocks/unpack.lua | 148 ++++++++++ src/luarocks/util.lua | 180 ++++++++++++ test/run_tests.sh | 76 +++++ test/test_deps.lua | 67 +++++ test/test_require.lua | 25 ++ 41 files changed, 6781 insertions(+) create mode 100644 COPYING create mode 100644 Makefile create mode 100755 configure create mode 100644 install.bat create mode 100755 makedist create mode 100755 src/bin/luarocks create mode 100755 src/bin/luarocks-admin create mode 100644 src/luarocks/build.lua create mode 100644 src/luarocks/build/builtin.lua create mode 100644 src/luarocks/build/cmake.lua create mode 100644 src/luarocks/build/command.lua create mode 100644 src/luarocks/build/make.lua create mode 100644 src/luarocks/cfg.lua create mode 100644 src/luarocks/command_line.lua create mode 100644 src/luarocks/deps.lua create mode 100644 src/luarocks/fetch.lua create mode 100644 src/luarocks/fetch/cvs.lua create mode 100644 src/luarocks/fetch/git.lua create mode 100644 src/luarocks/fetch/sscm.lua create mode 100644 src/luarocks/fs.lua create mode 100644 src/luarocks/fs/unix.lua create mode 100644 src/luarocks/fs/win32.lua create mode 100644 src/luarocks/help.lua create mode 100644 src/luarocks/install.lua create mode 100644 src/luarocks/list.lua create mode 100644 src/luarocks/make.lua create mode 100644 src/luarocks/make_manifest.lua create mode 100644 src/luarocks/manif.lua create mode 100644 src/luarocks/pack.lua create mode 100644 src/luarocks/path.lua create mode 100644 src/luarocks/persist.lua create mode 100644 src/luarocks/remove.lua create mode 100644 src/luarocks/rep.lua create mode 100644 src/luarocks/require.lua create mode 100644 src/luarocks/search.lua create mode 100644 src/luarocks/type_check.lua create mode 100644 src/luarocks/unpack.lua create mode 100644 src/luarocks/util.lua create mode 100755 test/run_tests.sh create mode 100644 test/test_deps.lua create mode 100755 test/test_require.lua diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..adec4655 --- /dev/null +++ b/COPYING @@ -0,0 +1,32 @@ +LuaRocks is free software: it can be used for both academic and commercial +purposes at absolutely no cost. There are no royalties or GNU-like "copyleft" +restrictions. LuaRocks qualifies as Open Source software. Its licenses are +compatible with the GPL. LuaRocks is not in the public domain and the Kepler +Project keeps its copyright. The legal details are below. + +The spirit of the license is that you are free to use LuaRocks for any purpose +at no cost without having to ask us. The only requirement is that if you do +use LuaRocks, then you should give us credit by including the appropriate +copyright notice somewhere in your product or its documentation. + +------------------------------------------------------------------------------ + +Copyright © 2007 Kepler Project. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..5241eb7a --- /dev/null +++ b/Makefile @@ -0,0 +1,123 @@ +# $Id: Makefile,v 1.30 2008/08/18 14:07:35 hisham Exp $ + +include config.unix + +DESTDIR = +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +LUADIR ?= $(PREFIX)/share/lua/5.1/ +LUA_DIR ?= /usr/local +LUA_BINDIR ?= $(LUA_DIR)/bin + +BIN_FILES = luarocks luarocks-admin +LUAROCKS_FILES = build/cmake.lua build/command.lua build.lua build/make.lua \ +command_line.lua cfg.lua deps.lua fetch.lua fs.lua fs/unix.lua \ +fs/win32.lua help.lua install.lua list.lua persist.lua \ +make_manifest.lua pack.lua path.lua rep.lua require.lua search.lua \ +type_check.lua util.lua remove.lua build/builtin.lua make.lua manif.lua unpack.lua \ +fetch/cvs.lua fetch/sscm.lua fetch/git.lua + +CONFIG_FILE = $(SYSCONFDIR)/config.lua + +all: + for f in $(BIN_FILES) ;\ + do \ + sed "1d" src/bin/$$f >> src/bin/$$f.bak ;\ + echo "#!$(LUA_BINDIR)/lua$(LUA_SUFFIX)" > src/bin/$$f ;\ + echo "package.path = [[$(LUADIR)/?.lua;$(LUADIR)/?/init.lua;]]..package.path" >> src/bin/$$f ;\ + cat src/bin/$$f.bak >> src/bin/$$f ;\ + rm src/bin/$$f.bak ;\ + done + cp src/luarocks/cfg.lua src/luarocks/cfg.lua.bak + rm src/luarocks/cfg.lua + if [ -n "$(PREFIX)" ] ;\ + then \ + echo "local LUAROCKS_PREFIX=[[$(PREFIX)]]" >> src/luarocks/cfg.lua ;\ + fi + if [ -n "$(LUA_INCDIR)" ] ;\ + then \ + echo "local LUA_INCDIR=[[$(LUA_INCDIR)]]" >> src/luarocks/cfg.lua ;\ + fi + if [ -n "$(LUA_LIBDIR)" ] ;\ + then \ + echo "local LUA_LIBDIR=[[$(LUA_LIBDIR)]]" >> src/luarocks/cfg.lua ;\ + fi + if [ -n "$(LUA_BINDIR)" ] ;\ + then \ + echo "local LUA_BINDIR=[[$(LUA_BINDIR)]]" >> src/luarocks/cfg.lua ;\ + fi + if [ -n "$(LUA_SUFFIX)" ] ;\ + then \ + echo "local LUA_INTERPRETER=[[lua$(LUA_SUFFIX)]]" >> src/luarocks/cfg.lua ;\ + fi + if [ -n "$(SYSCONFDIR)" ] ;\ + then \ + echo "local LUAROCKS_SYSCONFIG=[[$(SYSCONFDIR)/config.lua]]" >> src/luarocks/cfg.lua ;\ + fi + if [ -n "$(ROCKS_TREE)" ] ;\ + then \ + echo "local LUAROCKS_ROCKS_TREE=[[$(ROCKS_TREE)]]" >> src/luarocks/cfg.lua ;\ + fi + if [ -n "$(FORCE_CONFIG)" ] ;\ + then \ + echo "local LUAROCKS_FORCE_CONFIG=true" >> src/luarocks/cfg.lua ;\ + fi + echo "local LUAROCKS_UNAME_S=[[$(LUAROCKS_UNAME_S)]]" >> src/luarocks/cfg.lua + echo "local LUAROCKS_UNAME_M=[[$(LUAROCKS_UNAME_M)]]" >> src/luarocks/cfg.lua + echo "local LUAROCKS_DOWNLOADER=[[$(LUAROCKS_DOWNLOADER)]]" >> src/luarocks/cfg.lua + echo "local LUAROCKS_MD5CHECKER=[[$(LUAROCKS_MD5CHECKER)]]" >> src/luarocks/cfg.lua + cat src/luarocks/cfg.lua.bak >> src/luarocks/cfg.lua + rm src/luarocks/cfg.lua.bak + @echo + @echo "Done. Type 'make install' to install into $(PREFIX)." + @echo + +luadoc: + rm -rf doc/luadoc + mkdir -p doc/luadoc + cd src && luadoc -d ../doc/luadoc --nofiles luarocks/*.lua + +check_makefile: + echo $(BIN_FILES) | tr " " "\n" | sort > makefile_list.txt + ( cd src/bin && ls -d * ) | grep -v "CVS" | sort > luarocks_dir.txt + echo $(LUAROCKS_FILES) | tr " " "\n" | sort >> makefile_list.txt + ( cd src/luarocks && ls -d *.lua ) | sort >> luarocks_dir.txt + diff makefile_list.txt luarocks_dir.txt + rm makefile_list.txt luarocks_dir.txt + @echo + @echo "Makefile is sane." + @echo + +clean: + for f in $(BIN_FILES) ;\ + do \ + sed -i.bak "s,^#!.*lua.*,#!/usr/bin/env lua,;/^package.path/d" src/bin/$$f ;\ + rm src/bin/$$f.bak ;\ + done + sed -i.bak "/^local LUA/d" src/luarocks/cfg.lua + rm src/luarocks/cfg.lua.bak + +install: + mkdir -p "$(DESTDIR)$(BINDIR)" + cd src/bin && cp $(BIN_FILES) "$(DESTDIR)$(BINDIR)" + mkdir -p "$(DESTDIR)$(LUADIR)/luarocks" + cd src/luarocks && for f in $(LUAROCKS_FILES); do d="$(DESTDIR)$(LUADIR)/luarocks"/`dirname "$$f"`; mkdir -p "$$d"; cp "$$f" "$$d"; done + mkdir -p "$(DESTDIR)$(ROCKS_TREE)" + if [ ! -f "$(DESTDIR)$(CONFIG_FILE)" ] ;\ + then \ + mkdir -p `dirname "$(DESTDIR)$(CONFIG_FILE)"` ;\ + echo 'rocks_servers = {' >> "$(DESTDIR)$(CONFIG_FILE)" ;\ + echo ' [[http://luarocks.luaforge.net/rocks]]' >> "$(DESTDIR)$(CONFIG_FILE)" ;\ + echo '}' >> "$(DESTDIR)$(CONFIG_FILE)" ;\ + echo 'rocks_trees = {' >> "$(DESTDIR)$(CONFIG_FILE)" ;\ + if [ ! -n "$(FORCE_CONFIG)" ] ;\ + then \ + echo ' home..[[/.luarocks]],' >> "$(DESTDIR)$(CONFIG_FILE)" ;\ + fi ;\ + echo ' [[$(ROCKS_TREE)]]' >> "$(DESTDIR)$(CONFIG_FILE)" ;\ + echo '}' >> "$(DESTDIR)$(CONFIG_FILE)" ;\ + if [ -n "$(SCRIPTS_DIR)" ] ;\ + then \ + echo "scripts_dir = [[$(SCRIPTS_DIR)]]" >> "$(DESTDIR)$(CONFIG_FILE)" ;\ + fi ;\ + fi diff --git a/configure b/configure new file mode 100755 index 00000000..268b7241 --- /dev/null +++ b/configure @@ -0,0 +1,362 @@ +#!/bin/sh + +# A basic configure script for LuaRocks. +# Not doing any fancy shell stuff here to keep good compatibility. + +# Defaults + +PREFIX="/usr/local" +SYSCONFDIR="$PREFIX/etc/luarocks" +ROCKS_TREE="$PREFIX/lib/luarocks" +SCRIPTS_DIR="" +LUA_SUFFIX="" +LUA_DIR="/usr" +LUA_BINDIR="/usr/bin" +LUA_INCDIR="/usr/include" +LUA_LIBDIR="/usr/lib" + +# ---------------------------------------------------------------------------- +# FUNCTION DEFINITIONS +# ---------------------------------------------------------------------------- + +# Help + +show_help() { +cat </dev/null 2>/dev/null` + then + echo + echo '*WARNING*: the "~" sign is not expanded in flags.' + echo 'If you mean the home directory, use $HOME instead.' + echo + fi + case "$key" in + --help) + show_help + exit 0 + ;; + --prefix) + [ -n "$value" ] || die "Missing value in flag $key." + PREFIX="$value" + PREFIX_SET=yes + ;; + --sysconfdir) + [ -n "$value" ] || die "Missing value in flag $key." + SYSCONFDIR="$value" + SYSCONFDIR_SET=yes + ;; + --rocks-tree) + [ -n "$value" ] || die "Missing value in flag $key." + ROCKS_TREE="$value" + ROCKS_TREE_SET=yes + ;; + --scripts-dir) + [ -n "$value" ] || die "Missing value in flag $key." + SCRIPTS_DIR="$value" + SCRIPTS_DIR_SET=yes + ;; + --force-config) + FORCE_CONFIG=yes + ;; + --lua-suffix) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_SUFFIX="$value" + LUA_SUFFIX_SET=yes + ;; + --with-lua) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_DIR="$value" + LUA_DIR_SET=yes + ;; + --with-lua-include) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_INCDIR="$value" + LUA_INCDIR_SET=yes + ;; + --with-lua-lib) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_LIBDIR="$value" + LUA_LIBDIR_SET=yes + ;; + --with-downloader) + [ -n "$value" ] || die "Missing value in flag $key." + case "$value" in + wget|curl) LUAROCKS_DOWNLOADER="$value" ;; + *) echo "Invalid option: $value. See --help." ; exit 1 ;; + esac + LUAROCKS_DOWNLOADER_SET=yes + ;; + --with-md5-checker) + [ -n "$value" ] || die "Missing value in flag $key." + case "$value" in + md5sum|openssl|md5) LUAROCKS_MD5CHECKER="$value" ;; + *) echo "Invalid option: $value. See --help." ; exit 1 ;; + esac + LUAROCKS_MD5CHECKER_SET=yes + ;; + *) + echo "Error: Unknown flag: $1" + exit 1 + ;; + esac + shift +done + + +if [ "$PREFIX_SET" = "yes" -a ! "$SYSCONFDIR_SET" = "yes" ] +then + if [ "$PREFIX" = "/usr" ] + then SYSCONFDIR=/etc/luarocks + else SYSCONFDIR=$PREFIX/etc/luarocks + fi +fi + + +if [ "$PREFIX_SET" = "yes" -a ! "$ROCKS_TREE_SET" = "yes" ] +then + ROCKS_TREE=$PREFIX/lib/luarocks +fi + +if [ "$LUA_SUFFIX_SET" != "yes" ] +then + for suffix in "" "5.1" "51" "" + do + LUA_SUFFIX="$suffix" + if [ "$LUA_DIR_SET" = "yes" ] + then + if [ -f "$LUA_DIR/bin/lua$suffix" ] + then + find_lua="$LUA_DIR" + fi + else + find_lua=`find_program lua$suffix` + fi + if [ -n "$find_lua" ] + then + echo "Lua interpreter found: $find_lua/lua$suffix..." + break + fi + done +fi + +if [ "$LUA_DIR_SET" != "yes" ] +then + echo_n "Looking for Lua... " + if [ ! -n "$find_lua" ] + then + find_lua=`find_program lua$LUA_SUFFIX` + fi + + if [ -n "$find_lua" ] + then + LUA_DIR=`dirname $find_lua` + LUA_BINDIR="$find_lua" + echo "lua$LUA_SUFFIX found in \$PATH: $find_lua" + else + echo "lua$LUA_SUFFIX not found in \$PATH." + echo "You may want to use the flags --with-lua and/or --lua-suffix. See --help." + exit 1 + fi +fi + +if [ "$LUA_INCDIR_SET" != "yes" ] +then + LUA_INCDIR="$LUA_DIR/include" +fi + +if [ "$LUA_LIBDIR_SET" != "yes" ] +then + LUA_LIBDIR="$LUA_DIR/lib" +fi + +if [ "$LUA_DIR_SET" = "yes" ] +then + LUA_BINDIR="$LUA_DIR/bin" +fi + +echo_n "Checking Lua includes... " +lua_h="$LUA_INCDIR/lua.h" +if [ -f "$lua_h" ] +then + echo "lua.h found in $lua_h" +else + echo "lua.h not found (looked in $lua_h)" + echo "You may want to use the flag --with-lua-include. See --help." + exit 1 +fi + +if [ "$LUAROCKS_DOWNLOADER_SET" != "yes" ] +then + find_helper "downloader helper program" wget curl fetch + LUAROCKS_DOWNLOADER=$HELPER +fi + +if [ "$LUAROCKS_MD5CHECKER_SET" != "yes" ] +then + find_helper "MD5 checksum calculator" md5sum openssl md5 + LUAROCKS_MD5CHECKER=$HELPER +fi + +echo_n "Configuring for system... " +if uname -s +then + LUAROCKS_UNAME_S=`uname -s` +else + echo "Could not determine operating system. 'uname -s' failed." + exit 1 +fi +echo_n "Configuring for architecture... " +if uname -m +then + LUAROCKS_UNAME_M=`uname -m` +else + echo "Could not determine processor architecture. 'uname -m' failed." + exit 1 +fi + +if [ -f config.unix ]; then + rm -f config.unix +fi + +# Write config + +echo "Writing configuration..." +echo + +cat < config.unix +# This file was automatically generated by the configure script. +# Run "./configure --help" for details. + +PREFIX=$PREFIX +SYSCONFDIR=$SYSCONFDIR +ROCKS_TREE=$ROCKS_TREE +SCRIPTS_DIR=$SCRIPTS_DIR +LUA_SUFFIX=$LUA_SUFFIX +LUA_DIR=$LUA_DIR +LUA_INCDIR=$LUA_INCDIR +LUA_LIBDIR=$LUA_LIBDIR +LUA_BINDIR=$LUA_BINDIR +FORCE_CONFIG=$FORCE_CONFIG +LUAROCKS_UNAME_M=$LUAROCKS_UNAME_M +LUAROCKS_UNAME_S=$LUAROCKS_UNAME_S +LUAROCKS_DOWNLOADER=$LUAROCKS_DOWNLOADER +LUAROCKS_MD5CHECKER=$LUAROCKS_MD5CHECKER + +EOF + +echo "Installation prefix: $PREFIX" +echo "LuaRocks configuration directory: $SYSCONFDIR" +echo "Using Lua from: $LUA_DIR" + +make clean > /dev/null 2> /dev/null + +echo +echo "Done. You can now run 'make' to build." +echo diff --git a/install.bat b/install.bat new file mode 100644 index 00000000..141fe0f8 --- /dev/null +++ b/install.bat @@ -0,0 +1,312 @@ +@ECHO OFF + +REM Boy, it feels like 1994 all over again. + +SETLOCAL + +SET PREFIX=C:\LuaRocks +SET VERSION=1.0 +SET SYSCONFDIR=C:\LuaRocks +SET ROCKS_TREE=C:\LuaRocks +SET SCRIPTS_DIR= +SET FORCE=OFF +SET INSTALL_LUA=OFF +SET LUA_INTERPRETER= +SET LUA_PREFIX= +SET LUA_BINDIR= +SET LUA_INCDIR= +SET LUA_LIBDIR= +SET FORCE_CONFIG= +SET MKDIR=.\bin\mkdir -p + +REM *********************************************************** +REM Option parser +REM *********************************************************** + +:PARSE_LOOP +IF [%1]==[] GOTO DONE_PARSING +IF [%1]==[/?] ( + ECHO Installs LuaRocks. + ECHO. + ECHO /P [dir] Where to install. + ECHO Default is %PREFIX% + ECHO /CONFIG [dir] Location where the config file should be installed. + ECHO Default is %SYSCONFDIR% + ECHO /TREE [dir] Root of the local tree of installed rocks. + ECHO Default is %ROCKS_TREE% + ECHO /SCRIPTS [dir] Where to install scripts installed by rocks. + ECHO Default is TREE/bin. + ECHO. + ECHO /L Install LuaRocks' own copy of Lua even if detected. + ECHO /LUA [dir] Location where Lua is installed - e.g. c:\lua\5.1\ + ECHO /INC [dir] Location of Lua includes - e.g. c:\lua\5.1\include + ECHO /LIB [dir] Location of Lua libraries -e.g. c:\lua\5.1\lib + ECHO /BIN [dir] Location of Lua executables - e.g. c:\lua\5.1\bin + ECHO. + ECHO /FORCECONFIG Use a single config location. Do not use the + ECHO LUAROCKS_CONFIG variable or the user's home directory. + ECHO Useful to avoid conflicts when LuaRocks + ECHO is embedded within an application. + ECHO. + ECHO /F Remove installation directory if it already exists. + ECHO. + GOTO QUIT +) +IF /I [%1]==[/P] ( + SET PREFIX=%2 + SHIFT /1 + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/CONFIG] ( + SET SYSCONFDIR=%2 + SHIFT /1 + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/TREE] ( + SET ROCKS_TREE=%2 + SHIFT /1 + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/SCRIPTS] ( + SET SCRIPTS_DIR=%2 + SHIFT /1 + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/L] ( + SET INSTALL_LUA=ON + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/LUA] ( + SET LUA_PREFIX=%2 + SHIFT /1 + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/LIB] ( + SET LUA_LIBDIR=%2 + SHIFT /1 + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/INC] ( + SET LUA_INCDIR=%2 + SHIFT /1 + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/BIN] ( + SET LUA_BINDIR=%2 + SHIFT /1 + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/FORCECONFIG] ( + SET FORCE_CONFIG=ON + SHIFT /1 + GOTO PARSE_LOOP +) +IF /I [%1]==[/F] ( + SET FORCE=ON + SHIFT /1 + GOTO PARSE_LOOP +) +ECHO Unrecognized option: %1 +GOTO ERROR +:DONE_PARSING + +SET FULL_PREFIX=%PREFIX%\%VERSION% + +SET BINDIR=%FULL_PREFIX% +SET LIBDIR=%FULL_PREFIX% +SET LUADIR=%FULL_PREFIX%\lua +SET INCDIR=%FULL_PREFIX%\include + +REM *********************************************************** +REM Detect Lua +REM *********************************************************** + +IF [%INSTALL_LUA%]==[ON] GOTO USE_OWN_LUA + +FOR %%L IN (%LUA_PREFIX% c:\lua\5.1.2 c:\lua c:\kepler\1.1) DO ( + SET CURR=%%L + IF EXIST "%%L" ( + IF NOT [%LUA_BINDIR%]==[] ( + IF EXIST %LUA_BINDIR%\lua5.1.exe ( + SET LUA_INTERPRETER=%LUA_BINDIR%\lua5.1.exe + GOTO INTERPRETER_IS_SET + ) + IF EXIST %LUA_BINDIR%\lua.exe ( + SET LUA_INTERPRETER=%LUA_BINDIR%\lua.exe + GOTO INTERPRETER_IS_SET + ) + ECHO Lua executable lua.exe or lua5.1.exe not found in %LUA_BINDIR% + GOTO ERROR + ) + SET CURR=%%L + FOR %%E IN (\ \bin\) DO ( + IF EXIST "%%L%%E\lua5.1.exe" ( + SET LUA_INTERPRETER=%%L%%E\lua5.1.exe + SET LUA_BINDIR=%%L%%E + GOTO INTERPRETER_IS_SET + ) + IF EXIST "%%L%%E\lua.exe" ( + SET LUA_INTERPRETER=%%L%%E\lua.exe + SET LUA_BINDIR=%%L%%E + GOTO INTERPRETER_IS_SET + ) + ) + GOTO TRY_NEXT_LUA_DIR + :INTERPRETER_IS_SET + IF NOT "%LUA_LIBDIR%"=="" ( + IF EXIST %LUA_LIBDIR%\lua5.1.lib GOTO LIBDIR_IS_SET + ECHO lua5.1.lib not found in %LUA_LIBDIR% + GOTO ERROR + ) + FOR %%E IN (\ \lib\ \bin\) DO ( + IF EXIST "%CURR%%%E\lua5.1.lib" ( + SET LUA_LIBDIR=%CURR%%%E + GOTO LIBDIR_IS_SET + ) + ) + GOTO TRY_NEXT_LUA_DIR + :LIBDIR_IS_SET + IF NOT [%LUA_INCDIR%]==[] ( + IF EXIST %LUA_INCDIR%\lua.h GOTO INCDIR_IS_SET + ECHO lua.h not found in %LUA_INCDIR% + GOTO ERROR + ) + FOR %%E IN (\ \include\) DO ( + IF EXIST "%CURR%%%E\lua.h" ( + SET LUA_INCDIR=%CURR%%%E + GOTO INCDIR_IS_SET + ) + ) + GOTO TRY_NEXT_LUA_DIR + :INCDIR_IS_SET + %LUA_INTERPRETER% -v 2>NUL + IF NOT ERRORLEVEL 1 ( + GOTO LUA_IS_SET + ) + ) +:TRY_NEXT_LUA_DIR + REM wtf +) +ECHO Could not find Lua. Will install its own copy. +ECHO See /? for options for specifying the location of Lua. +:USE_OWN_LUA +SET INSTALL_LUA=ON +SET LUA_INTERPRETER=lua5.1 +SET LUA_BINDIR=%BINDIR% +SET LUA_LIBDIR=%LIBDIR% +SET LUA_INCDIR=%INCDIR% +:LUA_IS_SET +ECHO. +ECHO Will configure LuaRocks with the following paths: +ECHO Lua interpreter: %LUA_INTERPRETER% +ECHO Lua binaries: %LUA_BINDIR% +ECHO Lua libraries: %LUA_LIBDIR% +ECHO Lua includes: %LUA_INCDIR% +ECHO. + +REM *********************************************************** +REM Install LuaRocks files +REM *********************************************************** + +IF [%FORCE%]==[ON] ( + ECHO Removing %FULL_PREFIX%... + RD /S /Q "%FULL_PREFIX%" +) + +IF NOT EXIST "%FULL_PREFIX%" GOTO NOT_EXIST_PREFIX + ECHO %FULL_PREFIX% exists. Use /F to force removal and reinstallation. + GOTO ERROR +:NOT_EXIST_PREFIX + +ECHO Installing LuaRocks in %FULL_PREFIX%... +IF NOT EXIST "%BINDIR%" %MKDIR% "%BINDIR%" +IF ERRORLEVEL 1 GOTO ERROR +IF [%INSTALL_LUA%]==[ON] ( + IF NOT EXIST "%LUA_BINDIR%" %MKDIR% "%LUA_BINDIR%" + IF NOT EXIST "%LUA_INCDIR%" %MKDIR% "%LUA_INCDIR%" + COPY lua5.1\bin\*.* "%LUA_BINDIR%" >NUL + COPY lua5.1\include\*.* "%LUA_INCDIR%" >NUL +) +COPY bin\*.* "%BINDIR%" >NUL +IF ERRORLEVEL 1 GOTO ERROR +COPY src\bin\*.* "%BINDIR%" >NUL +IF ERRORLEVEL 1 GOTO ERROR +FOR %%C IN (luarocks luarocks-admin) DO ( + RENAME "%BINDIR%\%%C" %%C.lua + IF ERRORLEVEL 1 GOTO ERROR + DEL /F /Q "%BINDIR%\%%C.bat" 2>NUL + ECHO @ECHO OFF>> "%BINDIR%\%%C.bat" + ECHO SETLOCAL>> "%BINDIR%\%%C.bat" + ECHO SET LUA_PATH=%LUADIR%\?.lua;%LUADIR%\?\init.lua;%%LUA_PATH%%>> "%BINDIR%\%%C.bat" + ECHO SET PATH=%BINDIR%\;%%PATH%%>> "%BINDIR%\%%C.bat" + ECHO "%LUA_INTERPRETER%" "%BINDIR%\%%C.lua" %%*>> "%BINDIR%\%%C.bat" + ECHO ENDLOCAL>> "%BINDIR%\%%C.bat" +) +IF NOT EXIST "%LUADIR%\luarocks" %MKDIR% "%LUADIR%\luarocks" +IF ERRORLEVEL 1 GOTO ERROR +XCOPY /S src\luarocks\*.* "%LUADIR%\luarocks" >NUL +IF ERRORLEVEL 1 GOTO ERROR + +RENAME "%LUADIR%\luarocks\cfg.lua" "cfg.lua.bak" +ECHO local LUA_INCDIR=[[%LUA_INCDIR%]]>> "%LUADIR%\luarocks\cfg.lua" +ECHO local LUA_LIBDIR=[[%LUA_LIBDIR%]]>> "%LUADIR%\luarocks\cfg.lua" +ECHO local LUA_BINDIR=[[%LUA_BINDIR%]]>> "%LUADIR%\luarocks\cfg.lua" +ECHO local LUA_INTERPRETER=[[%LUA_INTERPRETER%]]>> "%LUADIR%\luarocks\cfg.lua" +ECHO local LUAROCKS_UNAME_S=[[WindowsNT]]>> "%LUADIR%\luarocks\cfg.lua" +ECHO local LUAROCKS_UNAME_M=[[x86]]>> "%LUADIR%\luarocks\cfg.lua" +ECHO local LUAROCKS_SYSCONFIG=[[%SYSCONFDIR%/config.lua]]>> "%LUADIR%\luarocks\cfg.lua" +ECHO local LUAROCKS_ROCKS_TREE=[[%ROCKS_TREE%]]>> "%LUADIR%\luarocks\cfg.lua" +ECHO local LUAROCKS_PREFIX=[[%PREFIX%]]>> "%LUADIR%\luarocks\cfg.lua" +IF NOT [%FORCE_CONFIG%]==[] ECHO local LUAROCKS_FORCE_CONFIG=true>> "%LUADIR%\luarocks\cfg.lua" +TYPE "%LUADIR%\luarocks\cfg.lua.bak">> "%LUADIR%\luarocks\cfg.lua" + +DEL /F /Q "%LUADIR%\luarocks\cfg.lua.bak" + +SET CONFIG_FILE=%SYSCONFDIR%\config.lua + +IF NOT EXIST "%SYSCONFDIR%" %MKDIR% "%SYSCONFDIR%" +IF NOT EXIST "%CONFIG_FILE%" ( + ECHO rocks_servers = {>> "%CONFIG_FILE%" + ECHO [[http://luarocks.luaforge.net/rocks]]>> "%CONFIG_FILE%" + ECHO }>> "%CONFIG_FILE%" + ECHO rocks_trees = {>> "%CONFIG_FILE%" + IF [%FORCE_CONFIG%]==[] ECHO home..[[/luarocks]],>> "%CONFIG_FILE%" + ECHO [[%ROCKS_TREE%]]>> "%CONFIG_FILE%" + ECHO }>> "%CONFIG_FILE%" + IF NOT [%SCRIPTS_DIR%]==[] ECHO scripts_dir=[[%SCRIPTS_DIR%]]>> "%CONFIG_FILE%" +) + +IF [%SCRIPTS_DIR%]==[] ( + %MKDIR% "%ROCKS_TREE%"\bin >NUL + COPY lua5.1\bin\*.dll "%ROCKS_TREE%"\bin >NUL +) +IF NOT [%SCRIPTS_DIR%]==[] ( + %MKDIR% "%SCRIPTS_DIR%" >NUL + COPY lua5.1\bin\*.dll "%SCRIPTS_DIR%" >NUL +) + +IF NOT EXIST "%ROCKS_TREE%" %MKDIR% "%ROCKS_TREE%" +IF NOT EXIST "%APPDATA%/luarocks" %MKDIR% "%APPDATA%/luarocks" + +REM *********************************************************** +REM Exit handlers +REM *********************************************************** + +ECHO LuaRocks is installed! +:QUIT +ENDLOCAL +EXIT /B 0 + +:ERROR +ECHO Failed installing LuaRocks. +ENDLOCAL +EXIT /B 1 diff --git a/makedist b/makedist new file mode 100755 index 00000000..3e78040a --- /dev/null +++ b/makedist @@ -0,0 +1,68 @@ +#!/bin/sh + +cvslist() { + if [ "$1" ] + then local prefix="$1/" + fi + cvs list $1 | grep -v "^?" | while read line + do + local path="$prefix$line" + echo "$path" + if [ -d "$path" ] + then + cvslist "$path" + fi + done +} + +if ! [ "$1" ] +then + echo "usage: $0 " + exit 1 +fi + +cvs list > /dev/null 2> /dev/null +if [ $? != 0 ] +then + echo "Your version of CVS may be too old. At least 1.12 is needed." + exit 1 +fi + +make clean + +out="luarocks-$1" +rm -rf "$out" +mkdir "$out" +list=`cvslist` +rm -f missing_ref +echo "$list" | while read i +do + if [ -f "$i" ] + then + dir=`dirname $i` + mkdir -p "$out/$dir" + cp "$i" "$out/$dir" + if echo "$i" | grep -q "^src/" + then + grep -qw `basename "$i"` Makefile || { + echo "Missing ref in makefile: $i" + touch missing_ref + exit 1 + } + fi + fi +done +if [ -e missing_ref ] +then + rm -f missing_ref + exit 1 +fi +rm -f "$out-win32.zip" "$out.tar.gz" +rm "$out/makedist" +rm "$out/install.bat" +tar czvpf "$out.tar.gz" "$out" +cp install.bat "$out" +cp -a win32/bin "$out" +cp -a win32/lua5.1 "$out" +zip -r "$out-win32.zip" "$out" +rm -rf "$out" diff --git a/src/bin/luarocks b/src/bin/luarocks new file mode 100755 index 00000000..8812ecc8 --- /dev/null +++ b/src/bin/luarocks @@ -0,0 +1,19 @@ +#!/usr/bin/env lua + +local command_line = require("luarocks.command_line") + +program_name = "luarocks" +program_description = "LuaRocks main command-line interface" + +commands = {} +commands.help = require("luarocks.help") +commands.pack = require("luarocks.pack") +commands.unpack = require("luarocks.unpack") +commands.build = require("luarocks.build") +commands.install = require("luarocks.install") +commands.search = require("luarocks.search") +commands.list = require("luarocks.list") +commands.remove = require("luarocks.remove") +commands.make = require("luarocks.make") + +command_line.run_command(...) diff --git a/src/bin/luarocks-admin b/src/bin/luarocks-admin new file mode 100755 index 00000000..826597f6 --- /dev/null +++ b/src/bin/luarocks-admin @@ -0,0 +1,14 @@ +#!/usr/bin/env lua + +local command_line = require("luarocks.command_line") + +program_name = "luarocks-admin" +program_description = "LuaRocks repository administration interface" + +commands = { +} + +commands.help = require("luarocks.help") +commands.make_manifest = require("luarocks.make_manifest") + +command_line.run_command(...) diff --git a/src/luarocks/build.lua b/src/luarocks/build.lua new file mode 100644 index 00000000..f0f7225d --- /dev/null +++ b/src/luarocks/build.lua @@ -0,0 +1,276 @@ + +--- Module implementing the LuaRocks "build" command. +-- Builds a rock, compiling its C parts if any. +module("luarocks.build", package.seeall) + +local path = require("luarocks.path") +local util = require("luarocks.util") +local rep = require("luarocks.rep") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local deps = require("luarocks.deps") +local manif = require("luarocks.manif") + +help_summary = "Build/compile a rock." +help_arguments = "{|| []}" +help = [[ +Build a rock, compiling its C parts if any. +Argument may be a rockspec file, a source rock file +or the name of a rock to be fetched from a repository. +]] + +--- Install files to a given location. +-- Takes a table where the array part is a list of filenames to be copied. +-- In the hash part, other keys are identifiers in Lua module format, +-- to indicate which subdirectory the file should be copied to. For example, +-- install_files({["foo.bar"] = "src/bar.lua"}, "boo") will copy src/bar.lua +-- to boo/foo. +-- @param files table or nil: A table containing a list of files to copy in +-- the format described above. If nil is passed, this function is a no-op. +-- Directories should be delimited by forward slashes as in internet URLs. +-- @param location string: The base directory files should be copied to. +-- @return boolean or (nil, string): True if succeeded or +-- nil and an error message. +local function install_files(files, location) + assert(type(files) == "table" or not files) + assert(type(location) == "string") + if files then + for k, file in pairs(files) do + local dest = location + if type(k) == "string" then + dest = fs.make_path(location, path.module_to_path(k)) + end + fs.make_dir(dest) + local ok = fs.copy(fs.make_path(file), dest) + if not ok then + return nil, "Failed copying "..file + end + end + end + return true +end + +--- Write to the current directory the contents of a table, +-- where each key is a file name and its value is the file content. +-- @param files table: The table of files to be written. +local function extract_from_rockspec(files) + for name, content in pairs(files) do + local fd = io.open(fs.make_path(fs.current_dir(), name), "w+") + fd:write(content) + fd:close() + end +end + +--- Applies patches inlined in the build.patches section +-- and extracts files inlined in the build.extra_files section +-- of a rockspec. +-- @param rockspec table: A rockspec table. +-- @return boolean or (nil, string): True if succeeded or +-- nil and an error message. +function apply_patches(rockspec) + assert(type(rockspec) == "table") + + local build = rockspec.build + if build.extra_files then + extract_from_rockspec(build.extra_files) + end + if build.patches then + extract_from_rockspec(build.patches) + for patch, _ in util.sortedpairs(build.patches) do + print("Applying patch "..patch.."...") + local ok, err = fs.patch(tostring(patch)) + if not ok then + return nil, "Failed applying patch "..patch + end + end + end + return true +end + +--- Build and install a rock given a rockspec. +-- @param rockspec_file string: local or remote filename of a rockspec. +-- @param need_to_fetch boolean: true if sources need to be fetched, +-- false if the rockspec was obtained from inside a source rock. +-- @return boolean or (nil, string): True if succeeded or +-- nil and an error message. +function build_rockspec(rockspec_file, need_to_fetch, minimal_mode) + assert(type(rockspec_file) == "string") + assert(type(need_to_fetch) == "boolean") + + local rockspec, err = fetch.load_rockspec(rockspec_file) + if err then + return nil, err + elseif not rockspec.build then + return nil, "Rockspec error: build table not specified" + elseif not rockspec.build.type then + return nil, "Rockspec error: build type not specified" + end + + local ok, err = deps.fulfill_dependencies(rockspec) + if err then + return nil, err + end + ok, err = deps.check_external_deps(rockspec, "build") + if err then + return nil, err + end + + local name, version = rockspec.name, rockspec.version + if rep.is_installed(name, version) then + rep.delete_version(name, version) + end + + if not minimal_mode then + local _, dir + if need_to_fetch then + ok, dir = fetch.fetch_sources(rockspec, true) + if not ok then + return nil, dir + end + fs.change_dir(dir) + elseif rockspec.source.file then + local ok, err = fs.unpack_archive(rockspec.source.file) + if not ok then + return nil, err + end + end + fs.change_dir(rockspec.source.dir) + end + + local dirs = { + lua = path.lua_dir(name, version), + lib = path.lib_dir(name, version), + conf = path.conf_dir(name, version), + bin = path.bin_dir(name, version), + } + + for _, dir in pairs(dirs) do + fs.make_dir(dir) + end + local rollback = util.schedule_function(function() + fs.delete(path.install_dir(name, version)) + fs.remove_dir_if_empty(path.versions_dir(name)) + end) + + local build = rockspec.build + + if not minimal_mode then + ok, err = apply_patches(rockspec) + if err then + return nil, err + end + end + + if build.type ~= "none" then + + -- Temporary compatibility + if build.type == "module" then build.type = "builtin" end + + ok, build_type = pcall(require, "luarocks.build." .. build.type) + if not ok or not type(build_type) == "table" then + return nil, "Failed initializing build back-end for build type '"..build.type.."'" + end + + ok, err = build_type.run(rockspec) + if not ok then + return nil, "Build error: " .. err + end + end + + if build.install then + for id, dir in pairs(dirs) do + ok, err = install_files(build.install[id], dir) + if not ok then + return nil, err + end + end + end + + local copy_directories = build.copy_directories or {"doc"} + + for _, dir in pairs(copy_directories) do + if fs.is_dir(dir) then + local dest = fs.make_path(path.install_dir(name, version), dir) + fs.make_dir(dest) + fs.copy_contents(dir, dest) + end + end + + for _, dir in pairs(dirs) do + fs.remove_dir_if_empty(dir) + end + + fs.pop_dir() + + fs.copy(rockspec.local_filename, path.rockspec_file(name, version)) + if need_to_fetch then + fs.pop_dir() + end + + ok, err = rep.install_bins(name, version) + if not ok then return nil, err end + + ok, err = rep.run_hook(rockspec, "post_install") + if err then + return nil, err + end + + ok, err = manif.update_manifest(name, version) + if err then + return nil, err + end + util.remove_scheduled_function(rollback) + return true +end + +--- Build and install a rock. +-- @param rock_file string: local or remote filename of a rock. +-- @param need_to_fetch boolean: true if sources need to be fetched, +-- false if the rockspec was obtained from inside a source rock. +-- @return boolean or (nil, string): True if build was successful, +-- or false and an error message. +local function build_rock(rock_file, need_to_fetch) + assert(type(rock_file) == "string") + assert(type(need_to_fetch) == "boolean") + + local dir, err = fetch.fetch_and_unpack_rock(rock_file) + if not dir then + return nil, err + end + local rockspec_file = path.rockspec_name_from_rock(rock_file) + fs.change_dir(dir) + local ok, err = build_rockspec(rockspec_file, need_to_fetch) + fs.pop_dir() + return ok, err +end + +--- Driver function for "build" command. +-- @param name string: A local or remote rockspec or rock file. +-- If a package name is given, forwards the request to "search" and, +-- if returned a result, installs the matching rock. +-- @param version string: When passing a package name, a version number may +-- also be given. +-- @return boolean or (nil, string): True if build was successful; nil and an +-- error message otherwise. +function run(...) + local flags, name, version = util.parse_flags(...) + if type(name) ~= "string" then + return nil, "Argument missing, see help." + end + assert(type(version) == "string" or not version) + + if name:match("%.rockspec$") then + return build_rockspec(name, true) + elseif name:match("%.src%.rock$") then + return build_rock(name, false) + elseif name:match("%.all%.rock$") then + local install = require("luarocks.install") + return install.install_binary_rock(name) + elseif name:match("%.rock$") then + return build_rock(name, true) + elseif not name:match(fs.dir_separator) then + local search = require("luarocks.search") + return search.act_on_src_or_rockspec(run, name, version) + end + return nil, "Don't know what to do with "..name +end diff --git a/src/luarocks/build/builtin.lua b/src/luarocks/build/builtin.lua new file mode 100644 index 00000000..c93edaa2 --- /dev/null +++ b/src/luarocks/build/builtin.lua @@ -0,0 +1,152 @@ + +--- A builtin build system: back-end to provide a portable way of building C-based Lua modules. +module("luarocks.build.builtin", package.seeall) + +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local util = require("luarocks.util") +local cfg = require("luarocks.cfg") + +--- Check if platform was detected +-- @param query string: The platform name to check. +-- @return boolean: true if LuaRocks is currently running on queried platform. +local function is_platform(query) + assert(type(query) == "string") + + for _, platform in ipairs(cfg.platforms) do + if platform == query then + return true + end + end +end + +--- Run a command displaying its execution on standard output. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +local function execute(...) + io.stdout:write(table.concat({...}, " ").."\n") + return fs.execute(...) +end + +--- Driver function for the builtin build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors ocurred, +-- nil and an error message otherwise. +function run(rockspec) + assert(type(rockspec) == "table") + local compile_object, compile_library + + local build = rockspec.build + local variables = rockspec.variables + + local function add_flags(extras, flag, flags) + if flags then + if type(flags) ~= "table" then + flags = { tostring(flags) } + end + util.variable_substitutions(flags, variables) + for _, v in ipairs(flags) do + table.insert(extras, flag:format(v)) + end + end + end + + if is_platform("win32") then + compile_object = function(object, source, defines, incdirs) + local extras = {} + add_flags(extras, "-D%s", defines) + add_flags(extras, "-I%s", incdirs) + return execute(variables.CC.." "..variables.CFLAGS, "-c", "-Fo"..object, "-I"..variables.LUA_INCDIR, source, unpack(extras)) + end + compile_library = function(library, objects, libraries, libdirs, name) + local extras = { unpack(objects) } + add_flags(extras, "-libpath:%s", libdirs) + add_flags(extras, "%s.lib", libraries) + local basename = fs.base_name(library):gsub(".[^.]*$", "") + local deffile = basename .. ".def" + local def = io.open(fs.make_path(fs.current_dir(), deffile), "w+") + def:write("EXPORTS\n") + def:write("luaopen_"..name:gsub("%.", "_").."\n") + def:close() + local ok = execute(variables.LD, "-dll", "-def:"..deffile, "-out:"..library, fs.make_path(variables.LUA_LIBDIR, "lua5.1.lib"), unpack(extras)) + local manifestfile = basename..".dll.manifest" + if ok and fs.exists(manifestfile) then + ok = execute(variables.MT, "-manifest", manifestfile, "-outputresource:"..basename..".dll;2") + end + return ok + end + else + compile_object = function(object, source, defines, incdirs) + local extras = {} + add_flags(extras, "-D%s", defines) + add_flags(extras, "-I%s", incdirs) + return execute(variables.CC.." "..variables.CFLAGS, "-I"..variables.LUA_INCDIR, "-c", source, "-o", object, unpack(extras)) + end + compile_library = function (library, objects, libraries, libdirs) + local extras = { unpack(objects) } + add_flags(extras, "-L%s", libdirs) + add_flags(extras, "-l%s", libraries) + if is_platform("cygwin") then + add_flags(extras, "-l%s", {"lua"}) + end + return execute(variables.LD.." "..variables.LIBFLAG, "-o", library, unpack(extras)) + end + end + + local ok = true + local built_modules = {} + local luadir = path.lua_dir(rockspec.name, rockspec.version) + local libdir = path.lib_dir(rockspec.name, rockspec.version) + local docdir = path.doc_dir(rockspec.name, rockspec.version) + for name, info in pairs(build.modules) do + local moddir = path.module_to_path(name) + if type(info) == "string" then + local ext = info:match(".([^.]+)$") + if ext == "lua" then + local dest = fs.make_path(luadir, moddir) + built_modules[info] = dest + else + info = {info} + end + end + if type(info) == "table" then + local objects = {} + local sources = info.sources + if info[1] then sources = info end + if type(sources) == "string" then sources = {sources} end + for _, source in ipairs(sources) do + local object = source:gsub(".[^.]*$", "."..cfg.obj_extension) + if not object then + object = source.."."..cfg.obj_extension + end + ok = compile_object(object, source, info.defines, info.incdirs) + if not ok then break end + table.insert(objects, object) + end + if not ok then break end + local module_name = fs.make_path(moddir, name:match("([^.]*)$").."."..cfg.lib_extension):gsub("//", "/") + if moddir ~= "" then + fs.make_dir(moddir) + end + local dest = fs.make_path(libdir, moddir) + built_modules[module_name] = dest + ok = compile_library(module_name, objects, info.libraries, info.libdirs, name) + if not ok then break end + end + end + for name, dest in pairs(built_modules) do + fs.make_dir(dest) + ok = fs.copy(name, dest) + if not ok then break end + end + if ok then + if fs.is_dir("lua") then + fs.copy_contents("lua", luadir) + end + end + if ok then + return true + else + return nil, "Build error" + end +end diff --git a/src/luarocks/build/cmake.lua b/src/luarocks/build/cmake.lua new file mode 100644 index 00000000..7fd1fcc8 --- /dev/null +++ b/src/luarocks/build/cmake.lua @@ -0,0 +1,54 @@ + +--- Build back-end for CMake-based modules. +module("luarocks.build.cmake", package.seeall) + +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local cfg = require("luarocks.cfg") + +--- Driver function for the "cmake" build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors ocurred, +-- nil and an error message otherwise. +function run(rockspec) + assert(type(rockspec) == "table") + local build = rockspec.build + local variables = build.variables or {} + + -- Pass Env variables + build.variables.CMAKE_MODULE_PATH=os.getenv("CMAKE_MODULE_PATH") + build.variables.CMAKE_LIBRARY_PATH=os.getenv("CMAKE_LIBRARY_PATH") + build.variables.CMAKE_INCLUDE_PATH=os.getenv("CMAKE_INCLUDE_PATH") + + util.variable_substitutions(variables, rockspec.variables) + + -- If inline cmake is present create CMakeLists.txt from it. + if type(build.cmake) == "string" then + local cmake = assert(io.open(fs.current_dir().."/CMakeLists.txt", "w")) + cmake:write(build.cmake) + cmake:close() + end + + + -- Execute cmake with variables. + local args = "" + if cfg.cmake_generator then + args = args .. ' -G"'..cfg.cmake_generator.. '"' + end + for k,v in pairs(variables) do + args = args .. ' -D' ..k.. '="' ..v.. '"' + end + + if not fs.execute("cmake . " ..args) then + return nil, "Failed cmake." + end + + if not fs.execute("make -fMakefile") then + return nil, "Failed building." + end + + if not fs.execute("make -fMakefile install") then + return nil, "Failed installing." + end + return true +end diff --git a/src/luarocks/build/command.lua b/src/luarocks/build/command.lua new file mode 100644 index 00000000..72d3de7f --- /dev/null +++ b/src/luarocks/build/command.lua @@ -0,0 +1,30 @@ + +--- Build back-end for raw listing of commands in rockspec files. +module("luarocks.build.command", package.seeall) + +local fs = require("luarocks.fs") +local util = require("luarocks.util") + +--- Driver function for the "command" build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors ocurred, +-- nil and an error message otherwise. +function run(rockspec) + assert(type(rockspec) == "table") + + local build = rockspec.build + + util.variable_substitutions(build, rockspec.variables) + + if build.build_command then + if not fs.execute(build.build_command) then + return nil, "Failed building." + end + end + if build.install_command then + if not fs.execute(build.install_command) then + return nil, "Failed installing." + end + end + return true +end diff --git a/src/luarocks/build/make.lua b/src/luarocks/build/make.lua new file mode 100644 index 00000000..b3e553a9 --- /dev/null +++ b/src/luarocks/build/make.lua @@ -0,0 +1,84 @@ + +--- Build back-end for using Makefile-based packages. +module("luarocks.build.make", package.seeall) + +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local cfg = require("luarocks.cfg") + +--- Call "make" with given target and variables +-- @param pass boolean: If true, run make; if false, do nothing. +-- @param target string: The make target; an empty string indicates +-- the default target. +-- @param variables table: A table containing string-string key-value +-- pairs representing variable assignments to be passed to make. +-- @return boolean: false if any errors occurred, true otherwise. +local function make_pass(pass, target, variables) + assert(type(pass) == "boolean") + assert(type(target) == "string") + assert(type(variables) == "table") + + local assignments = {} + for k,v in pairs(variables) do + table.insert(assignments, k.."="..v) + end + if pass then + return fs.execute(cfg.make.." "..target, unpack(assignments)) + else + return true + end +end + +--- Driver function for the "make" build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors ocurred, +-- nil and an error message otherwise. +function run(rockspec) + assert(type(rockspec) == "table") + + local build = rockspec.build + + if build.build_pass == nil then build.build_pass = true end + if build.install_pass == nil then build.install_pass = true end + build.build_variables = build.build_variables or {} + build.install_variables = build.install_variables or {} + build.build_target = build.build_target or "" + build.install_target = build.install_target or "install" + local makefile = build.makefile or cfg.makefile + if makefile then + -- Assumes all make's accept -f. True for POSIX make, GNU make and Microsoft nmake. + build.build_target = "-f "..makefile.." "..build.build_target + build.install_target = "-f "..makefile.." "..build.install_target + end + + if build.variables then + for var, val in pairs(build.variables) do + build.build_variables[var] = val + build.install_variables[var] = val + end + end + + util.variable_substitutions(build.build_variables, rockspec.variables) + util.variable_substitutions(build.install_variables, rockspec.variables) + + local auto_variables = { "CC" } + + for _, variable in pairs(auto_variables) do + if not build.build_variables[variable] then + build.build_variables[variable] = rockspec.variables[variable] + end + if not build.install_variables[variable] then + build.install_variables[variable] = rockspec.variables[variable] + end + end + + local ok = make_pass(build.build_pass, build.build_target, build.build_variables) + if not ok then + return nil, "Failed building." + end + ok = make_pass(build.install_pass, build.install_target, build.install_variables) + if not ok then + return nil, "Failed installing." + end + return true +end diff --git a/src/luarocks/cfg.lua b/src/luarocks/cfg.lua new file mode 100644 index 00000000..3c4c5aed --- /dev/null +++ b/src/luarocks/cfg.lua @@ -0,0 +1,242 @@ + +local rawset, next, table, pairs, print, require, io, os, setmetatable = + rawset, next, table, pairs, print, require, io, os, setmetatable + +--- Configuration for LuaRocks. +-- Tries to load the user's configuration file and +-- defines defaults for unset values. See the +-- config +-- file format documentation for details. +module("luarocks.cfg") + +local persist = require("luarocks.persist") + +local detected = {} +local system,proc + +-- A proper installation of LuaRocks will hardcode the system +-- and proc values with LUAROCKS_UNAME_S and LUAROCKS_UNAME_M, +-- so that this detection does not run every time. When it is +-- performed, we use the Unix way to identify the system, +-- even on Windows (assuming UnxUtils or Cygwin). +system = LUAROCKS_UNAME_S or io.popen("uname -s"):read("*l") +proc = LUAROCKS_UNAME_M or io.popen("uname -m"):read("*l") +if proc:match("i[%d]86") then + proc = "x86" +elseif proc:match("amd64") or proc:match("x86_64") then + proc = "x86_64" +elseif proc:match("Power Macintosh") then + proc = "powerpc" +end + +if system == "FreeBSD" then + detected.unix = true + detected.freebsd = true +elseif system == "Darwin" then + detected.unix = true + detected.macosx = true +elseif system == "Linux" then + detected.unix = true + detected.linux = true +elseif system and system:match("^CYGWIN") then + detected.unix = true + detected.cygwin = true +elseif system and system:match("^Windows") then + detected.windows = true +else + detected.unix = true + -- Fall back to Unix in unknown systems. +end + +local sys_config_file, home_config_file, home_tree +if detected.windows then + home = os.getenv("APPDATA") or "c:" + sys_config_file = "c:/luarocks/config.lua" + home_config_file = home.."/luarocks/config.lua" + home_tree = home.."/luarocks/" +else + home = os.getenv("HOME") or "" + sys_config_file = "/etc/luarocks/config.lua" + home_config_file = home.."/.luarocks/config.lua" + home_tree = home.."/.luarocks/" +end + +variables = {} +rocks_trees = {} + +persist.load_into_table(LUAROCKS_SYSCONFIG or sys_config_file, _M) + +if not LUAROCKS_FORCE_CONFIG then + home_config_file = os.getenv("LUAROCKS_CONFIG") or home_config_file + local home_overrides = persist.load_into_table(home_config_file, { home = home }) + if home_overrides then + local util = require("luarocks.util") + util.deep_merge(_M, home_overrides) + end +end + +local defaults = { + arch = "unknown", + lib_extension = "unknown", + obj_extension = "unknown", + rocks_servers = { + "http://luarocks.luaforge.net/rocks" + }, + lua_extension = "lua", + lua_interpreter = LUA_INTERPRETER or "lua", + downloader = LUAROCKS_DOWNLOADER or "wget", + md5checker = LUAROCKS_MD5CHECKER or "md5sum", + variables = {} +} + +defaults.external_deps_subdirs = { + bin = "bin", + lib = "lib", + include = "include" +} +defaults.runtime_external_deps_subdirs = defaults.external_deps_subdirs + +if detected.windows then + home_config_file = home_config_file:gsub("\\","/") + defaults.lib_extension = "dll" + defaults.obj_extension = "obj" + local rootdir = LUAROCKS_ROCKS_TREE or home_tree + defaults.root_dir = rootdir + defaults.rocks_dir = rootdir.."/rocks/" + defaults.scripts_dir = rootdir.."/bin/" + defaults.external_deps_dirs = { "c:/external/" } + defaults.variables.LUA_BINDIR = LUA_BINDIR and LUA_BINDIR:gsub("\\", "/") or "c:/lua5.1/bin" + defaults.variables.LUA_INCDIR = LUA_INCDIR and LUA_INCDIR:gsub("\\", "/") or "c:/lua5.1/include" + defaults.variables.LUA_LIBDIR = LUA_LIBDIR and LUA_LIBDIR:gsub("\\", "/") or "c:/lua5.1/lib" + defaults.arch = "win32-"..proc + defaults.platforms = {"win32", "windows" } + defaults.cmake_generator = "MinGW Makefiles" + -- TODO: Split Windows flavors between mingw and msvc + -- defaults.make = "make" + -- defaults.makefile = "Makefile" + defaults.make = "nmake" + defaults.makefile = "Makefile.win" + defaults.variables.CC = "cl" + defaults.variables.LD = "link" + defaults.variables.MT = "mt" + defaults.variables.CFLAGS = "/MD /O2" + defaults.variables.LIBFLAG = "/dll" + defaults.external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "?.lib", "?.dll" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "?.dll" }, + include = { "?.h" } + } +end + +if detected.unix then + defaults.lib_extension = "so" + defaults.obj_extension = "o" + local rootdir = LUAROCKS_ROCKS_TREE or home_tree + defaults.root_dir = rootdir + defaults.rocks_dir = rootdir.."/rocks/" + defaults.scripts_dir = rootdir.."/bin/" + defaults.external_deps_dirs = { "/usr/local", "/usr" } + defaults.variables.LUA_BINDIR = LUA_BINDIR or "/usr/local/bin" + defaults.variables.LUA_INCDIR = LUA_INCDIR or "/usr/local/include" + defaults.variables.LUA_LIBDIR = LUA_LIBDIR or "/usr/local/lib" + defaults.variables.CFLAGS = "-O2" + defaults.cmake_generator = "Unix Makefiles" + defaults.make = "make" + defaults.platforms = { "unix" } + defaults.variables.CC = "cc" + defaults.variables.LD = "ld" + defaults.variables.LIBFLAG = "-shared" + defaults.external_deps_patterns = { + bin = { "?" }, + lib = { "lib?.a", "lib?.so" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?" }, + lib = { "lib?.so" }, + include = { "?.h" } + } +end + +if detected.cygwin then + defaults.lib_extension = "so" -- can be overridden in the config file for mingw builds + defaults.arch = "cygwin-"..proc + defaults.platforms = {"unix", "cygwin"} + defaults.cmake_generator = "Unix Makefiles" + defaults.variables.CC = "echo -llua | xargs gcc" + defaults.variables.LD = "echo -llua | xargs gcc" + defaults.variables.LIBFLAG = "-shared" +end + +defaults.external_lib_extension = defaults.lib_extension + +if detected.macosx then + defaults.external_lib_extension = "dylib" + defaults.arch = "macosx-"..proc + defaults.platforms = {"unix", "macosx"} + defaults.variables.CC = "export MACOSX_DEPLOYMENT_TARGET=10.3; gcc" + defaults.variables.LD = "export MACOSX_DEPLOYMENT_TARGET=10.3; gcc" + defaults.variables.LIBFLAG = "-bundle -undefined dynamic_lookup -all_load" +end + +if detected.linux then + defaults.arch = "linux-"..proc + defaults.platforms = {"unix", "linux"} + defaults.variables.CC = "gcc" + defaults.variables.LD = "gcc" + defaults.variables.LIBFLAG = "-shared" +end + +if detected.freebsd then + defaults.arch = "freebsd-"..proc + defaults.make = "gmake" + defaults.platforms = {"unix", "freebsd"} + defaults.variables.CC = "gcc" + defaults.variables.LD = "gcc" + defaults.variables.LIBFLAG = "-shared" +end + +if proc == "x86_64" and not defaults.variables.CFLAGS:match("-fPIC") then + defaults.variables.CFLAGS = defaults.variables.CFLAGS.." -fPIC" +end + +-- Expose some more values detected by LuaRocks for use by rockspec authors. +defaults.variables.LUA = defaults.lua_interpreter +defaults.variables.LIB_EXTENSION = defaults.lib_extension +defaults.variables.OBJ_EXTENSION = defaults.obj_extension +defaults.variables.LUAROCKS_PREFIX = LUAROCKS_PREFIX + +local cfg_mt = { + __index = function(t, k) + local default = defaults[k] + if default then + rawset(t, k, default) + end + return default + end +} + +if not _M.variables then + _M.variables = {} +end +for k,v in pairs(defaults.variables) do + if not _M.variables[k] then + _M.variables[k] = v + end +end + +setmetatable(_M, cfg_mt) + +if not next(rocks_trees) then + if home_tree then + table.insert(rocks_trees, home_tree) + end + if LUAROCKS_ROCKS_TREE then + table.insert(rocks_trees, LUAROCKS_ROCKS_TREE) + end +end diff --git a/src/luarocks/command_line.lua b/src/luarocks/command_line.lua new file mode 100644 index 00000000..b3020284 --- /dev/null +++ b/src/luarocks/command_line.lua @@ -0,0 +1,133 @@ + +program_version = "1.0" + +--- Functions for command-line scripts. +module("luarocks.command_line", package.seeall) + +local util = require("luarocks.util") +local cfg = require("luarocks.cfg") +local fs = require("luarocks.fs") + +--- Display an error message and exit. +-- @param message string: The error message. +local function die(message) + assert(type(message) == "string") + + local ok, err = pcall(util.run_scheduled_functions) + if not ok then + print("\nLuaRocks "..program_version.." internal bug (please report at luarocks-developers@lists.luaforge.net):\n"..err) + end + print("\nError: "..message) + os.exit(1) +end + +--- Main command-line processor. +-- Parses input arguments and calls the appropriate driver function +-- to execute the action requested on the command-line, forwarding +-- to it any additional arguments passed by the user. +-- Uses the global table "commands", which contains +-- the loaded modules representing commands. +-- @param ... string: Arguments given on the command-line. +function run_command(...) + local args = {...} + local cmdline_vars = {} + for i = #args, 1, -1 do + local arg = args[i] + if arg:match("^[^-][^=]*=") then + local var, val = arg:match("^([A-Z_][A-Z0-9_]*)=(.*)") + if val then + cmdline_vars[var] = val + table.remove(args, i) + else + die("Invalid assignment: "..arg) + end + end + end + local nonflags = { util.parse_flags(unpack(args)) } + local flags = table.remove(nonflags, 1) + + if flags["to"] then + if flags["to"] == true then + die("Argument error: use --to=") + end + local root_dir = fs.absolute_name(flags["to"]) + cfg.root_dir = root_dir + cfg.rocks_dir = root_dir.."/rocks" + cfg.scripts_dir = root_dir.."/bin" + else + local trees = cfg.rocks_trees + for i = #trees, 1, -1 do + local tree = trees[i] + if fs.make_dir(tree) and fs.is_writable(tree) then + cfg.root_dir = tree + cfg.rocks_dir = tree.."/rocks" + cfg.scripts_dir = rawget(cfg, "scripts_dir") or tree.."/bin" + break + end + end + end + + cfg.root_dir = cfg.root_dir:gsub("/+$", "") + cfg.rocks_dir = cfg.rocks_dir:gsub("/+$", "") + cfg.scripts_dir = cfg.scripts_dir:gsub("/+$", "") + + cfg.variables.ROCKS_TREE = cfg.root_dir + cfg.variables.SCRIPTS_DIR = cfg.scripts_dir + + if flags["from"] then + if flags["from"] == true then + die("Argument error: use --from=") + end + local protocol, path = fs.split_url(flags["from"]) + table.insert(cfg.rocks_servers, 1, protocol.."://"..path) + end + + if flags["only-from"] then + if flags["only-from"] == true then + die("Argument error: use --only-from=") + end + cfg.rocks_servers = { flags["only-from"] } + end + + local command + + if flags["version"] then + print(program_name.." "..program_version) + print(program_description) + print() + os.exit(0) + elseif flags["help"] or #nonflags == 0 then + command = "help" + args = nonflags + else + command = nonflags[1] + for i, arg in ipairs(args) do + if arg == command then + table.remove(args, i) + break + end + end + end + + if command ~= "help" then + for k, v in pairs(cmdline_vars) do + cfg.variables[k] = v + end + end + + command = command:gsub("-", "_") + if commands[command] then + local xp, ok, err = xpcall(function() return commands[command].run(unpack(args)) end, function(err) + die(debug.traceback("LuaRocks "..program_version + .." bug (please report at luarocks-developers@lists.luaforge.net).\n" + ..err, 2)) + end) + if xp and (not ok) then + die(err) + end + else + die("Unknown command: "..command) + end + + util.run_scheduled_functions() +end diff --git a/src/luarocks/deps.lua b/src/luarocks/deps.lua new file mode 100644 index 00000000..d5a64e52 --- /dev/null +++ b/src/luarocks/deps.lua @@ -0,0 +1,618 @@ + +--- Dependency handling functions. +-- Dependencies are represented in LuaRocks through strings with +-- a package name followed by a comma-separated list of constraints. +-- Each constraint consists of an operator and a version number. +-- In this string format, version numbers are represented as +-- naturally as possible, like they are used by upstream projects +-- (e.g. "2.0beta3"). Internally, LuaRocks converts them to a purely +-- numeric representation, allowing comparison following some +-- "common sense" heuristics. The precise specification of the +-- comparison criteria is the source code of this module, but the +-- test/test_deps.lua file included with LuaRocks provides some +-- insights on what these criteria are. +module("luarocks.deps", package.seeall) + +local rep = require("luarocks.rep") +local search = require("luarocks.search") +local install = require("luarocks.install") +local cfg = require("luarocks.cfg") +local manif = require("luarocks.manif") +local fs = require("luarocks.fs") +local fetch = require("luarocks.fetch") +local path = require("luarocks.path") + +local operators = { + ["=="] = "==", + ["~="] = "~=", + [">"] = ">", + ["<"] = "<", + [">="] = ">=", + ["<="] = "<=", + ["~>"] = "~>", + -- plus some convenience translations + [""] = "==", + ["="] = "==", + ["!="] = "~=" +} + +local deltas = { + scm = 1000, + cvs = 1000, + rc = -1000, + pre = -10000, + beta = -100000, + alpha = -1000000 +} + +local version_mt = { + --- Equality comparison for versions. + -- All version numbers must be equal. + -- If both versions have revision numbers, they must be equal; + -- otherwise the revision number is ignored. + -- @param v1 table: version table to compare. + -- @param v2 table: version table to compare. + -- @return boolean: true if they are considered equivalent. + __eq = function(v1, v2) + if #v1 ~= #v2 then + return false + end + for i = 1, #v1 do + if v1[i] ~= v2[i] then + return false + end + end + if v1.revision and v2.revision then + return (v1.revision == v2.revision) + end + return true + end, + --- Size comparison for versions. + -- All version numbers are compared. + -- If both versions have revision numbers, they are compared; + -- otherwise the revision number is ignored. + -- @param v1 table: version table to compare. + -- @param v2 table: version table to compare. + -- @return boolean: true if v1 is considered lower than v2. + __lt = function(v1, v2) + for i = 1, math.max(#v1, #v2) do + local v1i, v2i = v1[i] or 0, v2[i] or 0 + if v1i ~= v2i then + return (v1i < v2i) + end + end + if v1.revision and v2.revision then + return (v1.revision < v2.revision) + end + return false + end +} + +local version_cache = {} +setmetatable(version_cache, { + __mode = "kv" +}) + +--- Parse a version string, converting to table format. +-- A version table contains all components of the version string +-- converted to numeric format, stored in the array part of the table. +-- If the version contains a revision, it is stored numerically +-- in the 'revision' field. The original string representation of +-- the string is preserved in the 'string' field. +-- Returned version tables use a metatable +-- allowing later comparison through relational operators. +-- @param vstring string: A version number in string format. +-- @return table or nil: A version table or nil +-- if the input string contains invalid characters. +function parse_version(vstring) + if not vstring then return nil end + assert(type(vstring) == "string") + + local cached = version_cache[vstring] + if cached then + return cached + end + + local version = {} + local i = 1 + + local function add_token(number) + version[i] = version[i] and version[i] + number/100000 or number + i = i + 1 + end + + -- trim leading and trailing spaces + vstring = vstring:match("^%s*(.*)%s*$") + version.string = vstring + -- store revision separately if any + local main, revision = vstring:match("(.*)%-(%d+)$") + if revision then + vstring = main + version.revision = tonumber(revision) + end + while #vstring > 0 do + -- extract a number + local token, rest = vstring:match("^(%d+)[%.%-%_]*(.*)") + if token then + add_token(tonumber(token)) + else + -- extract a word + token, rest = vstring:match("^(%a+)[%.%-%_]*(.*)") + if not token then + return nil + end + local last = #version + version[i] = deltas[token] or (token:byte() / 1000) + end + vstring = rest + end + setmetatable(version, version_mt) + version_cache[vstring] = version + return version +end + +--- Utility function to compare version numbers given as strings. +-- @param a string: one version. +-- @param b string: another version. +-- @return boolean: True if a > b. +function compare_versions(a, b) + return parse_version(a) > parse_version(b) +end + +--- Consumes a constraint from a string, converting it to table format. +-- For example, a string ">= 1.0, > 2.0" is converted to a table in the +-- format {op = ">=", version={1,0}} and the rest, "> 2.0", is returned +-- back to the caller. +-- @param input string: A list of constraints in string format. +-- @return (table, string) or nil: A table representing the same +-- constraints and the string with the unused input, or nil if the +-- input string is invalid. +local function parse_constraint(input) + assert(type(input) == "string") + + local no_upgrade, op, version, rest = input:match("^(@?)([<>=~!]*)%s*([%w%.%_%-]+)[%s,]*(.*)") + op = operators[op] + version = parse_version(version) + if not op or not version then return nil end + return { op = op, version = version, no_upgrade = no_upgrade=="@" and true or nil }, rest +end + +--- Convert a list of constraints from string to table format. +-- For example, a string ">= 1.0, < 2.0" is converted to a table in the format +-- {{op = ">=", version={1,0}}, {op = "<", version={2,0}}}. +-- Version tables use a metatable allowing later comparison through +-- relational operators. +-- @param input string: A list of constraints in string format. +-- @return table or nil: A table representing the same constraints, +-- or nil if the input string is invalid. +function parse_constraints(input) + assert(type(input) == "string") + + local constraints, constraint = {}, nil + while #input > 0 do + constraint, input = parse_constraint(input) + if constraint then + table.insert(constraints, constraint) + else + return nil + end + end + return constraints +end + +--- Convert a dependency from string to table format. +-- For example, a string "foo >= 1.0, < 2.0" +-- is converted to a table in the format +-- {name = "foo", constraints = {{op = ">=", version={1,0}}, +-- {op = "<", version={2,0}}}}. Version tables use a metatable +-- allowing later comparison through relational operators. +-- @param dep string: A dependency in string format +-- as entered in rockspec files. +-- @return table or nil: A table representing the same dependency relation, +-- or nil if the input string is invalid. +function parse_dep(dep) + assert(type(dep) == "string") + + local name, rest = dep:match("^%s*(%a[%w%-]*%w)%s*(.*)") + if not name then return nil end + local constraints = parse_constraints(rest) + if not constraints then return nil end + return { name = name, constraints = constraints } +end + +--- Convert a version table to a string. +-- @param v table: The version table +-- @param internal boolean or nil: Whether to display versions in their +-- internal representation format or how they were specified. +-- @return string: The dependency information pretty-printed as a string. +function show_version(v, internal) + assert(type(v) == "table") + assert(type(internal) == "boolean" or not internal) + + return (internal + and table.concat(v, ":")..(v.revision and tostring(v.revision) or "") + or v.string) +end + +--- Convert a dependency in table format to a string. +-- @param dep table: The dependency in table format +-- @param internal boolean or nil: Whether to display versions in their +-- internal representation format or how they were specified. +-- @return string: The dependency information pretty-printed as a string. +function show_dep(dep, internal) + assert(type(dep) == "table") + assert(type(internal) == "boolean" or not internal) + + local pretty = {} + for _, c in ipairs(dep.constraints) do + table.insert(pretty, c.op .. " " .. show_version(c.version, internal)) + end + return dep.name.." "..table.concat(pretty, ", ") +end + +--- A more lenient check for equivalence between versions. +-- This returns true if the requested components of a version +-- match and ignore the ones that were not given. For example, +-- when requesting "2", then "2", "2.1", "2.3.5-9"... all match. +-- When requesting "2.1", then "2.1", "2.1.3" match, but "2.2" +-- doesn't. +-- @param version string or table: Version to be tested; may be +-- in string format or already parsed into a table. +-- @param requested string or table: Version requested; may be +-- in string format or already parsed into a table. +-- @return boolean: True if the tested version matches the requested +-- version, false otherwise. +local function partial_match(version, requested) + assert(type(version) == "string" or type(version) == "table") + assert(type(requested) == "string" or type(version) == "table") + + if type(version) ~= "table" then version = parse_version(version) end + if type(requested) ~= "table" then requested = parse_version(requested) end + if not version or not requested then return false end + + for i, ri in ipairs(requested) do + local vi = version[i] or 0 + if ri ~= vi then return false end + end + if requested.revision then + return requested.revision == version.revision + end + return true +end + +--- Check if a version satisfies a set of constraints. +-- @param version table: A version in table format +-- @param constraints table: An array of constraints in table format. +-- @return boolean: True if version satisfies all constraints, +-- false otherwise. +function match_constraints(version, constraints) + assert(type(version) == "table") + assert(type(constraints) == "table") + local ok = true + setmetatable(version, version_mt) + for _, constr in pairs(constraints) do + local constr_version = constr.version + setmetatable(constr.version, version_mt) + if constr.op == "==" then ok = version == constr_version + elseif constr.op == "~=" then ok = version ~= constr_version + elseif constr.op == ">" then ok = version > constr_version + elseif constr.op == "<" then ok = version < constr_version + elseif constr.op == ">=" then ok = version >= constr_version + elseif constr.op == "<=" then ok = version <= constr_version + elseif constr.op == "~>" then ok = partial_match(version, constr_version) + end + if not ok then break end + end + return ok +end + +--- Attempt to match a dependency to an installed rock. +-- @param dep table: A dependency parsed in table format. +-- @param blacklist table: Versions that can't be accepted. Table where keys +-- are program versions and values are 'true'. +-- @return table or nil: A table containing fields 'name' and 'version' +-- representing an installed rock which matches the given dependency, +-- or nil if it could not be matched. +local function match_dep(dep, blacklist) + assert(type(dep) == "table") + + local versions + if dep.name == "lua" then + versions = { "5.1" } + else + versions = manif.get_versions(dep.name) + end + if not versions then + return nil + end + if blacklist then + local i = 1 + while versions[i] do + if blacklist[versions[i]] then + table.remove(versions, i) + else + i = i + 1 + end + end + end + local candidates = {} + for _, vstring in ipairs(versions) do + local version = parse_version(vstring) + if match_constraints(version, dep.constraints) then + table.insert(candidates, version) + end + end + if #candidates == 0 then + return nil + else + table.sort(candidates) + return { + name = dep.name, + version = candidates[#candidates].string + } + end +end + +--- Attempt to match dependencies of a rockspec to installed rocks. +-- @param rockspec table: The rockspec loaded as a table. +-- @param blacklist table or nil: Program versions to not use as valid matches. +-- Table where keys are program names and values are tables where keys +-- are program versions and values are 'true'. +-- @return table, table: A table where keys are dependencies parsed +-- in table format and values are tables containing fields 'name' and +-- version' representing matches, and a table of missing dependencies +-- parsed as tables. +function match_deps(rockspec, blacklist) + assert(type(rockspec) == "table") + assert(type(blacklist) == "table" or not blacklist) + local matched, missing, no_upgrade = {}, {}, {} + + for _, dep in ipairs(rockspec.dependencies) do + local found = match_dep(dep, blacklist and blacklist[dep.name] or nil) + if found then + if dep.name ~= "lua" then + matched[dep] = found + end + else + if dep.constraints[1] and dep.constraints[1].no_upgrade then + no_upgrade[dep.name] = dep + else + missing[dep.name] = dep + end + end + end + return matched, missing, no_upgrade +end + +--- Return a set of values of a table. +-- @param tbl table: The input table. +-- @return table: The array of keys. +local function values_set(tbl) + local set = {} + for _, v in pairs(tbl) do + set[v] = true + end + return set +end + +--- Check dependencies of a rock and attempt to install any missing ones. +-- Packages are installed using the LuaRocks "install" command. +-- Aborts the program if a dependency could not be fulfilled. +-- @param rockspec table: A rockspec in table format. +-- @return boolean or (nil, string): True if no errors occurred, or +-- nil and an error message if any test failed. +function fulfill_dependencies(rockspec) + + if rockspec.supported_platforms then + if not platforms_set then + platforms_set = values_set(cfg.platforms) + end + local supported = nil + for _, plat in pairs(rockspec.supported_platforms) do + local neg, plat = plat:match("^(!?)(.*)") + if neg == "!" then + if platforms_set[plat] then + return nil, "This rockspec for "..rockspec.package.." does not support "..plat.." platforms." + end + else + if platforms_set[plat] then + supported = true + else + if supported == nil then + supported = false + end + end + end + end + if supported == false then + local plats = table.concat(cfg.platforms, ", ") + return nil, "This rockspec for "..rockspec.package.." does not support "..plats.." platforms." + end + end + + local matched, missing, no_upgrade = match_deps(rockspec) + + if next(no_upgrade) then + print("Missing dependencies for "..rockspec.name.." "..rockspec.version..":") + for _, dep in pairs(no_upgrade) do + print(show_dep(dep)) + end + if next(missing) then + for _, dep in pairs(missing) do + print(show_dep(dep)) + end + end + print() + for _, dep in pairs(no_upgrade) do + print("This version of "..rockspec.name.." is designed for use with") + print(show_dep(dep)..", but is configured to avoid upgrading it") + print("automatically. Please upgrade "..dep.name.." with") + print(" luarocks install "..dep.name) + print("or choose an older version of "..rockspec.name.." with") + print(" luarocks search "..rockspec.name) + end + return nil, "Failed matching dependencies." + end + + if next(missing) then + print() + print("Missing dependencies for "..rockspec.name..":") + for _, dep in pairs(missing) do + print(show_dep(dep)) + end + print() + + for _, dep in pairs(missing) do + -- Double-check in case dependency was filled during recursion. + if not match_dep(dep) then + local rock = search.find_suitable_rock(dep) + if not rock then + return nil, "Could not find a rock to satisfy dependency: "..show_dep(dep) + end + local ok, err = install.run(rock) + if not ok then + return nil, "Failed installing dependency: "..rock.." - "..err + end + end + end + end + return true +end + +--- Set up path-related variables for external dependencies. +-- For each key in the external_dependencies table in the +-- rockspec file, four variables are created: _DIR, _BINDIR, +-- _INCDIR and _LIBDIR. These are not overwritten +-- if already set (e.g. by the LuaRocks config file or through the +-- command-line). Values in the external_dependencies table +-- are tables that may contain a "header" or a "library" field, +-- with filenames to be tested for existence. +-- @param rockspec table: The rockspec table. +-- @param mode string: if "build" is given, checks all files; +-- if "install" is given, do not scan for headers. +-- @return boolean or (nil, string): True if no errors occurred, or +-- nil and an error message if any test failed. +function check_external_deps(rockspec, mode) + assert(type(rockspec) == "table") + + local vars = rockspec.variables + local patterns = cfg.external_deps_patterns + local subdirs = cfg.external_deps_subdirs + if mode == "install" then + patterns = cfg.runtime_external_deps_patterns + subdirs = cfg.runtime_external_deps_subdirs + end + local dirs = { + BINDIR = { subdir = subdirs.bin, testfile = "program", pattern = patterns.bin }, + INCDIR = { subdir = subdirs.include, testfile = "header", pattern = patterns.include }, + LIBDIR = { subdir = subdirs.lib, testfile = "library", pattern = patterns.lib } + } + if mode == "install" then + dirs.INCDIR = nil + end + if rockspec.external_dependencies then + for name, files in pairs(rockspec.external_dependencies) do + local ok = true + local failed_file = nil + for _, extdir in ipairs(cfg.external_deps_dirs) do + ok = true + local prefix = vars[name.."_DIR"] + if not prefix then + prefix = extdir + end + for dirname, dirdata in pairs(dirs) do + dirdata.dir = vars[name.."_"..dirname] or fs.make_path(prefix, dirdata.subdir) + local file = files[dirdata.testfile] + if file then + local files = {} + if not file:match("%.") then + for _, pattern in ipairs(dirdata.pattern) do + table.insert(files, pattern:gsub("?", file)) + end + else + table.insert(files, file) + end + local found = false + failed_file = nil + for _, f in pairs(files) do + if f:match("%.so$") or f:match("%.dylib$") or f:match("%.dll$") then + f = f:gsub("%.[^.]+$", "."..cfg.external_lib_extension) + end + local testfile = fs.make_path(dirdata.dir, f) + if fs.exists(testfile) then + found = true + break + else + if failed_file then + failed_file = failed_file .. ", or " .. f + else + failed_file = f + end + end + end + if not found then + ok = false + break + end + end + end + if ok then + for dirname, dirdata in pairs(dirs) do + vars[name.."_"..dirname] = dirdata.dir + end + vars[name.."_DIR"] = prefix + break + end + end + if not ok then + return nil, "Could not find expected file "..failed_file.." for "..name.." -- you may have to install "..name.." in your system and/or set the "..name.."_DIR variable" + end + end + end + return true +end + +--- Recursively scan dependencies, to build a transitive closure of all +-- dependent packages. +-- @param results table: The results table being built. +-- @param name string: Package name. +-- @param version string: Package version. +-- @return (table, table): The results and a table of missing dependencies. +function scan_deps(results, missing, manifest, name, version) + assert(type(results) == "table") + assert(type(missing) == "table") + assert(type(name) == "string") + assert(type(version) == "string") + + local err + if results[name] then + return results, missing + end + if not manifest.dependencies then manifest.dependencies = {} end + local dependencies = manifest.dependencies + if not dependencies[name] then dependencies[name] = {} end + local dependencies_name = dependencies[name] + local deplist = dependencies_name[version] + local rockspec, err + if not deplist then + rockspec, err = fetch.load_local_rockspec(path.rockspec_file(name, version)) + if err then + missing[name.." "..version] = true + return results, missing + end + dependencies_name[version] = rockspec.dependencies + else + rockspec = { dependencies = deplist } + end + local matched, failures = match_deps(rockspec) + for _, match in pairs(matched) do + results, missing = scan_deps(results, missing, manifest, match.name, match.version) + end + if next(failures) then + for _, failure in pairs(failures) do + missing[show_dep(failure)] = true + end + end + results[name] = version + return results, missing +end diff --git a/src/luarocks/fetch.lua b/src/luarocks/fetch.lua new file mode 100644 index 00000000..b61fab28 --- /dev/null +++ b/src/luarocks/fetch.lua @@ -0,0 +1,284 @@ + +--- Functions related to fetching and loading local and remote files. +module("luarocks.fetch", package.seeall) + +local fs = require("luarocks.fs") +local type_check = require("luarocks.type_check") +local path = require("luarocks.path") +local deps = require("luarocks.deps") +local persist = require("luarocks.persist") +local util = require("luarocks.util") + +--- Fetch a local or remote file. +-- Make a remote or local URL/pathname local, fetching the file if necessary. +-- Other "fetch" and "load" functions use this function to obtain files. +-- If a local pathname is given, it is returned as a result. +-- @param url string: a local pathname or a remote URL. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @return string or (nil, string): the absolute local pathname for the +-- fetched file, or nil and a message in case of errors. +function fetch_url(url, filename) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + local protocol, pathname = fs.split_url(url) + if protocol == "file" then + return fs.absolute_name(pathname) + elseif protocol == "http" or protocol == "ftp" or protocol == "https" then + local ok = fs.download(url) + if not ok then + return nil, "Failed downloading "..url + end + return fs.make_path(fs.current_dir(), filename or fs.base_name(url)) + else + return nil, "Unsupported protocol "..protocol + end +end + +--- For remote URLs, create a temporary directory and download URL inside it. +-- This temporary directory will be deleted on program termination. +-- For local URLs, just return the local pathname and its directory. +-- @param url string: URL to be downloaded +-- @param tmpname string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @param filename string or nil: local filename of URL to be downloaded, +-- in case it can't be inferred from the URL. +-- @return (string, string) or (nil, string): absolute local pathname of +-- the fetched file and temporary directory name; or nil and an error message. +function fetch_url_at_temp_dir(url, tmpname, filename) + assert(type(url) == "string") + assert(type(tmpname) == "string") + assert(type(filename) == "string" or not filename) + filename = filename or fs.base_name(url) + + local protocol, pathname = fs.split_url(url) + if protocol == "file" then + return pathname, fs.dir_name(pathname) + else + local dir = fs.make_temp_dir(tmpname) + if not dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, dir) + fs.change_dir(dir) + local file, err = fetch_url(url, filename) + if not file then + return nil, "Error fetching file: "..err + end + fs.pop_dir() + return file, dir + end +end + +--- Obtain a rock and unpack it. +-- If a directory is not given, a temporary directory will be created, +-- which will be deleted on program termination. +-- @param rock_file string: URL or filename of the rock. +-- @param dest string or nil: if given, directory will be used as +-- a permanent destination. +-- @return string or (nil, string): the directory containing the contents +-- of the unpacked rock. +function fetch_and_unpack_rock(rock_file, dest) + assert(type(rock_file) == "string") + assert(type(dest) == "string" or not dest) + + local name = fs.base_name(rock_file):match("(.*)%.[^.]*%.rock") + + local rock_file, err = fetch_url_at_temp_dir(rock_file,"luarocks-rock-"..name) + if not rock_file then + return nil, "Could not fetch rock file: " .. err + end + + rock_file = fs.absolute_name(rock_file) + local dir + if dest then + dir = dest + fs.make_dir(dir) + else + dir = fs.make_temp_dir(name) + end + if not dest then + util.schedule_function(fs.delete, dir) + end + fs.change_dir(dir) + local ok = fs.unzip(rock_file) + if not ok then + return nil, "Failed unpacking rock file: " .. rock_file + end + fs.pop_dir() + return dir +end + +--- Back-end function that actually loads the local rockspec. +-- Performs some validation and postprocessing of the rockspec contents. +-- @param file string: The local filename of the rockspec file. +-- @return table or (nil, string): A table representing the rockspec +-- or nil followed by an error message. +function load_local_rockspec(filename) + assert(type(filename) == "string") + + local rockspec, err = persist.load_into_table(filename) + if not rockspec then + return nil, "Could not load rockspec file "..filename.." ("..err..")" + end + + local ok, err = type_check.type_check_rockspec(rockspec) + if not ok then + return nil, filename..": "..err + end + + if rockspec.rockspec_format then + if deps.compare_versions(rockspec.rockspec_format, type_check.rockspec_format) then + return nil, "Rockspec format "..rockspec.rockspec_format.." is not supported, please upgrade LuaRocks." + end + end + + util.platform_overrides(rockspec.build) + util.platform_overrides(rockspec.dependencies) + util.platform_overrides(rockspec.external_dependencies) + util.platform_overrides(rockspec.source) + util.platform_overrides(rockspec.hooks) + + local basename = fs.base_name(filename) + rockspec.name = basename:match("(.*)-[^-]*-[0-9]*") + if not rockspec.name then + return nil, "Expected filename in format 'name-version-revision.rockspec'." + end + + local protocol, pathname = fs.split_url(rockspec.source.url) + if protocol == "http" or protocol == "https" or protocol == "ftp" or protocol == "file" then + rockspec.source.file = rockspec.source.file or fs.base_name(rockspec.source.url) + end + rockspec.source.protocol, rockspec.source.pathname = protocol, pathname + + -- Temporary compatibility + if not rockspec.source.module then + rockspec.source.module = rockspec.source.cvs_module + rockspec.source.tag = rockspec.source.cvs_tag + end + + local name_version = rockspec.package:lower() .. "-" .. rockspec.version + if basename ~= name_version .. ".rockspec" then + return nil, "Inconsistency between rockspec filename ("..basename..") and its contents ("..name_version..".rockspec)." + end + + rockspec.local_filename = filename + local filebase = rockspec.source.file or rockspec.source.url + local base = fs.base_name(filebase) + base = base:gsub("%.[^.]*$", ""):gsub("%.tar$", "") + rockspec.source.dir = rockspec.source.dir + or rockspec.source.module + or ((filebase:match(".lua$") or filebase:match(".c$")) and ".") + or base + if rockspec.dependencies then + for i = 1, #rockspec.dependencies do + local parsed = deps.parse_dep(rockspec.dependencies[i]) + if not parsed then + return nil, "Parse error processing dependency '"..rockspec.dependencies[i].."'" + end + rockspec.dependencies[i] = parsed + end + else + rockspec.dependencies = {} + end + local ok, err = path.configure_paths(rockspec) + if err then + return nil, "Error verifying paths: "..err + end + + return rockspec +end + +--- Load a local or remote rockspec into a table. +-- This is the entry point for the LuaRocks tools. +-- Only the LuaRocks runtime loader should use +-- load_local_rockspec directly. +-- @param filename string: Local or remote filename of a rockspec. +-- @return table or (nil, string): A table representing the rockspec +-- or nil followed by an error message. +function load_rockspec(filename) + assert(type(filename) == "string") + + local name = fs.base_name(filename):match("(.*)%.rockspec") + if not name then + return nil, "Filename '"..filename.."' does not look like a rockspec." + end + + local filename, err = fetch_url_at_temp_dir(filename,"luarocks-rockspec-"..name) + if not filename then + return nil, err + end + + return load_local_rockspec(filename) +end + +--- Download sources for building a rock using the basic URL downloader. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Whether to extract the sources from +-- the fetched source tarball or not. +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function get_sources(rockspec, extract, dest_dir) + assert(type(rockspec) == "table") + assert(type(extract) == "boolean") + assert(type(dest_dir) == "string" or not dest_dir) + + local url = rockspec.source.url + local name = rockspec.name.."-"..rockspec.version + local filename = rockspec.source.file + local source_file, dir, err + if dest_dir then + fs.change_dir(dest_dir) + source_file, err = fetch_url(url, filename) + fs.pop_dir() + dir = dest_dir + else + source_file, dir = fetch_url_at_temp_dir(url, "luarocks-source-"..name, filename) + end + if not source_file then + return nil, err or dir + end + if rockspec.source.md5 then + if not fs.check_md5(source_file, rockspec.source.md5) then + return nil, "MD5 check for "..filename.." has failed." + end + end + if extract then + fs.change_dir(dir) + fs.unpack_archive(rockspec.source.file) + fs.pop_dir() + end + return source_file, dir +end + +--- Download sources for building a rock, calling the appropriate protocol method. +-- @param rockspec table: The rockspec table +-- @param extract boolean: When downloading compressed formats, whether to extract +-- the sources from the fetched archive or not. +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function fetch_sources(rockspec, extract, dest_dir) + assert(type(rockspec) == "table") + assert(type(extract) == "boolean") + assert(type(dest_dir) == "string" or not dest_dir) + + local protocol = rockspec.source.protocol + local proto + if protocol == "http" or protocol == "https" or protocol == "ftp" or protocol == "file" then + proto = require("luarocks.fetch") + else + ok, proto = pcall(require, "luarocks.fetch."..protocol) + if not ok then + return nil, "Unknown protocol "..protocol + end + end + + return proto.get_sources(rockspec, extract, dest_dir) +end diff --git a/src/luarocks/fetch/cvs.lua b/src/luarocks/fetch/cvs.lua new file mode 100644 index 00000000..291388cb --- /dev/null +++ b/src/luarocks/fetch/cvs.lua @@ -0,0 +1,43 @@ + +--- Fetch back-end for retrieving sources from CVS. +module("luarocks.fetch.cvs", package.seeall) + +local fs = require("luarocks.fs") +local util = require("luarocks.util") + +--- Download sources for building a rock, using CVS. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function get_sources(rockspec, extract, dest_dir) + assert(type(rockspec) == "table") + assert(type(dest_dir) == "string" or not dest_dir) + + local name_version = rockspec.name .. "-" .. rockspec.version + local module = rockspec.source.module or fs.base_name(rockspec.source.url) + local command = {"cvs", "-d"..rockspec.source.pathname, "export", module} + if rockspec.source.tag then + table.insert(command, 4, "-r") + table.insert(command, 5, rockspec.source.tag) + end + local dir + if not dest_dir then + dir = fs.make_temp_dir(name_version) + if not dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, dir) + else + dir = dest_dir + end + fs.change_dir(dir) + if not fs.execute(unpack(command)) then + return nil, "Failed fetching files from CVS." + end + fs.pop_dir() + return module, dir +end + diff --git a/src/luarocks/fetch/git.lua b/src/luarocks/fetch/git.lua new file mode 100644 index 00000000..d0d204a9 --- /dev/null +++ b/src/luarocks/fetch/git.lua @@ -0,0 +1,53 @@ + +--- Fetch back-end for retrieving sources from GIT. +module("luarocks.fetch.git", package.seeall) + +local fs = require("luarocks.fs") +local util = require("luarocks.util") + +--- Download sources for building a rock, using git. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function get_sources(rockspec, extract, dest_dir) + assert(type(rockspec) == "table") + assert(type(dest_dir) == "string" or not dest_dir) + + local name_version = rockspec.name .. "-" .. rockspec.version + local module = fs.base_name(rockspec.source.url) + -- Strip off .git from base name if present + module = module:gsub("%.git$", "") + local command = {"git", "clone", rockspec.source.url, module} + local checkout_command + local tag_or_branch = rockspec.source.tag or rockspec.source.branch + if tag_or_branch then + checkout_command = {"git", "checkout", tag_or_branch} + end + local dir + if not dest_dir then + dir = fs.make_temp_dir(name_version) + if not dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, dir) + else + dir = dest_dir + end + fs.change_dir(dir) + if not fs.execute(unpack(command)) then + return nil, "Failed fetching files from GIT while cloning." + end + if checkout_command then + fs.change_dir(module) + if not fs.execute(unpack(checkout_command)) then + return nil, "Failed fetching files from GIT while getting tag/branch." + end + fs.pop_dir() + end + fs.pop_dir() + return module, dir +end + diff --git a/src/luarocks/fetch/sscm.lua b/src/luarocks/fetch/sscm.lua new file mode 100644 index 00000000..a2c70b96 --- /dev/null +++ b/src/luarocks/fetch/sscm.lua @@ -0,0 +1,40 @@ + +--- Fetch back-end for retrieving sources from Surround SCM Server +module("luarocks.fetch.sscm", package.seeall) + +local fs = require("luarocks.fs") + +--- Download sources via Surround SCM Server for building a rock. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function get_sources(rockspec, extract, dest_dir) + assert(type(rockspec) == "table") + assert(type(dest_dir) == "string" or not dest_dir) + + local module = rockspec.source.module or fs.base_name(rockspec.source.url) + local branch, repository = string.match(rockspec.source.pathname, "^([^/]*)/(.*)") + if not branch or not repository then + return nil, "Error retrieving branch and repository from rockspec." + end + -- Search for working directory. + local working_dir + local tmp = io.popen(string.format([[sscm property "/" -d -b%s -p%s]], branch, repository)) + for line in tmp:lines() do + --%c because a chr(13) comes in the end. + working_dir = string.match(line, "Working directory:[%s]*(.*)%c$") + if working_dir then break end + end + tmp:close() + if not working_dir then + return nil, "Error retrieving working directory from SSCM." + end + if not fs.execute([["sscm"]], "get", "*", "-e" , "-r", "-b"..branch, "-p"..repository, "-tmodify", "-wreplace") then + return nil, "Failed fetching files from SSCM." + end + -- FIXME: This function does not honor the dest_dir parameter. + return module, working_dir +end diff --git a/src/luarocks/fs.lua b/src/luarocks/fs.lua new file mode 100644 index 00000000..7027167a --- /dev/null +++ b/src/luarocks/fs.lua @@ -0,0 +1,43 @@ + +local rawset = rawset + +--- Proxy module for filesystem and platform abstractions. +-- All code using "fs" code should require "luarocks.fs", +-- and not the various platform-specific implementations. +-- However, see the documentation of the implementation +-- for the API reference. +module("luarocks.fs", package.seeall) + +local cfg = require("luarocks.cfg") + +local fs_impl = nil +for _, platform in ipairs(cfg.platforms) do + local ok, result = pcall(require, "luarocks.fs."..platform) + if ok then + fs_impl = result + if fs_impl then + break + end + end +end + +local fs_unix = require("luarocks.fs.unix") + +local fs_mt = { + __index = function(t, k) + local impl = fs_impl and fs_impl[k] + if not impl then + impl = fs_unix[k] + end + rawset(t, k, impl) + return impl + end +} + +setmetatable(luarocks.fs, fs_mt) + +fs_unix.init_fs_functions(luarocks.fs) +if fs_impl then + fs_impl.init_fs_functions(luarocks.fs) +end + diff --git a/src/luarocks/fs/unix.lua b/src/luarocks/fs/unix.lua new file mode 100644 index 00000000..2a849329 --- /dev/null +++ b/src/luarocks/fs/unix.lua @@ -0,0 +1,519 @@ + +local assert, type, table, io, package, math, os, ipairs = + assert, type, table, io, package, math, os, ipairs + +--- Unix implementation of filesystem and platform abstractions. +module("luarocks.fs.unix", package.seeall) + +local cfg = require("luarocks.cfg") + +math.randomseed(os.time()) + +dir_stack = {} + +local fs_absolute_name, + fs_base_name, + fs_copy, + fs_current_dir, + fs_dir_stack, + fs_execute, + fs_execute_string, + fs_is_dir, + fs_make_dir, + fs_make_path, + fs_exists, + fs_find, + fs_Q + +function init_fs_functions(impl) + fs_absolute_name = impl.absolute_name + fs_base_name = impl.base_name + fs_copy = impl.copy + fs_current_dir = impl.current_dir + fs_dir_stack = impl.dir_stack + fs_execute = impl.execute + fs_execute_string = impl.execute_string + fs_is_dir = impl.is_dir + fs_make_dir = impl.make_dir + fs_make_path = impl.make_path + fs_exists = impl.exists + fs_find = impl.find + fs_Q = impl.Q +end + +dir_separator = "/" + +--- Quote argument for shell processing. +-- Adds single quotes and escapes. +-- @param arg string: Unquoted argument. +-- @return string: Quoted argument. +function Q(arg) + assert(type(arg) == "string") + + return "'" .. arg:gsub("\\", "\\\\"):gsub("'", "'\\''") .. "'" +end + +--- Obtain current directory. +-- Uses the module's internal dir stack. +-- @return string: the absolute pathname of the current directory. +function current_dir() + local current = os.getenv("PWD") + if not current then + local pipe = io.popen("pwd") + current = pipe:read("*l") + pipe:close() + end + for _, dir in ipairs(fs_dir_stack) do + current = fs_absolute_name(dir, current) + end + return current +end + +--- Run the given command. +-- The command is executed in the current directory in the dir stack. +-- @param cmd string: No quoting/escaping is applied to the command. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function execute_string(cmd) + if os.execute("cd " .. fs_Q(fs_current_dir()) .. " && " .. cmd) == 0 then + return true + else + return false + end +end + +--- Run the given command, quoting its arguments. +-- The command is executed in the current directory in the dir stack. +-- @param command string: The command to be executed. No quoting/escaping +-- is applied. +-- @param ... Strings containing additional arguments, which are quoted. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function execute(command, ...) + assert(type(command) == "string") + + for _, arg in ipairs({...}) do + assert(type(arg) == "string") + command = command .. " " .. fs_Q(arg) + end + return fs_execute_string(command) +end + +--- Change the current directory. +-- Uses the module's internal dir stack. This does not have exact +-- semantics of chdir, as it does not handle errors the same way, +-- but works well for our purposes for now. +-- @param dir string: The directory to switch to. +function change_dir(dir) + assert(type(dir) == "string") + table.insert(fs_dir_stack, dir) +end + +--- Change directory to root. +-- Allows leaving a directory (e.g. for deleting it) in +-- a crossplatform way. +function change_dir_to_root() + table.insert(fs_dir_stack, "/") +end + +--- Change working directory to the previous in the dir stack. +function pop_dir() + table.remove(fs_dir_stack) +end + +--- Create a directory if it does not already exist. +-- If any of the higher levels in the path name does not exist +-- too, they are created as well. +-- @param dir string: pathname of directory to create. +-- @return boolean: true on success, false on failure. +function make_dir(dir) + assert(dir) + return fs_execute("mkdir -p", dir) +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param dir string: pathname of directory to remove. +function remove_dir_if_empty(dir) + assert(dir) + fs_execute_string("rmdir "..fs_Q(dir).." 1> /dev/null 2> /dev/null") +end + +--- Copy a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function copy(src, dest) + assert(src and dest) + if fs_execute("cp", src, dest) then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Recursively copy the contents of a directory. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function copy_contents(src, dest) + assert(src and dest) + if fs_execute_string("cp -a "..fs_Q(src).."/* "..fs_Q(dest).." 1> /dev/null 2>/dev/null") then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Delete a file or a directory and all its contents. +-- For safety, this only accepts absolute paths. +-- @param arg string: Pathname of source +-- @return boolean: true on success, false on failure. +function delete(arg) + assert(arg) + assert(arg:sub(1,1) == "/") + return fs_execute_string("rm -rf " .. fs_Q(arg) .. " 1> /dev/null 2>/dev/null") +end + +--- List the contents of a directory. +-- @param at string or nil: directory to list (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. +function dir(at) + assert(type(at) == "string" or not at) + if not at then + at = fs_current_dir() + end + if not fs_is_dir(at) then + return {} + end + local result = {} + local pipe = io.popen("cd "..fs_Q(at).." && ls") + for file in pipe:lines() do + table.insert(result, file) + end + pipe:close() + return result +end + +--- Recursively scan the contents of a directory. +-- @param at string or nil: directory to scan (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. +function find(at) + assert(type(at) == "string" or not at) + if not at then + at = fs_current_dir() + end + if not fs_is_dir(at) then + return {} + end + local result = {} + local pipe = io.popen("cd "..fs_Q(at).." && find * 2>/dev/null") + for file in pipe:lines() do + table.insert(result, file) + end + pipe:close() + return result +end + +--- Compress files in a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @param ... Filenames to be stored in the archive are given as +-- additional arguments. +-- @return boolean: true on success, false on failure. +function zip(zipfile, ...) + return fs_execute("zip -r", zipfile, ...) +end + +--- Uncompress files from a .zip archive. +-- @param zipfile string: pathname of .zip archive to be extracted. +-- @return boolean: true on success, false on failure. +function unzip(zipfile) + assert(zipfile) + return fs_execute("unzip", zipfile) +end + +--- Test for existance of a file. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function exists(file) + assert(file) + return fs_execute("test -r", file) +end + +--- Test is file/dir is writable. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function is_writable(file) + assert(file) + return fs_execute("test -w", file) +end + +--- Test is pathname is a directory. +-- @param file string: pathname to test +-- @return boolean: true if it is a directory, false otherwise. +function is_dir(file) + assert(file) + return fs_execute("test -d", file) +end + +--- Download a remote file. +-- @param url string: URL to be fetched. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @return boolean: true on success, false on failure. +function download(url, filename) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + if cfg.downloader == "wget" then + if filename then + return fs_execute("wget --quiet --continue --output-document ", filename, url) + else + return fs_execute("wget --quiet --continue ", url) + end + elseif cfg.downloader == "curl" then + filename = filename or fs_base_name(url) + return fs_execute_string("curl "..fs_Q(url).." 2> /dev/null 1> "..fs_Q(filename)) + end +end + +--- Strip the path off a path+filename. +-- @param pathname string: A path+name, such as "/a/b/c". +-- @return string: The filename without its path, such as "c". +function base_name(pathname) + assert(type(pathname) == "string") + + local base = pathname:match(".*/([^/]*)") + return base or pathname +end + +--- Strip the name off a path+filename. +-- @param pathname string: A path+name, such as "/a/b/c". +-- @return string: The filename without its path, such as "/a/b/". +-- For entries such as "/a/b/", "/a/" is returned. If there are +-- no directory separators in input, "" is returned. +function dir_name(pathname) + assert(type(pathname) == "string") + + local dir = pathname:gsub("/*$", ""):match("(.*/)[^/]*") + return dir or "" +end + +--- Create a temporary directory. +-- @param name string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @return string or nil: name of temporary directory or nil on failure. +function make_temp_dir(name) + assert(type(name) == "string") + + local temp_dir = (os.getenv("TMP") or "/tmp") .. "/" .. name .. "-" .. tostring(math.floor(math.random() * 10000)) + if fs_make_dir(temp_dir) then + return temp_dir + else + return nil + end +end + +--- Apply a patch. +-- @param patchname string: The filename of the patch. +function patch(patchname) + return fs_execute("patch -p1 -f -i ", patchname) +end + +--- Unpack an archive. +-- Extract the contents of an archive, detecting its format by +-- filename extension. +-- @param archive string: Filename of archive. +-- @return boolean or (boolean, string): true on success, false and an error message on failure. +function unpack_archive(archive) + assert(type(archive) == "string") + + local ok + if archive:match("%.tar%.gz$") or archive:match("%.tgz$") then + -- ok = fs_execute("tar zxvpf ", archive) + ok = fs_execute_string("gunzip -c "..archive.."|tar -xf -") + elseif archive:match("%.tar%.bz2$") then + -- ok = fs_execute("tar jxvpf ", archive) + ok = fs_execute_string("bunzip2 -c "..archive.."|tar -xf -") + elseif archive:match("%.zip$") then + ok = fs_execute("unzip ", archive) + elseif archive:match("%.lua$") or archive:match("%.c$") then + -- Ignore .lua and .c files; they don't need to be extracted. + return true + else + local ext = archive:match(".*(%..*)") + return false, "Unrecognized filename extension "..(ext or "") + end + if not ok then + return false, "Failed extracting "..archive + end + return true +end + +--- Check the MD5 checksum for a file. +-- @param file string: The file to be checked. +-- @param md5sum string: The string with the expected MD5 checksum. +-- @return boolean: true if the MD5 checksum for 'file' equals 'md5sum', false if not +-- or if it could not perform the check for any reason. +function check_md5(file, md5sum) + file = fs_absolute_name(file) + local computed + if cfg.md5checker == "md5sum" then + local pipe = io.popen("md5sum "..file) + computed = pipe:read("*l"):gsub("[^%x]+", "") + pipe:close() + if computed then + computed = computed:sub(1,32) + end + elseif cfg.md5checker == "openssl" then + local pipe = io.popen("openssl md5 "..file) + computed = pipe:read("*l") + pipe:close() + if computed then + computed = computed:sub(-32) + end + elseif cfg.md5checker == "md5" then + local pipe = io.popen("md5 "..file) + computed = pipe:read("*l") + pipe:close() + if computed then + computed = computed:sub(-32) + end + end + if not computed then + return false + end + if computed:match("^"..md5sum) then + return true + else + return false + end +end + +--- Return an absolute pathname from a potentially relative one. +-- @param pathname string: pathname to convert. +-- @param relative_to string or nil: path to prepend when making +-- pathname absolute, or the current dir in the dir stack if +-- not given. +-- @return string: The pathname converted to absolute. +function absolute_name(pathname, relative_to) + assert(type(pathname) == "string") + assert(type(relative_to) == "string" or not relative_to) + + relative_to = relative_to or fs_current_dir() + if pathname:sub(1,1) == "/" then + return pathname + else + return relative_to .. "/" .. pathname + end +end + +--- Describe a path in a cross-platform way. +-- Use this function to avoid platform-specific directory +-- separators in other modules. If the first item contains a +-- protocol descriptor (e.g. "http:"), paths are always constituted +-- with forward slashes. +-- @param ... strings representing directories +-- @return string: a string with a platform-specific representation +-- of the path. +function make_path(...) + local items = {...} + local i = 1 + while items[i] do + if items[i] == "" then + table.remove(items, i) + else + i = i + 1 + end + end + return table.concat(items, "/") +end + +--- Split protocol and path from an URL or local pathname. +-- URLs should be in the "protocol://path" format. +-- For local pathnames, "file" is returned as the protocol. +-- @param url string: an URL or a local pathname. +-- @return string, string: the protocol, and the absolute pathname without the protocol. +function split_url(url) + assert(type(url) == "string") + + local protocol, pathname = url:match("^([^:]*)://(.*)") + if not protocol then + protocol = "file" + pathname = url + end + if protocol == "file" then + pathname = fs_absolute_name(pathname) + end + return protocol, pathname +end + +--- Create a wrapper to make a script executable from the command-line. +-- @param file string: Pathname of script to be made executable. +-- @param dest string: Directory where to put the wrapper. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function wrap_script(file, dest) + assert(type(file) == "string") + assert(type(dest) == "string") + + local base = fs_base_name(file) + local wrapname = dest.."/"..base + local wrapper = io.open(wrapname, "w") + if not wrapper then + return nil, "Could not open "..wrapname.." for writing." + end + wrapper:write("#!/bin/sh\n\n") + wrapper:write('LUA_PATH="'..package.path..';$LUA_PATH"\n') + wrapper:write('LUA_CPATH="'..package.cpath..';$LUA_CPATH"\n') + wrapper:write('export LUA_PATH LUA_CPATH\n') + wrapper:write('exec "'..fs_make_path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)..'" -lluarocks.require "'..file..'" "$@"\n') + wrapper:close() + if fs_execute("chmod +x",wrapname) then + return true + else + return nil, "Could not make "..wrapname.." executable." + end +end + +--- Check if a file (typically inside path.bin_dir) is an actual binary +-- or a Lua wrapper. +-- @param filename string: the file name with full path. +-- @return boolean: returns true if file is an actual binary +-- (or if it couldn't check) or false if it is a Lua wrapper. +local function is_actual_binary(filename) + local file = io.open(filename) + if file then + local found = false + if file:read():match("#!/bin/sh") then + local line = file:read() + line = file:read() + if not(line and line:match("LUA_PATH")) then + found = true + end + end + file:close() + if found then + return false + else + return true + end + else + return true + end + return false +end + +function copy_binary(filename, dest) + return fs_copy(filename, dest) +end + diff --git a/src/luarocks/fs/win32.lua b/src/luarocks/fs/win32.lua new file mode 100644 index 00000000..5702a3aa --- /dev/null +++ b/src/luarocks/fs/win32.lua @@ -0,0 +1,369 @@ +--- Windows implementation of filesystem and platform abstractions. +-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities +-- used by this module. +module("luarocks.fs.win32", package.seeall) + +local cfg = require("luarocks.cfg") + +local fs_base_name, + fs_copy, + fs_current_dir, + fs_execute, + fs_execute_string, + fs_is_dir, + fs_make_path, + fs_Q + +function init_fs_functions(impl) + fs_base_name = impl.base_name + fs_copy = impl.copy + fs_current_dir = impl.current_dir + fs_execute = impl.execute + fs_execute_string = impl.execute_string + fs_is_dir = impl.is_dir + fs_make_path = impl.make_path + fs_Q = impl.Q +end + +--- Quote argument for shell processing. Fixes paths on Windows. +-- Adds single quotes and escapes. +-- @param arg string: Unquoted argument. +-- @return string: Quoted argument. +function Q(arg) + assert(type(arg) == "string") + -- Quote DIR for Windows + if arg:match("^[\.a-zA-Z]?:?[\\/]") then + return '"' .. arg:gsub("/", "\\"):gsub('"', '\\"') .. '"' + end + -- URLs and anything else + return '"' .. arg:gsub('"', '\\"') .. '"' +end + +local function command_at(dir, cmd) + local drive = dir:match("^([A-Za-z]:)") + cmd = "cd " .. fs_Q(dir) .. " & " .. cmd + if drive then + cmd = drive .. " & " .. cmd + end + return cmd +end + +--- Run the given command. +-- The command is executed in the current directory in the dir stack. +-- @param cmd string: No quoting/escaping is applied to the command. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function execute_string(cmd) + if os.execute(command_at(fs_current_dir(), cmd)) == 0 then + return true + else + return false + end +end + +--- Test for existance of a file. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function exists(file) + assert(file) + return fs_execute("if not exist " .. fs_Q(file) .. + " invalidcommandname 2>NUL 1>NUL") +end + +--- Test is pathname is a directory. +-- @param file string: pathname to test +-- @return boolean: true if it is a directory, false otherwise. +function is_dir(file) + assert(file) + return fs_execute("chdir /D " .. fs_Q(file) .. " 2>NUL 1>NUL") +end + +--- Test is file/dir is writable. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function is_writable(file) + assert(file) + local result + if is_dir(file) then + local file2 = file .. '/.tmpluarockstestwritable' + local fh = io.open(file2, 'w') + result = fh ~= nil + if fh then fh:close() end + os.remove(file2) + else + local fh = io.open(file, 'r+') + result = fh ~= nil + if fh then fh:close() end + end + return result +end + + +--- Create a directory if it does not already exist. +-- If any of the higher levels in the path name does not exist +-- too, they are created as well. +-- @param dir string: pathname of directory to create. +-- @return boolean: true on success, false on failure. +function make_dir(dir) + assert(dir) + fs_execute("mkdir "..fs_Q(dir).." 1> NUL 2> NUL") + return 1 +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param dir string: pathname of directory to remove. +function remove_dir_if_empty(dir) + assert(dir) + fs_execute_string("rmdir "..fs_Q(dir).." 1> NUL 2> NUL") +end + +--- Copy a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function copy(src, dest) + assert(src and dest) + if dest:match("[/\\]$") then dest = dest:sub(1, -2) end + if fs_execute("cp", src, dest) then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Recursively copy the contents of a directory. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function copy_contents(src, dest) + assert(src and dest) + if fs_execute_string("cp -a "..src.."\\*.* "..fs_Q(dest).." 1> NUL 2> NUL") then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Delete a file or a directory and all its contents. +-- For safety, this only accepts absolute paths. +-- @param arg string: Pathname of source +-- @return boolean: true on success, false on failure. +function delete(arg) + assert(arg) + assert(arg:match("^[\a-zA-Z]?:?[\\/]")) + fs_execute("chmod a+rw -R ", arg) + return fs_execute_string("rm -rf " .. fs_Q(arg) .. " 1> NUL 2> NUL") +end + +--- List the contents of a directory. +-- @param at string or nil: directory to list (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. +function dir(at) + assert(type(at) == "string" or not at) + if not at then + at = fs_current_dir() + end + if not fs_is_dir(at) then + return {} + end + local result = {} + local pipe = io.popen(command_at(at, "ls")) + for file in pipe:lines() do + table.insert(result, file) + end + pipe:close() + + return result +end + +--- Recursively scan the contents of a directory. +-- @param at string or nil: directory to scan (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. Paths are returned with forward slashes. +function find(at) + assert(type(at) == "string" or not at) + if not at then + at = fs_current_dir() + end + if not fs_is_dir(at) then + return {} + end + local result = {} + local pipe = io.popen(command_at(at, "find 2> NUL")) + for file in pipe:lines() do + -- Windows find is a bit different + if file:sub(1,2)==".\\" then file=file:sub(3) end + if file ~= "." then + table.insert(result, (file:gsub("\\", "/"))) + end + end + return result +end + + +--- Download a remote file. +-- @param url string: URL to be fetched. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @return boolean: true on success, false on failure. +function download(url, filename) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + if filename then + return fs_execute("wget --quiet --continue --output-document ", filename, url) + else + return fs_execute("wget --quiet --continue ", url) + end +end + +--- Strip the path off a path+filename. +-- @param pathname string: A path+name, such as "/a/b/c". +-- @return string: The filename without its path, such as "c". +function base_name(pathname) + assert(type(pathname) == "string") + + local base = pathname:match(".*[/\\]([^/\\]*)") + return base or pathname +end + +--- Strip the last extension of a filename. +-- Example: "foo.tar.gz" becomes "foo.tar". +-- If filename has no dots, returns it unchanged. +-- @param filename string: The file name to strip. +-- @return string: The stripped name. +local function strip_extension(filename) + assert(type(filename) == "string") + + return (filename:gsub("%.[^.]+$", "")) or filename +end + +--- Uncompress gzip file. +-- @param archive string: Filename of archive. +-- @return boolean : success status +local function gunzip(archive) + local cmd = fs_execute("gunzip -h 1>NUL 2>NUL") and 'gunzip' or + fs_execute("gzip -h 1>NUL 2>NUL") and 'gzip -d' + local ok = fs_execute(cmd, archive) + return ok +end + +--- Unpack an archive. +-- Extract the contents of an archive, detecting its format by +-- filename extension. +-- @param archive string: Filename of archive. +-- @return boolean or (boolean, string): true on success, false and an error message on failure. +function unpack_archive(archive) + assert(type(archive) == "string") + + local ok + if archive:match("%.tar%.gz$") then + ok = gunzip(archive) + if ok then + ok = fs_execute("tar -xf ", strip_extension(archive)) + end + elseif archive:match("%.tgz$") then + ok = gunzip(archive) + if ok then + ok = fs_execute("tar -xf ", strip_extension(archive)..".tar") + end + elseif archive:match("%.tar%.bz2$") then + ok = fs_execute("bunzip2 ", archive) + if ok then + ok = fs_execute("tar -xf ", strip_extension(archive)) + end + elseif archive:match("%.zip$") then + ok = fs_execute("unzip ", archive) + elseif archive:match("%.lua$") or archive:match("%.c$") then + -- Ignore .lua and .c files; they don't need to be extracted. + return true + else + local ext = archive:match(".*(%..*)") + return false, "Unrecognized filename extension "..(ext or "") + end + if not ok then + return false, "Failed extracting "..archive + end + return true +end + +--- Return an absolute pathname from a potentially relative one. +-- @param pathname string: pathname to convert. +-- @param relative_to string or nil: path to prepend when making +-- pathname absolute, or the current dir in the dir stack if +-- not given. +-- @return string: The pathname converted to absolute. +function absolute_name(pathname, relative_to) + assert(type(pathname) == "string") + assert(type(relative_to) == "string" or not relative_to) + + relative_to = relative_to or fs_current_dir() + if pathname:match("^[\.a-zA-Z]?:?[\\/]") then + return pathname + else + return relative_to .. "/" .. pathname + end +end + +--- Create a wrapper to make a script executable from the command-line. +-- @param file string: Pathname of script to be made executable. +-- @param dest string: Directory where to put the wrapper. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function wrap_script(file, dest) + assert(type(file) == "string") + assert(type(dest) == "string") + + local base = fs_base_name(file) + local wrapname = dest.."/"..base..".bat" + local wrapper = io.open(wrapname, "w") + if not wrapper then + return nil, "Could not open "..wrapname.." for writing." + end + wrapper:write("@echo off\n") + wrapper:write("setlocal\n") + wrapper:write('set LUA_PATH='..package.path..";%LUA_PATH%\n") + wrapper:write('set LUA_CPATH='..package.cpath..";%LUA_CPATH%\n") + wrapper:write('"'..fs_make_path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)..'" -lluarocks.require "'..file..'" %*\n') + wrapper:write("endlocal\n") + wrapper:close() + return true +end + +function is_actual_binary(name) + name = name:lower() + if name:match("%.bat$") or name:match("%.exe$") then + return true + end + return false +end + +function copy_binary(filename, dest) + local ok, err = fs_copy(filename, dest) + if not ok then + return nil, err + end + local exe_pattern = "%.[Ee][Xx][Ee]$" + local base = fs_base_name(filename) + if base:match(exe_pattern) then + base = base:gsub(exe_pattern, ".lua") + local helpname = dest.."/"..base + local helper = io.open(helpname, "w") + if not helper then + return nil, "Could not open "..helpname.." for writing." + end + helper:write('package.path=\"'..package.path:gsub("\\","\\\\")..';\"..package.path\n') + helper:write('package.cpath=\"'..package.path:gsub("\\","\\\\")..';\"..package.cpath\n') + helper:close() + end + return true +end diff --git a/src/luarocks/help.lua b/src/luarocks/help.lua new file mode 100644 index 00000000..76e77a33 --- /dev/null +++ b/src/luarocks/help.lua @@ -0,0 +1,69 @@ + +--- Module implementing the LuaRocks "help" command. +-- This is a generic help display module, which +-- uses a global table called "commands" to find commands +-- to show help for; each command should be represented by a +-- table containing "help" and "help_summary" fields. +module("luarocks.help", package.seeall) + +local util = require("luarocks.util") + +help_summary = "Help on commands." + +help_arguments = "[]" +help = [[ + is the command to show help for. +]] + +--- Driver function for the "help" command. +-- @param command string or nil: command to show help for; if not +-- given, help summaries for all commands are shown. +-- @return boolean or (nil, string): true if there were no errors +-- or nil and an error message if an invalid command was requested. +function run(...) + local flags, command = util.parse_flags(...) + + if not command then + print([[ +LuaRocks ]]..program_version..[[, a module deployment system for Lua + +]]..program_name..[[ - ]]..program_description..[[ + +usage: ]]..program_name..[[ [--from= | --only-from=] [--to=] [VAR=VALUE]... [] + +Variables from the "variables" table of the configuration file +can be overriden with VAR=VALUE assignments. + +--from= Fetch rocks/rockspecs from this server + (takes priority over config file) +--only-from= Fetch rocks/rockspecs from this server only + (overrides any entries in the config file) +--to= Which tree to operate on. + +Supported commands: +]]) + local names = {} + for name, command in pairs(commands) do + table.insert(names, name) + end + table.sort(names) + for _, name in ipairs(names) do + local command = commands[name] + print(name, command.help_summary) + end + else + command = command:gsub("-", "_") + if commands[command] then + local arguments = commands[command].help_arguments or "" + print() + print(program_name.." "..command.." "..arguments) + print() + print(command.." - "..commands[command].help_summary) + print() + print(commands[command].help) + else + return nil, "Unknown command '"..command.."'" + end + end + return true +end diff --git a/src/luarocks/install.lua b/src/luarocks/install.lua new file mode 100644 index 00000000..b115a8a8 --- /dev/null +++ b/src/luarocks/install.lua @@ -0,0 +1,115 @@ + +--- Module implementing the LuaRocks "install" command. +-- Installs binary rocks. +module("luarocks.install", package.seeall) + +local path = require("luarocks.path") +local rep = require("luarocks.rep") +local fetch = require("luarocks.fetch") +local util = require("luarocks.util") +local fs = require("luarocks.fs") +local deps = require("luarocks.deps") +local manif = require("luarocks.manif") +local cfg = require("luarocks.cfg") + +help_summary = "Install a rock." + +help_arguments = "{| []}" + +help = [[ +Argument may be the name of a rock to be fetched from a repository +or a filename of a locally available rock. +]] + +--- Install a binary rock. +-- @param rock_file string: local or remote filename of a rock. +-- @return boolean or (nil, string): True if succeeded or +-- nil and an error message. +function install_binary_rock(rock_file) + local name, version, arch = path.parse_rock_name(rock_file) + if not name then + return nil, "Filename "..rock_file.." does not match format 'name-version-revision.arch.rock'." + end + if arch ~= "all" and arch ~= cfg.arch then + return nil, "Incompatible architecture "..arch + end + if rep.is_installed(name, version) then + rep.delete_version(name, version) + end + local rollback = util.schedule_function(function() + fs.delete(path.install_dir(name, version)) + fs.remove_dir_if_empty(path.versions_dir(name)) + end) + local ok, err = fetch.fetch_and_unpack_rock(rock_file, path.install_dir(name, version)) + if not ok then return nil, err end + ok, err = rep.install_bins(name, version) + + local rockspec, err = fetch.load_rockspec(path.rockspec_file(name, version)) + if err then + return nil, "Failed loading rockspec for installed package: "..err + end + + ok, err = deps.check_external_deps(rockspec, "install") + if err then + return nil, err + end + + ok, err = deps.fulfill_dependencies(rockspec) + if err then + return nil, err + end + + ok, err = rep.run_hook(rockspec, "post_install") + if err then + return nil, err + end + + ok, err = manif.update_manifest(name, version) + if err then + return nil, err + end + util.remove_scheduled_function(rollback) + return true +end + +--- Driver function for the "install" command. +-- @param name string: name of a binary rock. If an URL or pathname +-- to a binary rock is given, fetches and installs it. If a rockspec or a +-- source rock is given, forwards the request to the "build" command. +-- If a package name is given, forwards the request to "search" and, +-- if returned a result, installs the matching rock. +-- @param version string: When passing a package name, a version number +-- may also be given. +-- @return boolean or (nil, string): True if installation was +-- successful, nil and an error message otherwise. +function run(...) + local flags, name, version = util.parse_flags(...) + if type(name) ~= "string" then + return nil, "Argument missing, see help." + end + + if name:match("%.rockspec$") or name:match("%.src%.rock$") then + local build = require("luarocks.build") + return build.run(name) + elseif name:match("%.rock$") then + return install_binary_rock(name) + else + local search = require("luarocks.search") + local results, err = search.find_suitable_rock(search.make_query(name, version)) + if err then + return nil, err + elseif type(results) == "string" then + local url = results + print("Installing "..url.."...") + return run(url) + else + print() + print("Could not determine which rock to install.") + print() + print("Search results:") + print("---------------") + search.print_results(results) + return nil, (next(results) and "Please narrow your query." or "No results found.") + end + end +end diff --git a/src/luarocks/list.lua b/src/luarocks/list.lua new file mode 100644 index 00000000..1251653c --- /dev/null +++ b/src/luarocks/list.lua @@ -0,0 +1,35 @@ + +--- Module implementing the LuaRocks "list" command. +-- Lists currently installed rocks. +module("luarocks.list", package.seeall) + +local search = require("luarocks.search") +local cfg = require("luarocks.cfg") +local util = require("luarocks.util") +local fs = require("luarocks.fs") + +help_summary = "Lists currently installed rocks." + +help = [[ + is a substring of a rock name to filter by. +]] + +--- Driver function for "list" command. +-- @param filter string or nil: A substring of a rock name to filter by. +-- @param version string or nil: a version may also be passed. +-- @return boolean: True if succeeded, nil on errors. +function run(...) + local flags, filter, version = util.parse_flags(...) + local results = {} + local query = search.make_query(filter or "", version) + query.exact_name = false + for _, tree in ipairs(cfg.rocks_trees) do + search.manifest_search(results, fs.make_path(tree, "rocks"), query) + end + print() + print("Installed rocks:") + print("----------------") + print() + search.print_results(results, false) + return true +end diff --git a/src/luarocks/make.lua b/src/luarocks/make.lua new file mode 100644 index 00000000..1c116cdb --- /dev/null +++ b/src/luarocks/make.lua @@ -0,0 +1,54 @@ + +--- Module implementing the LuaRocks "make" command. +-- Builds sources in the current directory, but unlike "build", +-- it does not fetch sources, etc., assuming everything is +-- available in the current directory. +module("luarocks.make", package.seeall) + +local build = require("luarocks.build") +local fs = require("luarocks.fs") +local util = require("luarocks.util") + +help_summary = "Compile package in current directory using a rockspec." +help_arguments = "[]" +help = [[ +Builds sources in the current directory, but unlike "build", +it does not fetch sources, etc., assuming everything is +available in the current directory. If no argument is given, +look for a rockspec in the current directory. If more than one +is found, you must specify which to use, through the command-line. + +This command is useful as a tool for debugging rockspecs. +To install rocks, you'll normally want to use the "install" and +"build" commands. See the help on those for details. +]] + +--- Driver function for "make" command. +-- @param name string: A local rockspec. +-- @return boolean or (nil, string): True if build was successful; nil and an +-- error message otherwise. +function run(...) + local flags, rockspec = util.parse_flags(...) + assert(type(rockspec) == "string" or not rockspec) + + if not rockspec then + local files = fs.dir(fs.current_dir()) + for _, file in pairs(files) do + if file:match(".rockspec$") then + if rockspec then + return nil, "Please specify which rockspec file to use." + else + rockspec = file + end + end + end + if not rockspec then + return nil, "Argument missing: please specify a rockspec to use on current directory." + end + end + if not rockspec:match("%.rockspec$") then + return nil, "Invalid argument: 'make' takes a rockspec as a parameter. See help." + end + + return build.build_rockspec(rockspec, false, true) +end diff --git a/src/luarocks/make_manifest.lua b/src/luarocks/make_manifest.lua new file mode 100644 index 00000000..6edbd14b --- /dev/null +++ b/src/luarocks/make_manifest.lua @@ -0,0 +1,32 @@ + +--- Module implementing the luarocks-admin "make_manifest" command. +-- Compile a manifest file for a repository. +module("luarocks.make_manifest", package.seeall) + +local manif = require("luarocks.manif") +local cfg = require("luarocks.cfg") + +help_summary = "Compile a manifest file for a repository." + +help = [[ +, if given, is a local repository pathname. +]] + +--- Driver function for "make_manifest" command. +-- @param repo string or nil: Pathname of a local repository. If not given, +-- the default local repository configured as cfg.rocks_dir is used. +-- @return boolean or (nil, string): True if manifest was generated, +-- or nil and an error message. +function run(repo) + assert(type(repo) == "string" or not repo) + repo = repo or cfg.rocks_dir + + print("Making manifest for "..repo) + + ok = manif.make_manifest(repo) + if ok then + print("Generating index.html for "..repo) + manif.make_index(repo) + end + return ok +end diff --git a/src/luarocks/manif.lua b/src/luarocks/manif.lua new file mode 100644 index 00000000..98772319 --- /dev/null +++ b/src/luarocks/manif.lua @@ -0,0 +1,439 @@ + +--- Functions for querying and manipulating manifest files. +module("luarocks.manif", package.seeall) + +local util = require("luarocks.util") +local fs = require("luarocks.fs") +local search = require("luarocks.search") +local rep = require("luarocks.rep") +local deps = require("luarocks.deps") +local cfg = require("luarocks.cfg") +local persist = require("luarocks.persist") +local fetch = require("luarocks.fetch") +local type_check = require("luarocks.type_check") + +manifest_cache = {} + +--- Get all versions of a package listed in a manifest file. +-- @param name string: a package name. +-- @param manifest table or nil: a manifest table; if not given, the +-- default local manifest table is used. +-- @return table: An array of strings listing installed +-- versions of a package. +function get_versions(name, manifest) + assert(type(name) == "string") + assert(type(manifest) == "table" or not manifest) + + if not manifest then + manifest = load_local_manifest(cfg.rocks_dir) + if not manifest then + return {} + end + end + + local item = manifest.repository[name] + if item then + return util.keys(item) + end + return {} +end + +--- Back-end function that actually loads the manifest +-- and stores it in the manifest cache. +-- @param file string: The local filename of the manifest file. +-- @param repo_url string: The repository identifier. +local function manifest_loader(file, repo_url, quick) + local manifest = persist.load_into_table(file) + if not manifest then + return nil, "Failed loading manifest for "..repo_url + end + if not quick then + local ok, err = type_check.type_check_manifest(manifest) + if not ok then + return nil, "Error checking manifest: "..err + end + end + + manifest_cache[repo_url] = manifest + return manifest +end + +--- Load a local or remote manifest describing a repository. +-- All functions that use manifest tables assume they were obtained +-- through either this function or load_local_manifest. +-- @param repo_url string: URL or pathname for the repository. +-- @return table or (nil, string): A table representing the manifest, +-- or nil followed by an error message. +function load_manifest(repo_url) + assert(type(repo_url) == "string") + + if manifest_cache[repo_url] then + return manifest_cache[repo_url] + end + + local protocol, pathname = fs.split_url(repo_url) + if protocol == "file" then + pathname = fs.make_path(pathname, "manifest") + else + local url = fs.make_path(repo_url, "manifest") + local name = repo_url:gsub("[/:]","_") + local file, dir = fetch.fetch_url_at_temp_dir(url, "luarocks-manifest-"..name) + if not file then + return nil, "Failed fetching manifest for "..repo_url + end + pathname = file + end + return manifest_loader(pathname, repo_url) +end + +--- Load a local manifest describing a repository. +-- All functions that use manifest tables assume they were obtained +-- through either this function or load_manifest. +-- @param repo_url string: URL or pathname for the repository. +-- @return table or (nil, string): A table representing the manifest, +-- or nil followed by an error message. +function load_local_manifest(repo_url) + assert(type(repo_url) == "string") + + if manifest_cache[repo_url] then + return manifest_cache[repo_url] + end + + local pathname = fs.make_path(repo_url, "manifest") + + return manifest_loader(pathname, repo_url, true) +end + +--- Sort function for ordering rock identifiers in a manifest's +-- modules table. Rocks are ordered alphabetically by name, and then +-- by version which greater first. +-- @param a string: Version to compare. +-- @param b string: Version to compare. +-- @return boolean: The comparison result, according to the +-- rule outlined above. +local function sort_pkgs(a, b) + assert(type(a) == "string") + assert(type(b) == "string") + + local na, va = a:match("(.*)/(.*)$") + local nb, vb = b:match("(.*)/(.*)$") + + return (na == nb) and deps.compare_versions(va, vb) or na < nb +end + +--- Output a table listing items of a package. +-- @param itemsfn function: a function for obtaining items of a package. +-- pkg and version will be passed to it; it should return a table with +-- items as keys. +-- @param pkg string: package name +-- @param version string: package version +-- @param tbl table: the package matching table: keys should be item names +-- and values arrays of strings with packages names in "name/version" format. +local function store_package_items(itemsfn, pkg, version, tbl) + assert(type(itemsfn) == "function") + assert(type(pkg) == "string") + assert(type(version) == "string") + assert(type(tbl) == "table") + + local path = pkg.."/"..version + local result = {} + for item, _ in pairs(itemsfn(pkg, version)) do + table.insert(result, item) + if not tbl[item] then + tbl[item] = {} + end + table.insert(tbl[item], path) + end + return result +end + +--- Sort items of a package matching table by version number (higher versions first). +-- @param tbl table: the package matching table: keys should be strings +-- and values arrays of strings with packages names in "name/version" format. +local function sort_package_matching_table(tbl) + assert(type(tbl) == "table") + + if next(tbl) then + for item, pkgs in pairs(tbl) do + if #pkgs > 1 then + table.sort(pkgs, sort_pkgs) + -- Remove duplicates from the sorted array. + local prev = nil + local i = 1 + while pkgs[i] do + local curr = pkgs[i] + if curr == prev then + table.remove(pkgs, i) + else + prev = curr + i = i + 1 + end + end + end + end + end +end + +--- Commit manifest to disk in given local repository. +-- @param repo string: The directory of the local repository. +-- @param manifest table: The manifest table +-- @return boolean or (nil, string): true if successful, or nil and a +-- message in case of errors. +local function save_manifest(repo, manifest) + assert(type(repo) == "string") + assert(type(manifest) == "table") + + local filename = fs.make_path(repo, "manifest") + return persist.save_from_table(filename, manifest) +end + +--- Process the dependencies of a package to determine its dependency +-- chain for loading modules. +-- @param name string: Package name. +-- @param version string: Package version. +-- @return (table, table): A table listing dependencies as string-string pairs +-- of names and versions, and a similar table of missing dependencies. +local function update_dependencies(manifest) + for pkg, versions in pairs(manifest.repository) do + for version, repos in pairs(versions) do + local current = pkg.." "..version + for _, repo in ipairs(repos) do + if repo.arch == "installed" then + local missing + repo.dependencies, missing = deps.scan_deps({}, {}, manifest, pkg, version) + repo.dependencies[pkg] = nil + if missing then + for miss, _ in pairs(missing) do + if miss == current then + print("Tree inconsistency detected: "..current.." has no rockspec.") + else + print("Missing dependency for "..pkg.." "..version..": "..miss) + end + end + end + end + end + end + end +end + +--- Store search results in a manifest table. +-- @param results table: The search results as returned by search.disk_search. +-- @param manifest table: A manifest table (must contain repository, modules, commands tables). +local function store_results(results, manifest) + assert(type(results) == "table") + assert(type(manifest) == "table") + + for pkg, versions in pairs(results) do + local pkgtable = manifest.repository[pkg] or {} + for version, repos in pairs(versions) do + local versiontable = {} + for _, repo in ipairs(repos) do + local repotable = {} + repotable.arch = repo.arch + if repo.arch == "installed" then + repotable.modules = store_package_items(rep.package_modules, pkg, version, manifest.modules) + repotable.commands = store_package_items(rep.package_commands, pkg, version, manifest.commands) + end + table.insert(versiontable, repotable) + end + pkgtable[version] = versiontable + end + manifest.repository[pkg] = pkgtable + end + update_dependencies(manifest) + sort_package_matching_table(manifest.modules) + sort_package_matching_table(manifest.commands) +end + +--- Load a manifest file from a local repository and add to the repository +-- information with regard to the given name and version. +-- A file called 'manifest' will be written in the root of the given +-- repository directory. +-- @param name string: Name of a package from the repository. +-- @param version string: Version of a package from the repository. +-- @param repo string or nil: Pathname of a local repository. If not given, +-- the default local repository configured as cfg.rocks_dir is used. +-- @return boolean or (nil, string): True if manifest was generated, +-- or nil and an error message. +function update_manifest(name, version, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(type(repo) == "string" or not repo) + repo = repo or cfg.rocks_dir + + print("Updating manifest for "..repo) + + local manifest, err = load_manifest(repo) + if not manifest then + print("No existing manifest. Attempting to rebuild...") + local ok, err = make_manifest(repo) + if not ok then + return nil, err + end + manifest, err = load_manifest(repo) + if not manifest then + return nil, err + end + end + + local results = {[name] = {[version] = {{arch = "installed", repo = repo}}}} + + store_results(results, manifest) + return save_manifest(repo, manifest) +end + +--- Scan a LuaRocks repository and output a manifest file. +-- A file called 'manifest' will be written in the root of the given +-- repository directory. +-- @param repo A local repository directory. +-- @return boolean or (nil, string): True if manifest was generated, +-- or nil and an error message. +function make_manifest(repo) + assert(type(repo) == "string") + + if not fs.is_dir(repo) then + return nil, "Cannot access repository at "..repo + end + + local query = search.make_query("") + query.exact_name = false + query.arch = "any" + local results = search.disk_search(repo, query) + + local manifest = { repository = {}, modules = {}, commands = {} } + manifest_cache[repo] = manifest + store_results(results, manifest) + return save_manifest(repo, manifest) +end + +local index_header = [[ + + + +Available rocks + + + + +

Available rocks

+

+Lua modules avaliable from this location for use with LuaRocks: +

+ +]] + +local index_package_start = [[ + + + +]] + +local index_footer = [[ +
+

$package - $summary
+

$detailed
+latest sources | project homepage | License: $license

+
+]] + +local index_package_end = [[ +
+

+manifest file +

+ + +]] + +function make_index(repo) + if not fs.is_dir(repo) then + return nil, "Cannot access repository at "..repo + end + local manifest = load_manifest(repo) + files = fs.find(repo) + local out = io.open(fs.make_path(repo, "index.html"), "w") + out:write(index_header) + for package, version_list in util.sortedpairs(manifest.repository) do + local latest_rockspec = nil + local output = index_package_start + for version, data in util.sortedpairs(version_list, deps.compare_versions) do + local out_versions = {} + local arches = 0 + output = output..version + local sep = ': ' + for _, item in ipairs(data) do + output = output .. sep .. ''..item.arch..'' + sep = ', ' + if item.arch == 'rockspec' then + local rs = ("%s-%s.rockspec"):format(package, version) + if not latest_rockspec then latest_rockspec = rs end + output = output:gsub("$url", rs) + else + output = output:gsub("$url", ("%s-%s.%s.rock"):format(package, version, item.arch)) + end + end + output = output .. '
' + output = output:gsub("$na", arches) + end + output = output .. index_package_end + if latest_rockspec then + local rockspec = persist.load_into_table(fs.make_path(repo, latest_rockspec)) + local vars = { + anchor = package, + package = rockspec.package, + original = rockspec.source.url, + summary = rockspec.description.summary or "", + detailed = rockspec.description.detailed or "", + license = rockspec.description.license or "N/A", + homepage = rockspec.description.homepage or "" + } + vars.detailed = vars.detailed:gsub("\n\n", "

"):gsub("%s+", " ") + output = output:gsub("$(%w+)", vars) + else + output = output:gsub("$anchor", package) + output = output:gsub("$package", package) + output = output:gsub("$(%w+)", "") + end + out:write(output) + end + out:write(index_footer) + out:close() +end + diff --git a/src/luarocks/pack.lua b/src/luarocks/pack.lua new file mode 100644 index 00000000..3f64b1c0 --- /dev/null +++ b/src/luarocks/pack.lua @@ -0,0 +1,116 @@ + +--- Module implementing the LuaRocks "pack" command. +-- Creates a rock, packing sources or binaries. +module("luarocks.pack", package.seeall) + +local path = require("luarocks.path") +local rep = require("luarocks.rep") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local cfg = require("luarocks.cfg") +local util = require("luarocks.util") + +help_summary = "Create a rock, packing sources or binaries." +help_arguments = "{| []}" +help = [[ +Argument may be a rockspec file, for creating a source rock, +or the name of an installed package, for creating a binary rock. +In the latter case, the app version may be given as a second +argument. +]] + +--- Create a source rock. +-- Packages a rockspec and its required source files in a rock +-- file with the .src.rock extension, which can later be built and +-- installed with the "build" command. +-- @param rockspec_file string: An URL or pathname for a rockspec file. +-- @return string or (nil, string): The filename of the resulting +-- .src.rock file; or nil and an error message. +local function pack_source_rock(rockspec_file) + assert(type(rockspec_file) == "string") + + rockspec_file = fs.absolute_name(rockspec_file) + local rockspec, err = fetch.load_rockspec(rockspec_file) + if err then + return nil, "Error loading rockspec: "..err + end + + local name_version = rockspec.name .. "-" .. rockspec.version + local rock_file = fs.absolute_name(name_version .. ".src.rock") + + local source_file, dir = fetch.fetch_sources(rockspec, false) + if not source_file then + return nil, dir + end + fs.change_dir(dir) + + fs.delete(rock_file) + fs.copy(rockspec_file, dir) + if not fs.zip(rock_file, fs.base_name(rockspec_file), fs.base_name(source_file)) then + return nil, "Failed packing "..rock_file + end + fs.pop_dir() + + return rock_file +end + +-- @param name string: Name of package to pack. +-- @param version string or nil: A version number may also be passed. +-- @return string or (nil, string): The filename of the resulting +-- .src.rock file; or nil and an error message. +local function pack_binary_rock(name, version) + assert(type(name) == "string") + assert(type(version) == "string" or not version) + + local versions = rep.get_versions(name) + + if not versions then + return nil, "'"..name.."' does not seem to be an installed rock." + end + if not version then + if #versions > 1 then + return nil, "Please specify which version of '"..name.."' to pack." + end + version = versions[1] + end + if not version:match("[^-]+%-%d+") then + return nil, "Expected version "..version.." in version-revision format." + end + local prefix = path.install_dir(name, version) + if not fs.exists(prefix) then + return nil, "'"..name.." "..version.."' does not seem to be an installed rock." + end + local name_version = name .. "-" .. version + local rock_file = fs.absolute_name(name_version .. "."..cfg.arch..".rock") + fs.change_dir(prefix) + if not rep.is_binary_rock(name, version) then + rock_file = rock_file:gsub("%."..cfg.arch:gsub("%-","%%-").."%.", ".all.") + end + fs.delete(rock_file) + if not fs.zip(rock_file, unpack(fs.dir())) then + return nil, "Failed packing "..rock_file + end + fs.pop_dir() + return rock_file +end + +--- Driver function for the "pack" command. +-- @param arg string: may be a rockspec file, for creating a source rock, +-- or the name of an installed package, for creating a binary rock. +-- @param version string or nil: if the name of a package is given, a +-- version may also be passed. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +function run(...) + local flags, arg, version = util.parse_flags(...) + assert(type(version) == "string" or not version) + if type(arg) ~= "string" then + return nil, "Argument missing, see help." + end + + if arg:match(".*%.rockspec") then + return pack_source_rock(arg) + else + return pack_binary_rock(arg, version) + end +end diff --git a/src/luarocks/path.lua b/src/luarocks/path.lua new file mode 100644 index 00000000..e236db01 --- /dev/null +++ b/src/luarocks/path.lua @@ -0,0 +1,224 @@ + +--- Path and filename handling functions. +-- All paths are configured in this module, making it a single +-- point where the layout of the local installation is defined in LuaRocks. +module("luarocks.path", package.seeall) + +local fs = require("luarocks.fs") +local cfg = require("luarocks.cfg") + +--- Infer rockspec filename from a rock filename. +-- @param rock_name string: Pathname of a rock file. +-- @return string: Filename of the rockspec, without path. +function rockspec_name_from_rock(rock_name) + assert(type(rock_name) == "string") + local base_name = fs.base_name(rock_name) + return base_name:match("(.*)%.[^.]*.rock") .. ".rockspec" +end + +--- Get the repository directory for all versions of a package. +-- @param name string: The package name. +-- @return string: The resulting path -- does not guarantee that +-- @param repo string or nil: If given, specifies the local repository to use. +-- the package (and by extension, the path) exists. +function versions_dir(name, repo) + assert(type(name) == "string") + assert(not repo or type(repo) == "string") + + return fs.make_path(repo or cfg.rocks_dir, name) +end + +--- Get the local installation directory (prefix) for a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param repo string or nil: If given, specifies the local repository to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function install_dir(name, version, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(not repo or type(repo) == "string") + + return fs.make_path(repo or cfg.rocks_dir, name, version) +end + +--- Get the local filename of the rockspec of an installed rock. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param repo string or nil: If given, specifies the local repository to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the file) exists. +function rockspec_file(name, version, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(not repo or type(repo) == "string") + + return fs.make_path(repo or cfg.rocks_dir, name, version, name.."-"..version..".rockspec") +end + +--- Get the local installation directory for C libraries of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param repo string or nil: If given, specifies the local repository to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function lib_dir(name, version, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(not repo or type(repo) == "string") + + return fs.make_path(repo or cfg.rocks_dir, name, version, "lib") +end + +--- Get the local installation directory for Lua modules of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param repo string or nil: If given, specifies the local repository to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function lua_dir(name, version, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(not repo or type(repo) == "string") + + return fs.make_path(repo or cfg.rocks_dir, name, version, "lua") +end + +--- Get the local installation directory for documentation of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param repo string or nil: If given, specifies the local repository to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function doc_dir(name, version, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(not repo or type(repo) == "string") + + return fs.make_path(repo or cfg.rocks_dir, name, version, "doc") +end + +--- Get the local installation directory for configuration files of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param repo string or nil: If given, specifies the local repository to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function conf_dir(name, version, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(not repo or type(repo) == "string") + + return fs.make_path(repo or cfg.rocks_dir, name, version, "conf") +end + +--- Get the local installation directory for command-line scripts +-- of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param repo string or nil: If given, specifies the local repository to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function bin_dir(name, version, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(not repo or type(repo) == "string") + + return fs.make_path(repo or cfg.rocks_dir, name, version, "bin") +end + +--- Extract name, version and arch of a rock filename. +-- @param rock_file string: pathname of a rock +-- @return (string, string, string) or nil: name, version and arch +-- of rock, or nil if name could not be parsed +function parse_rock_name(rock_file) + assert(type(rock_file) == "string") + return fs.base_name(rock_file):match("(.*)-([^-]+-%d+)%.([^.]+)%.rock$") +end + +--- Extract name and version of a rockspec filename. +-- @param rockspec_file string: pathname of a rockspec +-- @return (string, string) or nil: name and version +-- of rockspec, or nil if name could not be parsed +function parse_rockspec_name(rockspec_file) + assert(type(rockspec_file) == "string") + return fs.base_name(rockspec_file):match("(.*)-([^-]+-%d+)%.(rockspec)") +end + +--- Make a rockspec or rock URL. +-- @param pathname string: Base URL or pathname. +-- @param name string: Package name. +-- @param version string: Package version. +-- @param arch string: Architecture identifier, or "rockspec" or "installed". +-- @return string: A URL or pathname following LuaRocks naming conventions. +function make_url(pathname, name, version, arch) + assert(type(pathname) == "string") + assert(type(name) == "string") + assert(type(version) == "string") + assert(type(arch) == "string") + + local filename = name.."-"..version + if arch == "installed" then + filename = fs.make_path(name, version, filename..".rockspec") + elseif arch == "rockspec" then + filename = filename..".rockspec" + else + filename = filename.."."..arch..".rock" + end + return fs.make_path(pathname, filename) +end + +--- Convert a pathname to a module identifier. +-- In Unix, for example, a path "foo/bar/baz.lua" is converted to +-- "foo.bar.baz"; "bla/init.lua" returns "bla"; "foo.so" returns "foo". +-- @param file string: Pathname of module +-- @return string: The module identifier, or nil if given path is +-- not a conformant module path (the function does not check if the +-- path actually exists). +function path_to_module(file) + assert(type(file) == "string") + + local name = file:match("(.*)%."..cfg.lua_extension.."$") + if name then + name = name:gsub(fs.dir_separator, ".") + local init = name:match("(.*)%.init$") + if init then + name = init + end + else + name = file:match("(.*)%."..cfg.lib_extension.."$") + if name then + name = name:gsub(fs.dir_separator, ".") + end + end + return name +end + +--- Obtain the directory name where a module should be stored. +-- For example, on Unix, "foo.bar.baz" will return "foo/bar". +-- @param mod string: A module name in Lua dot-separated format. +-- @return string: A directory name using the platform's separator. +function module_to_path(mod) + assert(type(mod) == "string") + return (mod:gsub("[^.]*$", ""):gsub("%.", fs.dir_separator)) +end + +--- Set up path-related variables for a given rock. +-- Create a "variables" table in the rockspec table, containing +-- adjusted variables according to the configuration file. +-- @param rockspec table: The rockspec table. +function configure_paths(rockspec) + assert(type(rockspec) == "table") + local vars = {} + for k,v in pairs(cfg.variables) do + vars[k] = v + end + local name, version = rockspec.name, rockspec.version + vars.PREFIX = install_dir(name, version) + vars.LUADIR = lua_dir(name, version) + vars.LIBDIR = lib_dir(name, version) + vars.CONFDIR = conf_dir(name, version) + vars.BINDIR = bin_dir(name, version) + vars.DOCDIR = doc_dir(name, version) + rockspec.variables = vars +end diff --git a/src/luarocks/persist.lua b/src/luarocks/persist.lua new file mode 100644 index 00000000..4f69184c --- /dev/null +++ b/src/luarocks/persist.lua @@ -0,0 +1,91 @@ + +--- Utility module for loading files into tables and +-- saving tables into files. +-- Implemented separately to avoid interdependencies, +-- as it is used in the bootstrapping stage of the cfg module. +module("luarocks.persist", package.seeall) + +--- Load a Lua file containing assignments, storing them in a table. +-- The global environment is not propagated to the loaded file. +-- @param filename string: the name of the file. +-- @param tbl table or nil: if given, this table is used to store +-- loaded values. +-- @return table or (nil, string): a table with the file's assignments +-- as fields, or nil and a message in case of errors. +function load_into_table(filename, tbl) + assert(type(filename) == "string") + assert(type(tbl) == "table" or not tbl) + + local chunk, err = loadfile(filename) + if not chunk then + return nil, err + end + local result = {} + if tbl then result = tbl end + setfenv(chunk, result) + chunk() + return result +end + +--- Write a table as Lua code representing a table to disk +-- (that is, in curly brackets notation). +-- This function handles only numbers, strings and tables +-- are keys (tables are handled recursively). +-- @param out userdata: a file object, open for writing. +-- @param tbl table: the table to be written. +local function write_table(out, tbl) + out:write("{") + local size = table.getn(tbl) + local sep = "" + local i = 1 + for k, v in pairs(tbl) do + out:write(sep) + if type(k) == "number" then + if k ~= i then + out:write(tostring(k).."=") + else + i = i + 1 + end + elseif type(k) == "table" then + out:write("[") + write_table(out, k) + out:write("]=") + else + if k:match("^[a-z_]+$") then + out:write(k.."=") + else + out:write("['"..k:gsub("'", "\\'").."']=") + end + end + local typ = type(v) + if typ == "table" then + write_table(out, v) + elseif typ == "string" then + out:write("'"..v:gsub("'", "\\'").."'") + else + out:write(tostring(v)) + end + sep = ", " + end + out:write("}\n") +end + +--- Save the contents of a table in a file. +-- Each element of the table is saved as a global assignment. +-- Only numbers, strings and tables (containing numbers, strings +-- or other recursively processed tables) are supported. +-- @return boolean or (nil, string): true if successful, or nil and a +-- message in case of errors. +function save_from_table(filename, tbl) + local out = io.open(filename, "w") + if not out then + return nil, "Cannot create file at "..filename + end + for k, v in pairs(tbl) do + out:write(k.." = ") + write_table(out, v) + out:write("\n") + end + out:close() + return true +end diff --git a/src/luarocks/remove.lua b/src/luarocks/remove.lua new file mode 100644 index 00000000..d7162336 --- /dev/null +++ b/src/luarocks/remove.lua @@ -0,0 +1,163 @@ + +--- Module implementing the LuaRocks "remove" command. +-- Uninstalls rocks. +module("luarocks.remove", package.seeall) + +local search = require("luarocks.search") +local deps = require("luarocks.deps") +local fetch = require("luarocks.fetch") +local rep = require("luarocks.rep") +local path = require("luarocks.path") +local util = require("luarocks.util") +local cfg = require("luarocks.cfg") +local manif = require("luarocks.manif") + +help_summary = "Uninstall a rock." +help_arguments = "[--force] []" +help = [[ +Argument is the name of a rock to be uninstalled. +If a version is not given, try to remove all versions at once. +Will only perform the removal if it does not break dependencies. +To override this check and force the removal, use --force. +]] + +--- Obtain a list of packages that depend on the given set of packages +-- (where all packages of the set are versions of one program). +-- @param name string: the name of a program +-- @param versions array of string: the versions to be deleted. +-- @return array of string: an empty table if no packages depend on any +-- of the given list, or an array of strings in "name/version" format. +local function check_dependents(name, versions) + local dependents = {} + local blacklist = {} + blacklist[name] = {} + for version, _ in pairs(versions) do + blacklist[name][version] = true + end + local local_rocks = {} + local query_all = search.make_query("") + query_all.exact_name = false + search.manifest_search(local_rocks, cfg.rocks_dir, query_all) + local_rocks[name] = nil + for rock_name, rock_versions in pairs(local_rocks) do + for rock_version, _ in pairs(rock_versions) do + local rockspec, err = fetch.load_rockspec(path.rockspec_file(rock_name, rock_version)) + if rockspec then + local _, missing = deps.match_deps(rockspec, blacklist) + if missing[name] then + table.insert(dependents, { name = rock_name, version = rock_version }) + end + end + end + end + return dependents +end + +--- Delete given versions of a program. +-- @param name string: the name of a program +-- @param versions array of string: the versions to be deleted. +-- @return boolean or (nil, string): true on success or nil and an error message. +local function delete_versions(name, versions) + + local manifest, err = manif.load_manifest(cfg.rocks_dir) + if not manifest then + return nil, err + end + + local commands = {} + for version, data in pairs(versions) do + for _, command in pairs(manifest.repository[name][version][1].commands) do + if not commands[command] then commands[command] = {} end + table.insert(commands[command], name.."/"..version) + end + end + + if next(commands) then + for command, users in pairs(commands) do + local providers_of_command = manifest.commands[command] + for _, user in ipairs(users) do + for i, value in ipairs(providers_of_command) do + if providers_of_command[i] == user then + table.remove(providers_of_command, i) + break + end + end + end + local remaining = next(providers_of_command) + if remaining then + local name, version = remaining:match("^([^/]*)/(.*)$") + rep.install_bins(name, version, command) + else + rep.delete_bin(command) + end + end + end + + for version, _ in pairs(versions) do + print("Removing "..name.." "..version.."...") + rep.delete_version(name, version) + end + + return true +end + +--- Driver function for the "install" command. +-- @param name string: name of a rock. If a version is given, refer to +-- a specific version; otherwise, try to remove all versions. +-- @param version string: When passing a package name, a version number +-- may also be given. +-- @return boolean or (nil, string): True if removal was +-- successful, nil and an error message otherwise. +function run(...) + local flags, name, version = util.parse_flags(...) + + if type(name) ~= "string" then + return nil, "Argument missing, see help." + end + local results = {} + search.manifest_search(results, cfg.rocks_dir, search.make_query(name, version)) + + local versions = results[name] + if not versions then + return nil, "Could not find rock '"..name..(version and " "..version or "").."' in local tree." + else + local version = next(versions) + local second = next(versions, version) + + print("Checking stability of dependencies on the absence of") + print(name.." "..table.concat(util.keys(versions), ", ").."...") + print() + + local dependents = check_dependents(name, versions) + + if #dependents == 0 or flags["force"] then + if #dependents > 0 then + print("The following packages may be broken by this forced removal:") + for _, dependent in ipairs(dependents) do + print(dependent.name.." "..dependent.version) + end + print() + end + local ok, err1 = delete_versions(name, versions) + local ok, err2 = manif.make_manifest(cfg.rocks_dir) + if err1 or err2 then + return nil, err1 or err2 + end + else + if not second then + print("Will not remove "..name.." "..version..".") + print("Removing it would break dependencies for: ") + else + print("Will not remove all versions of "..name..".") + print("Removing them would break dependencies for: ") + end + for _, dependent in ipairs(dependents) do + print(dependent.name.." "..dependent.version) + end + print() + print("Use --force to force removal (warning: this may break modules).") + return nil, "Failed removing." + end + end + return true +end diff --git a/src/luarocks/rep.lua b/src/luarocks/rep.lua new file mode 100644 index 00000000..b5797803 --- /dev/null +++ b/src/luarocks/rep.lua @@ -0,0 +1,187 @@ + +--- Functions for managing the repository on disk. +module("luarocks.rep", package.seeall) + +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local cfg = require("luarocks.cfg") +local util = require("luarocks.util") + +--- Get all installed versions of a package. +-- @param name string: a package name. +-- @return table or nil: An array of strings listing installed +-- versions of a package, or nil if none is available. +function get_versions(name) + assert(type(name) == "string") + + local dirs = fs.dir(path.versions_dir(name)) + return (dirs and #dirs > 0) and dirs or nil +end + +--- Check if a package exists in a local repository. +-- Version numbers are compared as exact string comparison. +-- @param name string: name of package +-- @param version string: package version in string format +-- @return boolean: true if a package is installed, +-- false otherwise. +function is_installed(name, version) + assert(type(name) == "string") + assert(type(version) == "string") + + return fs.is_dir(path.install_dir(name, version)) +end + +--- Delete a package from the local repository. +-- Version numbers are compared as exact string comparison. +-- @param name string: name of package +-- @param version string: package version in string format +function delete_version(name, version) + assert(type(name) == "string") + assert(type(version) == "string") + + fs.delete(path.install_dir(name, version)) + if not get_versions(name) then + fs.delete(fs.make_path(cfg.rocks_dir, name)) + end +end + +--- Delete a command-line item from the bin directory. +-- @param command string: name of script +function delete_bin(command) + assert(type(command) == "string") + + fs.delete(fs.make_path(cfg.scripts_dir, command)) +end + +--- Install bin entries in the repository bin dir. +-- @param name string: name of package +-- @param version string: package version in string format +-- @param single_file string or nil: optional parameter, indicating the name +-- of a single file to install; if not given, all bin files from the package +-- are installed. +-- @return boolean or (nil, string): True if succeeded or nil and +-- and error message. +function install_bins(name, version, single_file) + assert(type(name) == "string") + assert(type(version) == "string") + + local bindir = path.bin_dir(name, version) + if fs.exists(bindir) then + local ok, err = fs.make_dir(cfg.scripts_dir) + if not ok then + return nil, "Could not create "..cfg.scripts_dir + end + local files = single_file and {single_file} or fs.dir(bindir) + for _, file in pairs(files) do + local fullname = fs.make_path(bindir, file) + local match = file:match("%.lua$") + local file + if not match then + file = io.open(fullname) + end + if match or (file and file:read():match("#!.*lua.*")) then + ok, err = fs.wrap_script(fullname, cfg.scripts_dir) + else + ok, err = fs.copy_binary(fullname, cfg.scripts_dir) + end + if file then file:close() end + if not ok then + return nil, err + end + end + end + return true +end + +--- Obtain a list of modules within an installed package. +-- @param package string: The package name; for example "luasocket" +-- @param version string: The exact version number including revision; +-- for example "2.0.1-1". +-- @return table: A table of modules where keys are module identifiers +-- in "foo.bar" format and values are pathnames in architecture-dependent +-- "foo/bar.so" format. If no modules are found or if package or version +-- are invalid, an empty table is returned. +function package_modules(package, version) + assert(type(package) == "string") + assert(type(version) == "string") + + local result = {} + local luas = fs.find(path.lua_dir(package, version)) + local libs = fs.find(path.lib_dir(package, version)) + for _, file in ipairs(luas) do + local name = path.path_to_module(file) + if name then + result[name] = file + end + end + for _, file in ipairs(libs) do + local name = path.path_to_module(file) + if name then + result[name] = file + end + end + return result +end + +--- Obtain a list of command-line scripts within an installed package. +-- @param package string: The package name; for example "luasocket" +-- @param version string: The exact version number including revision; +-- for example "2.0.1-1". +-- @return table: A table of items where keys are command names +-- as strings and values are pathnames in architecture-dependent +-- ".../bin/foo" format. If no modules are found or if package or version +-- are invalid, an empty table is returned. +function package_commands(package, version) + assert(type(package) == "string") + assert(type(version) == "string") + + local result = {} + local bindir = path.bin_dir(package, version) + local bins = fs.find(bindir) + for _, file in ipairs(bins) do + if file then + result[file] = fs.make_path(bindir, file) + end + end + return result +end + +--- Check if a rock contains binary parts or if it is pure Lua. +-- @param name string: name of an installed rock +-- @param version string: version of an installed rock +-- @return boolean: returns true if rock contains platform-specific +-- binary code, or false if it is a pure-Lua rock. +function is_binary_rock(name, version) + local bin_dir = path.bin_dir(name, version) + local lib_dir = path.lib_dir(name, version) + if fs.exists(lib_dir) then + return true + end + if fs.exists(bin_dir) then + for _, name in pairs(fs.find(bin_dir)) do + if fs.is_actual_binary(fs.make_path(bin_dir, name)) then + return true + end + end + end + return false +end + +function run_hook(rockspec, hook_name) + local hooks = rockspec.hooks + if not hooks then + return true + end + if not hooks.substituted_variables then + util.variable_substitutions(hooks, rockspec.variables) + hooks.substituted_variables = true + end + local hook = hooks[hook_name] + if hook then + print(hook) + if not fs.execute(hook) then + return nil, "Failed running "..hook_name.." hook." + end + end + return true +end diff --git a/src/luarocks/require.lua b/src/luarocks/require.lua new file mode 100644 index 00000000..f5d26078 --- /dev/null +++ b/src/luarocks/require.lua @@ -0,0 +1,263 @@ + +local global_env = _G +local plain_require = require +local plain_package_path = package.path +local plain_package_cpath = package.cpath +local package, assert, ipairs, pairs, os, print, table, type, next = + package, assert, ipairs, pairs, os, print, table, type, next + +--- Application interface with LuaRocks. +-- Load this module to LuaRocks-enable an application: +-- this overrides the require() function, making it able to +-- load modules installed as rocks. +module("luarocks.require") + +local path = plain_require("luarocks.path") +local manif = plain_require("luarocks.manif") +local deps = plain_require("luarocks.deps") +local cfg = plain_require("luarocks.cfg") + +context = {} + +-- Contains a table when rocks trees are loaded, +-- or 'false' to indicate rocks trees failed to load. +-- 'nil' indicates rocks trees were not attempted to be loaded yet. +rocks_trees = nil + +local function load_rocks_trees() + local any_ok = false + local trees = {} + for _, tree in pairs(cfg.rocks_trees) do + local rocks_dir = tree .. "/rocks/" + local manifest, err = manif.load_local_manifest(rocks_dir) + if manifest then + any_ok = true + table.insert(trees, {rocks_dir=rocks_dir, manifest=manifest}) + end + end + if not any_ok then + rocks_trees = false + return false + end + rocks_trees = trees + return true +end + +--- Process the dependencies of a package to determine its dependency +-- chain for loading modules. +-- @parse name string: The name of an installed rock. +-- @parse version string: The version of the rock, in string format +-- @parse manifest table: The local manifest table where this rock +-- is installed. +local function add_context(name, version, manifest) + -- assert(type(name) == "string") + -- assert(type(version) == "string") + -- assert(type(manifest) == "table") + + if context[name] then + return + end + context[name] = version + + local pkgdeps = manifest.dependencies and manifest.dependencies[name][version] + if pkgdeps then + for _, dep in ipairs(pkgdeps) do + local package, constraints = dep.name, dep.constraints + + for _, tree in pairs(rocks_trees) do + local entries = tree.manifest.repository[package] + if entries then + for version, packages in pairs(entries) do + if (not constraints) or deps.match_constraints(deps.parse_version(version), constraints) then + add_context(package, version, tree.manifest) + end + end + end + end + end + end +end + +--- Internal sorting function. +-- @param a table: A provider table. +-- @param b table: Another provider table. +-- @return boolean: True if the version of a is greater than that of b. +local function sort_versions(a,b) + return a.version > b.version +end + +--- Specify a dependency chain for LuaRocks. +-- In the presence of multiple versions of packages, it is necessary to, +-- at some point, indicate which dependency chain we're following. +-- set_context does this by allowing one to pick a package to be the +-- root of this dependency chain. Once a dependency chain is picked it's +-- easy to know which modules to load ("I want to use *this* version of +-- A, which requires *that* version of B, which requires etc etc etc"). +-- @param name string: The package name of an installed rock. +-- @param version string or nil: Optionally, a version number +-- When a version is not given, it picks the highest version installed. +-- @return boolean: true if succeeded, false otherwise. +function set_context(name, version) + --assert(type(name) == "string") + --assert(type(version) == "string" or not version) + + if rocks_trees == false or (not rocks_trees and not load_rocks_trees()) then + return false + end + + local manifest + local vtables = {} + for _, tree in ipairs(rocks_trees) do + if version then + local manif_repo = tree.manifest.repository + if manif_repo[name] and manif_repo[name][version] then + manifest = tree.manifest + break + end + else + local versions = manif.get_versions(name, tree.manifest) + for _, version in ipairs(versions) do + table.insert(vtables, {version = deps.parse_version(version), manifest = tree.manifest}) + end + end + end + if not version then + if not next(vtables) then + table.sort(vtables, sort_versions) + local highest = vtables[#vtables] + version = highest.version.string + manifest = highest.manifest + end + end + if not manifest then + return false + end + + add_context(name, version, manifest) + -- TODO: platform independence + local lpath, cpath = "", "" + for name, version in pairs(context) do + lpath = lpath .. path.lua_dir(name, version) .. "/?.lua;" + lpath = lpath .. path.lua_dir(name, version) .. "/?/init.lua;" + cpath = cpath .. path.lib_dir(name, version) .."/?."..cfg.lib_extension..";" + end + global_env.package.path = lpath .. plain_package_path + global_env.package.cpath = cpath .. plain_package_cpath +end + +--- Call the vanilla require() function using specially constructed +-- package paths so that it finds exactly the version we want it to find. +-- @param name string: The rock name. +-- @param version string: The rock version. +-- @param module string: The module name, in require() notation. +-- @return The result returned by require(). +local function plain_require_on(module, name, version, rocks_dir, ...) + --assert(type(module) == "string") + --assert(type(name) == "string") + --assert(type(version) == "string") + + local global_package = global_env.package + local save_path = global_package.path + local save_cpath = global_package.cpath + global_package.path = path.lua_dir(name, version, rocks_dir) .. "/?.lua;" + .. path.lua_dir(name, version, rocks_dir) .. "/?/init.lua;" .. save_path + global_package.cpath = path.lib_dir(name, version, rocks_dir) .. "/?."..cfg.lib_extension..";" .. save_cpath + local result = plain_require(module, ...) + global_package.path = save_path + global_package.cpath = save_cpath + return result +end + +local function pick_module(module, constraints) + --assert(type(module) == "string") + --assert(not constraints or type(constraints) == "string") + + if not rocks_trees and not load_rocks_trees() then + return nil + end + + if constraints then + if type(constraints) == "string" then + constraints = deps.parse_constraints(constraints) + else + constraints = nil + end + end + + local providers = {} + for _, tree in pairs(rocks_trees) do + local entries = tree.manifest.modules[module] + if entries then + for _, entry in pairs(entries) do + local name, version = entry:match("^([^/]*)/(.*)$") + if context[name] == version then + return name, version, tree + end + version = deps.parse_version(version) + if (not constraints) or deps.match_constraints(version, constraints) then + table.insert(providers, {name = name, version = version, repo = tree}) + end + end + end + end + + if next(providers) then + table.sort(providers, sort_versions) + local first = providers[1] + return first.name, first.version.string, first.repo + end +end + +--- Inform which rock LuaRocks would use if require() is called +-- with the given arguments. +-- @param module string: The module name, like in plain require(). +-- @param constraints string or nil: An optional comma-separated +-- list of version constraints. +-- @return (string, string) or nil: Rock name and version if the +-- requested module can be supplied by LuaRocks, or nil if it can't. +function get_rock_from_module(module, constraints) + --assert(type(module) == "string") + --assert(not constraints or type(constraints) == "string") + local name, version = pick_module(module, constraints) + return name, version +end + +--- Function that overloads require(), adding LuaRocks support. +-- This function wraps around Lua's standard require() call, +-- allowing it to find modules installed by LuaRocks. +-- A module is searched in installed rocks that match the +-- current LuaRocks context. If module is not part of the +-- context, or if a context has not yet been set, the module +-- in the package with the highest version is used. +-- If a module is not available in any installed rock, plain +-- require() is called, using the original package.path and +-- package.cpath lookup locations. +-- Additionally, version constraints for the matching rock may +-- be given as a second parameter. If version constraints could +-- not be fulfilled, this equates to the rock not being +-- available: as such, plain require() with the default paths +-- is called as a fallback. +-- @param module string: The module name, like in plain require(). +-- @param constraints string or nil: An optional comma-separated +-- list of version constraints. +-- @return table: The module table (typically), like in plain +-- require(). See require() +-- in the Lua reference manual for details. +function require(module, ...) + --assert(type(module) == "string") + + if package.loaded[module] or rocks_trees == false or (not rocks_trees and not load_rocks_trees()) then + return plain_require(module, ...) + end + + local name, version, repo = pick_module(module, ...) + + if not name then + return plain_require(module, ...) + else + add_context(name, version, repo.manifest) + return plain_require_on(module, name, version, repo.rocks_dir, ...) + end +end + +global_env.require = require diff --git a/src/luarocks/search.lua b/src/luarocks/search.lua new file mode 100644 index 00000000..a3ac68f3 --- /dev/null +++ b/src/luarocks/search.lua @@ -0,0 +1,394 @@ + +--- Module implementing the LuaRocks "search" command. +-- Queries LuaRocks servers. +module("luarocks.search", package.seeall) + +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local manif = require("luarocks.manif") +local deps = require("luarocks.deps") +local cfg = require("luarocks.cfg") +local util = require("luarocks.util") + +help_summary = "Query the LuaRocks servers." +help_arguments = "[--source] [--binary] { [] | --all }" +help = [[ +--source Return only rockspecs and source rocks, + to be used with the "build" command. +--binary Return only pure Lua and binary rocks (rocks that can be used + with the "install" command without requiring a C toolchain). +--all List all contents of the server that are suitable to + this platform, do not filter by name. +]] + +--- Convert the arch field of a query table to table format. +-- @param query table: A query table. +local function query_arch_as_table(query) + local format = type(query.arch) + if format == "table" then + return + elseif format == "nil" then + local accept = {} + accept["src"] = true + accept["all"] = true + accept["rockspec"] = true + accept["installed"] = true + accept[cfg.arch] = true + query.arch = accept + elseif format == "string" then + local accept = {} + for a in string.gmatch(query.arch, "[%w_]+") do + accept[a] = true + end + query.arch = accept + end +end + +--- Store a search result (a rock or rockspec) in the results table. +-- @param results table: The results table, where keys are package names and +-- versions are tables matching version strings to an array of servers. +-- @param name string: Package name. +-- @param version string: Package version. +-- @param arch string: Architecture of rock ("all", "src" or platform +-- identifier), "rockspec" or "installed" +-- @param repo string: Pathname of a local repository of URL of +-- rocks server. +local function store_result(results, name, version, arch, repo) + assert(type(results) == "table") + assert(type(name) == "string") + assert(type(version) == "string") + assert(type(arch) == "string") + assert(type(repo) == "string") + + if not results[name] then results[name] = {} end + if not results[name][version] then results[name][version] = {} end + table.insert(results[name][version], { + arch = arch, + repo = repo + }) +end + +--- Test the name field of a query. +-- If query has a boolean field exact_name set to false, +-- then substring match is performed; otherwise, exact string +-- comparison is done. +-- @param query table: A query in dependency table format. +-- @param name string: A package name. +-- @return boolean: True if names match, false otherwise. +local function match_name(query, name) + assert(type(query) == "table") + assert(type(name) == "string") + if query.exact_name == false then + return name:find(query.name, 0, true) and true or false + else + return name == query.name + end +end + +--- Store a match in a results table if version matches query. +-- Name, version, arch and repository path are stored in a given +-- table, optionally checking if version and arch (if given) match +-- a query. +-- @param results table: The results table, where keys are package names and +-- versions are tables matching version strings to an array of servers. +-- @param repo string: URL or pathname of the repository. +-- @param name string: The name of the package being tested. +-- @param version string: The version of the package being tested. +-- @param arch string: The arch of the package being tested. +-- @param query table: A table describing the query in dependency +-- format (for example, {name = "filesystem", exact_name = false, +-- constraints = {op = "~>", version = {1,0}}}, arch = "rockspec"). +-- If the arch field is omitted, the local architecture (cfg.arch) +-- is used. The special value "any" is also recognized, returning all +-- matches regardless of architecture. +local function store_if_match(results, repo, name, version, arch, query) + if match_name(query, name) then + if query.arch[arch] or query.arch["any"] then + if deps.match_constraints(deps.parse_version(version), query.constraints) then + store_result(results, name, version, arch, repo) + end + end + end +end + +--- Perform search on a local repository. +-- @param repo string: The pathname of the local repository. +-- @param query table: A table describing the query in dependency +-- format (for example, {name = "filesystem", exact_name = false, +-- constraints = {op = "~>", version = {1,0}}}, arch = "rockspec"). +-- If the arch field is omitted, the local architecture (cfg.arch) +-- is used. The special value "any" is also recognized, returning all +-- matches regardless of architecture. +-- @param results table or nil: If given, this table will store the +-- results; if not given, a new table will be created. +-- @param table: The results table, where keys are package names and +-- versions are tables matching version strings to an array of servers. +-- If a table was given in the "results" parameter, that is the result value. +function disk_search(repo, query, results) + assert(type(repo) == "string") + assert(type(query) == "table") + assert(type(results) == "table" or not results) + if not results then + results = {} + end + query_arch_as_table(query) + + for _, name in pairs(fs.dir(repo)) do + local pathname = fs.make_path(repo, name) + local rname, rversion, rarch = path.parse_rock_name(name) + if not rname then + rname, rversion, rarch = path.parse_rockspec_name(name) + end + if fs.is_dir(pathname) then + for _, version in pairs(fs.dir(pathname)) do + if version:match("-%d+$") then + store_if_match(results, repo, name, version, "installed", query) + end + end + elseif rname then + store_if_match(results, repo, rname, rversion, rarch, query) + end + end + return results +end + +--- Perform search on a rocks server. +-- @param results table: The results table, where keys are package names and +-- versions are tables matching version strings to an array of servers. +-- @param repo string: The URL of the rocks server. +-- @param query table: A table describing the query in dependency +-- format (for example, {name = "filesystem", exact_name = false, +-- constraints = {op = "~>", version = {1,0}}}, arch = "rockspec"). +-- If the arch field is omitted, the local architecture (cfg.arch) +-- is used. The special value "any" is also recognized, returning all +-- matches regardless of architecture. +function manifest_search(results, repo, query) + assert(type(results) == "table") + assert(type(repo) == "string") + assert(type(query) == "table") + + query_arch_as_table(query) + local manifest, err = manif.load_manifest(repo) + if not manifest then + print(err) + return + end + for name, versions in pairs(manifest.repository) do + for version, items in pairs(versions) do + for _, item in ipairs(items) do + store_if_match(results, repo, name, version, item.arch, query) + end + end + end +end + +--- Search on all configured rocks servers. +-- @param query table: A dependency query. +-- @return table or (nil, string): A table where keys are package names +-- and values are tables matching version strings to an array of +-- rocks servers; if no results are found, an empty table is returned. +-- In case of errors, nil and and error message are returned. +local function search_repos(query) + assert(type(query) == "table") + + local results = {} + for _, repo in ipairs(cfg.rocks_servers) do + local protocol, pathname = fs.split_url(repo) + if protocol == "file" then + repo = pathname + end + manifest_search(results, repo, query) + end + return results +end + +--- Prepare a query in dependency table format. +-- @param name string: The query name. +-- @param version string or nil: +-- @return table: A query in table format +function make_query(name, version) + assert(type(name) == "string") + assert(type(version) == "string" or not version) + + local query = { + name = name, + constraints = {} + } + if version then + table.insert(query.constraints, { op = "~>", version = deps.parse_version(version)}) + end + return query +end + +--- Get the URL for the latest in a set of versions. +-- @param name string: The package name to be used in the URL. +-- @param versions table: An array of version informations, as stored +-- in search results tables. +-- @return string or nil: the URL for the latest version if one could +-- be picked, or nil. +local function pick_latest_version(name, versions) + assert(type(name) == "string") + assert(type(versions) == "table") + + local vtables = {} + for v, _ in pairs(versions) do + table.insert(vtables, deps.parse_version(v)) + end + table.sort(vtables) + local version = vtables[#vtables].string + local items = versions[version] + if items then + local pick = 1 + for i, item in ipairs(items) do + if (item.arch == 'src' and items[pick].arch == 'rockspec') + or (item.arch ~= 'src' and item.arch ~= 'rockspec') then + pick = i + end + end + return path.make_url(items[pick].repo, name, version, items[pick].arch) + end + return nil +end + +--- Attempt to get a single URL for a given search. +-- @param query table: A dependency query. +-- @return string or table or (nil, string): URL for matching rock if +-- a single one was found, a table of candidates if it could not narrow to +-- a single result, or nil followed by an error message. +function find_suitable_rock(query) + assert(type(query) == "table") + + local results, err = search_repos(query) + if not results then + return nil, err + end + local first = results and next(results) + if first and next(results, first) == nil then + return pick_latest_version(query.name, results[first]) + elseif not first then + return nil, "No results matching query were found." + else + return results + end +end + +--- Print a list of rocks/rockspecs on standard output. +-- @param results table: A table where keys are package names and versions +-- are tables matching version strings to an array of rocks servers. +-- @param show_repo boolean or nil: Whether to show repository +-- information or not. Default is true. +function print_results(results, show_repo) + assert(type(results) == "table") + assert(type(show_repo) == "boolean" or not show_repo) + -- Force display of repo location for the time being + show_repo = true -- show_repo == nil and true or show_repo + + for package, versions in util.sortedpairs(results) do + print(package) + for version, repos in util.sortedpairs(versions, deps.compare_versions) do + if show_repo then + for _, repo in ipairs(repos) do + print(" "..version.." ("..repo.arch..") - "..repo.repo) + end + else + print(" "..version) + end + end + print() + end +end + +--- Splits a list of search results into two lists, one for "source" results +-- to be used with the "build" command, and one for "binary" results to be +-- used with the "install" command. +-- @param results table: A search results table. +-- @return (table, table): Two tables, one for source and one for binary +-- results. +local function split_source_and_binary_results(results) + local sources, binaries = {}, {} + for name, versions in pairs(results) do + for version, repos in pairs(versions) do + for _, repo in ipairs(repos) do + local where = sources + if repo.arch == "all" or repo.arch == cfg.arch then + where = binaries + end + store_result(where, name, version, repo.arch, repo.repo) + end + end + end + return sources, binaries +end + +--- Given a name and optionally a version, try to find in the rocks +-- servers a single .src.rock or .rockspec file that satisfies +-- the request, and run the given function on it; or display to the +-- user possibilities if it couldn't narrow down a single match. +-- @param action function: A function that takes a .src.rock or +-- .rockspec URL as a parameter. +-- @string name string: A rock name +-- @string version string or nil: A version number may also be given. +-- @return The result of the action function, or nil and an error message. +function act_on_src_or_rockspec(action, name, version) + assert(type(action) == "function") + assert(type(name) == "string") + assert(type(version) == "string" or not version) + + local query = make_query(name, version) + query.arch = "src|rockspec" + local results, err = find_suitable_rock(query) + if type(results) == "string" then + return action(results) + elseif type(results) == "table" and next(results) then + print("Multiple search results were returned.") + print() + print("Search results:") + print("---------------") + print_results(results) + return nil, "Please narrow your query." + else + return nil, "Could not find a result named "..name.."." + end +end + +--- Driver function for "search" command. +-- @param name string: A substring of a rock name to search. +-- @param version string or nil: a version may also be passed. +-- @return boolean or (nil, string): True if build was successful; nil and an +-- error message otherwise. +function run(...) + local flags, name, version = util.parse_flags(...) + + if flags["all"] then + name, version = "", "" + end + + if type(name) ~= "string" and not flags["all"] then + return nil, "Enter name and version or use --all; see help." + end + + local query = make_query(name, version) + query.exact_name = false + local results, err = search_repos(query) + if not results then + return nil, err + end + print() + print("Search results:") + print("===============") + print() + local sources, binaries = split_source_and_binary_results(results) + if next(sources) and not flags["binary"] then + print("Rockspecs and source rocks:") + print("---------------------------") + print() + print_results(sources, true) + end + if next(binaries) and not flags["source"] then + print("Binary and pure-Lua rocks:") + print("--------------------------") + print() + print_results(binaries, true) + end + return true +end diff --git a/src/luarocks/type_check.lua b/src/luarocks/type_check.lua new file mode 100644 index 00000000..ec9d6c5c --- /dev/null +++ b/src/luarocks/type_check.lua @@ -0,0 +1,233 @@ + +--- Type-checking functions. +-- Functions and definitions for doing a basic lint check on files +-- loaded by LuaRocks. +module("luarocks.type_check", package.seeall) + +rockspec_format = "1.0" + +rockspec_types = { + rockspec_format = "string", + MUST_package = "string", + MUST_version = "string", + description = { + summary = "string", + detailed = "string", + homepage = "string", + license = "string", + maintainer = "string" + }, + dependencies = { + platforms = {}, + ANY = "string" + }, + supported_platforms = { + ANY = "string" + }, + external_dependencies = { + platforms = {}, + ANY = { + program = "string", + header = "string", + library = "string" + } + }, + MUST_source = { + platforms = {}, + MUST_url = "string", + md5 = "string", + file = "string", + dir = "string", + tag = "string", + branch = "string", + cvs_tag = "string", + cvs_module = "string" + }, + build = { + platforms = {}, + type = "string", + install = { + lua = { + MORE = true + }, + lib = { + MORE = true + }, + conf = { + MORE = true + }, + bin = { + MORE = true + } + }, + copy_directories = { + ANY = "string" + }, + MORE = true + }, + hooks = { + platforms = {}, + post_install = "string" + } +} + +rockspec_types.build.platforms.ANY = rockspec_types.build +rockspec_types.dependencies.platforms.ANY = rockspec_types.dependencies +rockspec_types.external_dependencies.platforms.ANY = rockspec_types.external_dependencies +rockspec_types.MUST_source.platforms.ANY = rockspec_types.MUST_source +rockspec_types.hooks.platforms.ANY = rockspec_types.hooks + +manifest_types = { + MUST_repository = { + -- packages + ANY = { + -- versions + ANY = { + -- items + ANY = { + MUST_arch = "string", + modules = { ANY = "string" }, + commands = { ANY = "string" }, + dependencies = { ANY = "string" }, + -- TODO: to be extended with more metadata. + } + } + } + }, + MUST_modules = { + -- modules + ANY = { + -- providers + ANY = "string" + } + }, + MUST_commands = { + -- modules + ANY = { + -- commands + ANY = "string" + } + }, + dependencies = { + -- each module + ANY = { + -- each version + ANY = { + -- each dependency + ANY = { + name = "string", + constraints = { + ANY = { + no_upgrade = "boolean", + op = "string", + version = { + string = "string", + ANY = 0, + } + } + } + } + } + } + } +} + +local type_check_table + +--- Type check an object. +-- The object is compared against an archetypical value +-- matching the expected type -- the actual values don't matter, +-- only their types. Tables are type checked recursively. +-- @param name any: The object name (for error messages). +-- @param item any: The object being checked. +-- @param expected any: The reference object. In case of a table, +-- its is structured as a type reference table. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +-- @see type_check_table +local function type_check_item(name, item, expected, context) + name = tostring(name) + + local item_type = type(item) + local expected_type = type(expected) + if expected_type == "number" then + if not tonumber(item) then + return nil, "Type mismatch on field "..context..name..": expected a number" + end + elseif expected_type == "table" then + if item_type ~= expected_type then + return nil, "Type mismatch on field "..context..name..": expected a table" + else + return type_check_table(item, expected, context..name..".") + end + elseif item_type ~= expected_type then + return nil, "Type mismatch on field "..context..name..": expected a "..expected_type + end + return true +end + +--- Type check the contents of a table. +-- The table's contents are compared against a reference table, +-- which contains the recognized fields, with archetypical values +-- matching the expected types -- the actual values of items in the +-- reference table don't matter, only their types (ie, for field x +-- in tbl that is correctly typed, type(tbl.x) == type(types.x)). +-- If the reference table contains a field called MORE, then +-- unknown fields in the checked table are accepted. +-- If it contains a field called ANY, then its type will be +-- used to check any unknown fields. If a field is prefixed +-- with MUST_, it is mandatory; its absence from the table is +-- a type error. +-- Tables are type checked recursively. +-- @param tbl table: The table to be type checked. +-- @param types table: The reference table, containing +-- values for recognized fields in the checked table. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +type_check_table = function(tbl, types, context) + assert(type(tbl) == "table") + assert(type(types) == "table") + + for k, v in pairs(tbl) do + local t = types[k] or (type(k) == "string" and types["MUST_"..k]) or types.ANY + if t then + local ok, err = type_check_item(k, v, t, context) + if not ok then return nil, err end + elseif types.MORE then + -- Accept unknown field + else + return nil, "Unknown field "..k + end + end + for k, v in pairs(types) do + local mandatory_key = k:match("^MUST_(.+)") + if mandatory_key then + if not tbl[mandatory_key] then + return nil, "Mandatory field "..context..mandatory_key.." is missing." + end + end + end + return true +end + +--- Type check a rockspec table. +-- Verify the correctness of elements from a +-- rockspec table, reporting on unknown fields and type +-- mismatches. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +function type_check_rockspec(rockspec) + assert(type(rockspec) == "table") + return type_check_table(rockspec, rockspec_types, "") +end + +--- Type check a manifest table. +-- Verify the correctness of elements from a +-- manifest table, reporting on unknown fields and type +-- mismatches. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +function type_check_manifest(manifest) + assert(type(manifest) == "table") + return type_check_table(manifest, manifest_types, "") +end diff --git a/src/luarocks/unpack.lua b/src/luarocks/unpack.lua new file mode 100644 index 00000000..60bc1295 --- /dev/null +++ b/src/luarocks/unpack.lua @@ -0,0 +1,148 @@ + +--- Module implementing the LuaRocks "unpack" command. +-- Unpack the contents of a rock. +module("luarocks.unpack", package.seeall) + +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local build = require("luarocks.build") + +help_summary = "Unpack the contents of a rock." +help_arguments = "{| []}" +help = [[ +Unpacks the contents of a rock in a newly created directory. +Argument may be a rock file, or the name of a rock in a rocks server. +In the latter case, the app version may be given as a second argument. +]] + +--- Load a rockspec file to the given directory, fetches the source +-- files specified in the rockspec, and unpack them inside the directory. +-- @param rockspec_file string: The URL for a rockspec file. +-- @param dir_name string: The directory where to store and unpack files. +-- @return table or (nil, string): the loaded rockspec table or +-- nil and an error message. +local function unpack_rockspec(rockspec_file, dir_name) + assert(type(rockspec_file) == "string") + assert(type(dir_name) == "string") + + local rockspec = fetch.load_rockspec(rockspec_file) + if not rockspec then + return nil, "Failed loading rockspec "..rockspec_file + end + fs.change_dir(dir_name) + local ok, sources_dir = fetch.fetch_sources(rockspec, true, ".") + if not ok then + return nil, sources_dir + end + fs.change_dir(dir_name) + build.apply_patches(rockspec) + fs.pop_dir() + return rockspec +end + +--- Load a .rock file to the given directory and unpack it inside it. +-- @param rock_file string: The URL for a .rock file. +-- @param dir_name string: The directory where to unpack. +-- @return table or (nil, string): the loaded rockspec table or +-- nil and an error message. +local function unpack_rock(rock_file, dir_name, kind) + assert(type(rock_file) == "string") + assert(type(dir_name) == "string") + + local ok, err = fetch.fetch_and_unpack_rock(rock_file, dir_name) + if not ok then + return nil, "Failed unzipping rock "..rock_file + end + fs.change_dir(dir_name) + local rockspec_file = dir_name..".rockspec" + local rockspec, err = fetch.load_rockspec(rockspec_file) + if not rockspec then + return nil, "Failed loading rockspec "..rockspec_file..": "..err + end + if kind == "src" then + if rockspec.source.file then + local ok, err = fs.unpack_archive(rockspec.source.file) + if not ok then + return nil, err + end + fs.change_dir(rockspec.source.dir) + build.apply_patches(rockspec) + fs.pop_dir() + end + end + return rockspec +end + +--- Create a directory and perform the necessary actions so that +-- the sources for the rock and its rockspec are unpacked inside it, +-- laid out properly so that the 'make' command is able to build the module. +-- @param file string: A rockspec or .rock URL. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +local function run_unpacker(file) + assert(type(file) == "string") + + local base_name = fs.base_name(file) + local dir_name, kind, extension = base_name:match("(.*)%.([^.]+)%.(rock)$") + if not extension then + dir_name, extension = base_name:match("(.*)%.(rockspec)$") + kind = "rockspec" + end + if not extension then + return nil, file.." does not seem to be a valid filename." + end + + if (fs.exists(dir_name)) then + return nil, "Directory "..dir_name.." already exists." + end + fs.make_dir(dir_name) + local rollback = util.schedule_function(fs.delete, fs.absolute_name(dir_name)) + + local rockspec, err + if extension == "rock" then + rockspec, err = unpack_rock(file, dir_name, kind) + elseif extension == "rockspec" then + rockspec, err = unpack_rockspec(file, dir_name) + end + if not rockspec then + return nil, err + end + if kind == "src" or kind == "rockspec" then + if rockspec.source.dir ~= "." then + local ok = fs.copy(rockspec.local_filename, rockspec.source.dir) + if not ok then + return nil, "Failed copying unpacked rockspec into unpacked source directory." + end + end + print() + print("Done. You may now enter directory ") + print(fs.make_path(dir_name, rockspec.source.dir)) + print("and type 'luarocks make' to build.") + end + util.remove_scheduled_function(rollback) + return true +end + +--- Driver function for the "unpack" command. +-- @param name string: may be a rock filename, for unpacking a +-- rock file or the name of a rock to be fetched and unpacked. +-- @param version string or nil: if the name of a package is given, a +-- version may also be passed. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +function run(...) + local flags, name, version = util.parse_flags(...) + + assert(type(version) == "string" or not version) + if type(name) ~= "string" then + return nil, "Argument missing, see help." + end + + if name:match(".*%.rock") or name:match(".*%.rockspec") then + return run_unpacker(name) + else + local search = require("luarocks.search") + return search.act_on_src_or_rockspec(run_unpacker, name, version) + end +end diff --git a/src/luarocks/util.lua b/src/luarocks/util.lua new file mode 100644 index 00000000..50efaa9d --- /dev/null +++ b/src/luarocks/util.lua @@ -0,0 +1,180 @@ + +local global_env = _G + +--- Utility functions shared by other modules. +-- Does not requires modules directly (only as locals +-- inside specific functions) to avoid interdependencies, +-- as this is used in the bootstrapping stage of luarocks.cfg. +module("luarocks.util", package.seeall) + +local scheduled_functions = {} + +--- Schedule a function to be executed upon program termination. +-- This is useful for actions such as deleting temporary directories +-- or failure rollbacks. +-- @param f function: Function to be executed. +-- @param ... arguments to be passed to function. +-- @return table: A token representing the scheduled execution, +-- which can be used to remove the item later from the list. +function schedule_function(f, ...) + assert(type(f) == "function") + + local item = { fn = f, args = {...} } + table.insert(scheduled_functions, item) + return item +end + +--- Unschedule a function. +-- This is useful for cancelling a rollback of a completed operation. +-- @param table: The token representing the scheduled function that was +-- returned from the schedule_function call. +function remove_scheduled_function(item) + for k, v in pairs(scheduled_functions) do + if v == item then + table.remove(scheduled_functions, k) + return + end + end +end + +--- Execute scheduled functions. +-- Some calls create temporary files and/or directories and register +-- corresponding cleanup functions. Calling this function will run +-- these function, erasing temporaries. +-- Functions are executed in the inverse order they were scheduled. +function run_scheduled_functions() + local fs = require("luarocks.fs") + fs.change_dir_to_root() + for i = #scheduled_functions, 1, -1 do + local item = scheduled_functions[i] + item.fn(unpack(item.args)) + end +end + +--- Extract flags from an arguments list. +-- Given string arguments, extract flag arguments into a flags set. +-- For example, given "foo", "--tux=beep", "--bla", "bar", "--baz", +-- it would return the following: +-- {["bla"] = true, ["tux"] = "beep", ["baz"] = true}, "foo", "bar". +function parse_flags(...) + local args = {...} + local flags = {} + for i = #args, 1, -1 do + local flag = args[i]:match("^%-%-(.*)") + if flag then + local var,val = flag:match("([a-z_%-]*)=(.*)") + if val then + flags[var] = val + else + flags[flag] = true + end + table.remove(args, i) + end + end + return flags, unpack(args) +end + +--- Merges contents of src on top of dst's contents. +-- @param dst Destination table, which will receive src's contents. +-- @param src Table which provides new contents to dst. +-- @see platform_overrides +function deep_merge(dst, src) + for k, v in pairs(src) do + if type(v) == "table" then + if not dst[k] then + dst[k] = {} + end + deep_merge(dst[k], v) + else + dst[k] = v + end + end +end + +--- Perform platform-specific overrides on a table. +-- Overrides values of table with the contents of the appropriate +-- subset of its "platforms" field. The "platforms" field should +-- be a table containing subtables keyed with strings representing +-- platform names. Names that match the contents of the global +-- cfg.platforms setting are used. For example, if +-- cfg.platforms= {"foo"}, then the fields of +-- tbl.platforms.foo will overwrite those of tbl with the same +-- names. For table values, the operation is performed recursively +-- (tbl.platforms.foo.x.y.z overrides tbl.x.y.z; other contents of +-- tbl.x are preserved). +-- @param tbl table or nil: Table which may contain a "platforms" field; +-- if it doesn't (or if nil is passed), this function does nothing. +function platform_overrides(tbl) + assert(type(tbl) == "table" or not tbl) + + local cfg = require("luarocks.cfg") + + if not tbl then return end + + if tbl.platforms then + for _, platform in ipairs(cfg.platforms) do + local platform_tbl = tbl.platforms[platform] + if platform_tbl then + deep_merge(tbl, platform_tbl) + end + end + end + tbl.platforms = nil +end + +--- Perform make-style variable substitutions on string values of a table. +-- For every string value tbl.x which contains a substring of the format +-- "$(XYZ)" will have this substring replaced by vars["XYZ"], if that field +-- exists in vars. Only string values are processed; this function +-- does not scan subtables recursively. +-- @param tbl table: Table to have its string values modified. +-- @param vars table: Table containing string-string key-value pairs +-- representing variables to replace in the strings values of tbl. +function variable_substitutions(tbl, vars) + assert(type(tbl) == "table") + assert(type(vars) == "table") + + local updated = {} + for k, v in pairs(tbl) do + if type(v) == "string" then + updated[k] = v:gsub("%$%((%a[%a%d_]+)%)", vars) + end + end + for k, v in pairs(updated) do + tbl[k] = v + end +end + +--- Return an array of keys of a table. +-- @param tbl table: The input table. +-- @return table: The array of keys. +function keys(tbl) + local ks = {} + for k,_ in pairs(tbl) do + table.insert(ks, k) + end + return ks +end + +-- The iterator function used internally by util.sortedpairs. +-- @param tbl table: The table to be iterated. +-- @param sort_function function or nil: An optional comparison function +-- to be used by table.sort when sorting keys. +-- @see sortedpairs +local function sortedpairs_iterator(tbl, sort_function) + local ks = keys(tbl) + table.sort(ks, sort_function) + for _, k in ipairs(ks) do + coroutine.yield(k, tbl[k]) + end +end + +--- A table iterator generator that returns elements sorted by key, +-- to be used in "for" loops. +-- @param tbl table: The table to be iterated. +-- @param sort_function function or nil: An optional comparison function +-- to be used by table.sort when sorting keys. +-- @return function: the iterator function. +function sortedpairs(tbl, sort_function) + return coroutine.wrap(function() sortedpairs_iterator(tbl, sort_function) end) +end diff --git a/test/run_tests.sh b/test/run_tests.sh new file mode 100755 index 00000000..aa06854f --- /dev/null +++ b/test/run_tests.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +if [ -e ./run_tests.sh ] +then + cd ../src +elif [ -d src ] +then + cd src +elif ! [ -d luarocks ] +then + echo "Go to the src directory and run this." + exit 1 +fi + +if [ ! -d ../rocks ] +then + echo "Downloading entire rocks repository for tests" + cd .. + wget -r -nH -np -R"index.*" http://luarocks.luaforge.net/rocks/ + cd src +fi + +rocks=( + `ls ../rocks/*.rockspec | grep -v luacom` + `ls ../rocks/*.src.rock | grep -v luacom` +) + +bin/luarocks-admin make-manifest ../rocks || exit 1 + +[ "$1" ] && rocks=("$1") + +TRY() { + "$@" || { + echo "Failed running: $@" + exit 1 + } +} + +list_search() { + bin/luarocks list $name | grep $version +} + +for rock in "${rocks[@]}" +do + base=`basename $rock` + baserockspec=`basename $rock .rockspec` + basesrcrock=`basename $rock .src.rock` + if [ "$base" != "$baserockspec" ] + then + base=$baserockspec + name=`echo $base | sed 's/\(.*\)-[^-]*-[^-]*$/\1/'` + version=`echo $base | sed 's/.*-\([^-]*-[^-]*\)$/\1/'` + TRY bin/luarocks pack $rock + TRY bin/luarocks build $base.src.rock + TRY rm $base.src.rock + else + base=$basesrcrock + name=`echo $base | sed 's/\(.*\)-[^-]*-[^-]*$/\1/'` + version=`echo $base | sed 's/.*-\([^-]*-[^-]*\)$/\1/'` + TRY bin/luarocks build $rock + fi + TRY bin/luarocks pack $name $version + TRY bin/luarocks install $base.*.rock + TRY rm $base.*.rock + TRY list_search $name $version + bin/luarocks remove $name $version + # TODO: differentiate between error and dependency block. +done + +if bin/luarocks install nonexistant | grep "No results" +then echo "OK, got expected error." +else exit 1 +fi + +TRY ../test/test_deps.lua +TRY ../test/test_require.lua diff --git a/test/test_deps.lua b/test/test_deps.lua new file mode 100644 index 00000000..7236273c --- /dev/null +++ b/test/test_deps.lua @@ -0,0 +1,67 @@ +#!/usr/bin/env lua + +deps = require "luarocks.deps" + +print(deps.show_dep(deps.parse_dep("lfs 2.1.9pre5"), true)) +print(deps.show_dep(deps.parse_dep("cgilua cvs-2"), true)) +print(deps.show_dep(deps.parse_dep("foobar 0.0.1beta"), true)) +print(deps.show_dep(deps.parse_dep("foobar 0.0.1a"), true)) + +print(deps.show_dep(deps.parse_dep("foobar 1"), true)) +print(deps.show_dep(deps.parse_dep("foobar 2.0"), true)) +print(deps.show_dep(deps.parse_dep("foobar 3.5a4"), true)) +print(deps.show_dep(deps.parse_dep("foobar 1.1pre2"), true)) +print(deps.show_dep(deps.parse_dep("foobar 2.0-beta3"), true)) +print(deps.show_dep(deps.parse_dep("foobar 5.3"), true)) +print(deps.show_dep(deps.parse_dep("foobar 3.5rc2"), true)) +print(deps.show_dep(deps.parse_dep("foobar 4.19p"), true)) + +print() +comparisons = { +-- first second eq le + {"Vista", "XP", false, true}, + {"XP", "3.1", false, true}, + {"1.0", "1.0", true, false}, + {"2.2.10", "2.2-10", false, false}, + {"2.2", "2.2-10", true, false}, + {"1.0beta1", "1.0rc3", false, true}, + {"2.0beta3", "2.0", false, true}, + {"2.0beta", "2.0beta2", false, true}, + {"2.0beta4", "2.0beta3", false, false}, + {"2.1alpha1", "2.0beta1", false, false}, + {"1.5p3", "1.5.1", false, true}, + {"1.1.3", "1.1.3a", false, true}, + {"1.5a100", "1.5b1", false, true}, + {"2.0alpha100", "2.0beta1", false, true}, + {"2.0.0beta3", "2.0beta2", false, false}, + {"2.0-1", "2.0-2", false, true}, + {"2.0-2", "2.0-1", false, false}, + --[[ + -- Corner cases I don't wish to handle by now. + {"2.0.0beta2", "2.0beta2", true, true}, + {"2.0.0beta2", "2.0beta3", false, true}, + ]] +} + +local v1, v2 + +err = false + +function result(test, expected) + if test == expected then + print(test, "OK") + else + print(test, "ERROR", deps.show_version(v1, true), deps.show_version(v2, true)) + err = true + end +end + +for _, c in ipairs(comparisons) do + v1, v2 = deps.parse_version(c[1]), deps.parse_version(c[2]) + print(c[1].." == "..c[2].." ?") + result(v1 == v2, c[3]) + print(c[1].." < "..c[2].." ?") + result(v1 < v2, c[4]) +end + +if err then os.exit(1) end diff --git a/test/test_require.lua b/test/test_require.lua new file mode 100755 index 00000000..2a652d29 --- /dev/null +++ b/test/test_require.lua @@ -0,0 +1,25 @@ +#!/usr/bin/env lua + +local luarocks = require("luarocks.require") + +luarocks.set_context("cgilua", "cvs-2") + +print(package.path) + +print(package.cpath) + +local socket = require("socket") +if not socket then os.exit(1) end +print(socket, socket._VERSION) + +local socket2 = require("socket") +if not socket2 then os.exit(1) end +print(socket2, socket2._VERSION) + +local mime = require("mime") +if not mime then os.exit(1) end +print(mime, mime._VERSION) + +local socket = require("lfs") +if not lfs then os.exit(1) end +print(lfs, lfs._VERSION) -- cgit v1.2.3-55-g6feb