From 831a3c8a20780cb56a3e6544c62ba7eedf751ed2 Mon Sep 17 00:00:00 2001 From: hisham Date: Wed, 1 Apr 2009 19:15:49 +0000 Subject: LuaRocks 1.x development: reorganization, fixes and work on tool independence git-svn-id: http://luarocks.org/svn/luarocks/trunk@4 9ca3f7c1-7366-0410-b1a3-b5c78f85698c --- src/luarocks/build.lua | 75 ++--- src/luarocks/build/builtin.lua | 15 +- src/luarocks/command_line.lua | 2 +- src/luarocks/deps.lua | 21 +- src/luarocks/dir.lua | 47 +++ src/luarocks/fetch.lua | 91 +++--- src/luarocks/fetch/cvs.lua | 17 +- src/luarocks/fetch/git.lua | 17 +- src/luarocks/fetch/sscm.lua | 3 +- src/luarocks/fs.lua | 7 +- src/luarocks/fs/lua.lua | 565 ++++++++++++++++++++++++++++++++ src/luarocks/fs/unix.lua | 107 +++---- src/luarocks/fs/win32.lua | 55 ++-- src/luarocks/install.lua | 24 +- src/luarocks/list.lua | 4 +- src/luarocks/make.lua | 2 +- src/luarocks/manif.lua | 93 +----- src/luarocks/manif_core.lua | 73 +++++ src/luarocks/pack.lua | 13 +- src/luarocks/path.lua | 34 +- src/luarocks/rep.lua | 15 +- src/luarocks/search.lua | 7 +- src/luarocks/tools/patch.lua | 708 +++++++++++++++++++++++++++++++++++++++++ src/luarocks/tools/tar.lua | 143 +++++++++ src/luarocks/tools/zip.lua | 230 +++++++++++++ src/luarocks/type_check.lua | 2 +- src/luarocks/unpack.lua | 9 +- src/luarocks/validate.lua | 159 +++++++++ 28 files changed, 2189 insertions(+), 349 deletions(-) create mode 100644 src/luarocks/dir.lua create mode 100644 src/luarocks/fs/lua.lua create mode 100644 src/luarocks/manif_core.lua create mode 100644 src/luarocks/tools/patch.lua create mode 100644 src/luarocks/tools/tar.lua create mode 100644 src/luarocks/tools/zip.lua create mode 100644 src/luarocks/validate.lua diff --git a/src/luarocks/build.lua b/src/luarocks/build.lua index f0f7225d..5d527876 100644 --- a/src/luarocks/build.lua +++ b/src/luarocks/build.lua @@ -8,6 +8,7 @@ local util = require("luarocks.util") local rep = require("luarocks.rep") local fetch = require("luarocks.fetch") local fs = require("luarocks.fs") +local dir = require("luarocks.dir") local deps = require("luarocks.deps") local manif = require("luarocks.manif") @@ -38,10 +39,10 @@ local function install_files(files, location) 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)) + dest = dir.path(location, path.module_to_path(k)) end fs.make_dir(dest) - local ok = fs.copy(fs.make_path(file), dest) + local ok = fs.copy(dir.path(file), dest) if not ok then return nil, "Failed copying "..file end @@ -55,7 +56,7 @@ end -- @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+") + local fd = io.open(dir.path(fs.current_dir(), name), "w+") fd:write(content) fd:close() end @@ -76,9 +77,9 @@ function apply_patches(rockspec) end if build.patches then extract_from_rockspec(build.patches) - for patch, _ in util.sortedpairs(build.patches) do + for patch, patchdata in util.sortedpairs(build.patches) do print("Applying patch "..patch.."...") - local ok, err = fs.patch(tostring(patch)) + local ok, err = fs.apply_patch(tostring(patch, patchdata)) if not ok then return nil, "Failed applying patch "..patch end @@ -91,28 +92,28 @@ end -- @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. +-- @return boolean or (nil, string, [string]): True if succeeded or +-- nil and an error message followed by an error code. 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) + local rockspec, err, errcode = fetch.load_rockspec(rockspec_file) if err then - return nil, err + return nil, err, errcode 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) + local ok, err, errcode = deps.fulfill_dependencies(rockspec) if err then - return nil, err + return nil, err, errcode end - ok, err = deps.check_external_deps(rockspec, "build") + ok, err, errcode = deps.check_external_deps(rockspec, "build") if err then - return nil, err + return nil, err, errcode end local name, version = rockspec.name, rockspec.version @@ -121,13 +122,13 @@ function build_rockspec(rockspec_file, need_to_fetch, minimal_mode) end if not minimal_mode then - local _, dir + local _, source_dir if need_to_fetch then - ok, dir = fetch.fetch_sources(rockspec, true) + ok, source_dir, errcode = fetch.fetch_sources(rockspec, true) if not ok then - return nil, dir + return nil, source_dir, errcode end - fs.change_dir(dir) + fs.change_dir(source_dir) elseif rockspec.source.file then local ok, err = fs.unpack_archive(rockspec.source.file) if not ok then @@ -144,8 +145,8 @@ function build_rockspec(rockspec_file, need_to_fetch, minimal_mode) bin = path.bin_dir(name, version), } - for _, dir in pairs(dirs) do - fs.make_dir(dir) + for _, d in pairs(dirs) do + fs.make_dir(d) end local rollback = util.schedule_function(function() fs.delete(path.install_dir(name, version)) @@ -178,8 +179,8 @@ function build_rockspec(rockspec_file, need_to_fetch, minimal_mode) end if build.install then - for id, dir in pairs(dirs) do - ok, err = install_files(build.install[id], dir) + for id, install_dir in pairs(dirs) do + ok, err = install_files(build.install[id], install_dir) if not ok then return nil, err end @@ -188,16 +189,16 @@ function build_rockspec(rockspec_file, need_to_fetch, minimal_mode) 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) + for _, copy_dir in pairs(copy_directories) do + if fs.is_dir(copy_dir) then + local dest = dir.path(path.install_dir(name, version), copy_dir) fs.make_dir(dest) - fs.copy_contents(dir, dest) + fs.copy_contents(copy_dir, dest) end end - for _, dir in pairs(dirs) do - fs.remove_dir_if_empty(dir) + for _, d in pairs(dirs) do + fs.remove_dir_if_empty(d) end fs.pop_dir() @@ -227,21 +228,21 @@ end -- @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) +-- @return boolean or (nil, string, [string]): True if build was successful, +-- or false and an error message and an optional error code. +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 + local unpack_dir, err, errcode = fetch.fetch_and_unpack_rock(rock_file) + if not unpack_dir then + return nil, err, errcode 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.change_dir(unpack_dir) + local ok, err, errcode = build_rockspec(rockspec_file, need_to_fetch) fs.pop_dir() - return ok, err + return ok, err, errcode end --- Driver function for "build" command. @@ -268,7 +269,7 @@ function run(...) return install.install_binary_rock(name) elseif name:match("%.rock$") then return build_rock(name, true) - elseif not name:match(fs.dir_separator) then + elseif not name:match(dir.separator) then local search = require("luarocks.search") return search.act_on_src_or_rockspec(run, name, version) end diff --git a/src/luarocks/build/builtin.lua b/src/luarocks/build/builtin.lua index c93edaa2..cfd71858 100644 --- a/src/luarocks/build/builtin.lua +++ b/src/luarocks/build/builtin.lua @@ -6,6 +6,7 @@ local fs = require("luarocks.fs") local path = require("luarocks.path") local util = require("luarocks.util") local cfg = require("luarocks.cfg") +local dir = require("luarocks.dir") --- Check if platform was detected -- @param query string: The platform name to check. @@ -62,13 +63,13 @@ function run(rockspec) local extras = { unpack(objects) } add_flags(extras, "-libpath:%s", libdirs) add_flags(extras, "%s.lib", libraries) - local basename = fs.base_name(library):gsub(".[^.]*$", "") + local basename = dir.base_name(library):gsub(".[^.]*$", "") local deffile = basename .. ".def" - local def = io.open(fs.make_path(fs.current_dir(), deffile), "w+") + local def = io.open(dir.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 ok = execute(variables.LD, "-dll", "-def:"..deffile, "-out:"..library, dir.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") @@ -103,7 +104,7 @@ function run(rockspec) if type(info) == "string" then local ext = info:match(".([^.]+)$") if ext == "lua" then - local dest = fs.make_path(luadir, moddir) + local dest = dir.path(luadir, moddir) built_modules[info] = dest else info = {info} @@ -124,11 +125,11 @@ function run(rockspec) table.insert(objects, object) end if not ok then break end - local module_name = fs.make_path(moddir, name:match("([^.]*)$").."."..cfg.lib_extension):gsub("//", "/") + local module_name = dir.path(moddir, name:match("([^.]*)$").."."..cfg.lib_extension):gsub("//", "/") if moddir ~= "" then fs.make_dir(moddir) end - local dest = fs.make_path(libdir, moddir) + local dest = dir.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 @@ -141,7 +142,7 @@ function run(rockspec) end if ok then if fs.is_dir("lua") then - fs.copy_contents("lua", luadir) + ok = fs.copy_contents("lua", luadir) end end if ok then diff --git a/src/luarocks/command_line.lua b/src/luarocks/command_line.lua index 9389d6ef..e220bb34 100644 --- a/src/luarocks/command_line.lua +++ b/src/luarocks/command_line.lua @@ -1,5 +1,5 @@ -program_version = "1.0.1" +program_version = "1.1" --- Functions for command-line scripts. module("luarocks.command_line", package.seeall) diff --git a/src/luarocks/deps.lua b/src/luarocks/deps.lua index d5a64e52..6f986575 100644 --- a/src/luarocks/deps.lua +++ b/src/luarocks/deps.lua @@ -17,10 +17,11 @@ 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 manif_core = require("luarocks.manif_core") local fs = require("luarocks.fs") local fetch = require("luarocks.fetch") local path = require("luarocks.path") +local dir = require("luarocks.dir") local operators = { ["=="] = "==", @@ -320,7 +321,7 @@ local function match_dep(dep, blacklist) if dep.name == "lua" then versions = { "5.1" } else - versions = manif.get_versions(dep.name) + versions = manif_core.get_versions(dep.name) end if not versions then return nil @@ -399,8 +400,9 @@ end -- 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. +-- @return boolean or (nil, string, [string]): True if no errors occurred, or +-- nil and an error message if any test failed, followed by an optional +-- error code. function fulfill_dependencies(rockspec) if rockspec.supported_platforms then @@ -469,9 +471,9 @@ function fulfill_dependencies(rockspec) if not rock then return nil, "Could not find a rock to satisfy dependency: "..show_dep(dep) end - local ok, err = install.run(rock) + local ok, err, errcode = install.run(rock) if not ok then - return nil, "Failed installing dependency: "..rock.." - "..err + return nil, "Failed installing dependency: "..rock.." - "..err, errcode end end end @@ -521,7 +523,7 @@ function check_external_deps(rockspec, mode) prefix = extdir end for dirname, dirdata in pairs(dirs) do - dirdata.dir = vars[name.."_"..dirname] or fs.make_path(prefix, dirdata.subdir) + dirdata.dir = vars[name.."_"..dirname] or dir.path(prefix, dirdata.subdir) local file = files[dirdata.testfile] if file then local files = {} @@ -535,10 +537,11 @@ function check_external_deps(rockspec, mode) local found = false failed_file = nil for _, f in pairs(files) do + -- small convenience hack 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) + local testfile = dir.path(dirdata.dir, f) if fs.exists(testfile) then found = true break @@ -565,7 +568,7 @@ function check_external_deps(rockspec, mode) 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" + 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", "dependency" end end end diff --git a/src/luarocks/dir.lua b/src/luarocks/dir.lua new file mode 100644 index 00000000..5cb3b846 --- /dev/null +++ b/src/luarocks/dir.lua @@ -0,0 +1,47 @@ + +module("luarocks.dir", package.seeall) + +separator = "/" + +--- Strip the path off a path+filename. +-- @param pathname string: A path+name, such as "/a/b/c" +-- or "\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") + + return (pathname:gsub("/*$", ""):match("(.*/)[^/]*")) or "" +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 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 diff --git a/src/luarocks/fetch.lua b/src/luarocks/fetch.lua index b61fab28..5b2d20cb 100644 --- a/src/luarocks/fetch.lua +++ b/src/luarocks/fetch.lua @@ -3,6 +3,7 @@ module("luarocks.fetch", package.seeall) local fs = require("luarocks.fs") +local dir = require("luarocks.dir") local type_check = require("luarocks.type_check") local path = require("luarocks.path") local deps = require("luarocks.deps") @@ -18,8 +19,9 @@ local util = require("luarocks.util") -- 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. +-- @return string or (nil, string, [string]): the absolute local pathname for the +-- fetched file, or nil and a message in case of errors, followed by +-- an optional error code. function fetch_url(url, filename) assert(type(url) == "string") assert(type(filename) == "string" or not filename) @@ -30,9 +32,9 @@ function fetch_url(url, filename) elseif protocol == "http" or protocol == "ftp" or protocol == "https" then local ok = fs.download(url) if not ok then - return nil, "Failed downloading "..url + return nil, "Failed downloading "..url, "network" end - return fs.make_path(fs.current_dir(), filename or fs.base_name(url)) + return dir.path(fs.current_dir(), filename or dir.base_name(url)) else return nil, "Unsupported protocol "..protocol end @@ -46,30 +48,31 @@ end -- 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. +-- @return (string, string) or (nil, string, [string]): absolute local pathname of +-- the fetched file and temporary directory name; or nil and an error message +-- followed by an optional error code 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) + filename = filename or dir.base_name(url) local protocol, pathname = fs.split_url(url) if protocol == "file" then - return pathname, fs.dir_name(pathname) + return pathname, dir.dir_name(pathname) else - local dir = fs.make_temp_dir(tmpname) - if not dir then + local temp_dir = fs.make_temp_dir(tmpname) + if not temp_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) + util.schedule_function(fs.delete, temp_dir) + fs.change_dir(temp_dir) + local file, err, errcode = fetch_url(url, filename) + fs.pop_dir() if not file then - return nil, "Error fetching file: "..err + return nil, "Error fetching file: "..err, errcode end - fs.pop_dir() - return file, dir + return file, temp_dir end end @@ -79,37 +82,37 @@ end -- @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 +-- @return string or (nil, string, [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 name = dir.base_name(rock_file):match("(.*)%.[^.]*%.rock") - local rock_file, err = fetch_url_at_temp_dir(rock_file,"luarocks-rock-"..name) + local rock_file, err, errcode = fetch_url_at_temp_dir(rock_file,"luarocks-rock-"..name) if not rock_file then - return nil, "Could not fetch rock file: " .. err + return nil, "Could not fetch rock file: " .. err, errcode end rock_file = fs.absolute_name(rock_file) - local dir + local unpack_dir if dest then - dir = dest - fs.make_dir(dir) + unpack_dir = dest + fs.make_dir(unpack_dir) else - dir = fs.make_temp_dir(name) + unpack_dir = fs.make_temp_dir(name) end if not dest then - util.schedule_function(fs.delete, dir) + util.schedule_function(fs.delete, unpack_dir) end - fs.change_dir(dir) + fs.change_dir(unpack_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 + return unpack_dir end --- Back-end function that actually loads the local rockspec. @@ -142,7 +145,7 @@ function load_local_rockspec(filename) util.platform_overrides(rockspec.source) util.platform_overrides(rockspec.hooks) - local basename = fs.base_name(filename) + local basename = dir.base_name(filename) rockspec.name = basename:match("(.*)-[^-]*-[0-9]*") if not rockspec.name then return nil, "Expected filename in format 'name-version-revision.rockspec'." @@ -150,7 +153,7 @@ function load_local_rockspec(filename) 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) + rockspec.source.file = rockspec.source.file or dir.base_name(rockspec.source.url) end rockspec.source.protocol, rockspec.source.pathname = protocol, pathname @@ -167,7 +170,7 @@ function load_local_rockspec(filename) rockspec.local_filename = filename local filebase = rockspec.source.file or rockspec.source.url - local base = fs.base_name(filebase) + local base = dir.base_name(filebase) base = base:gsub("%.[^.]*$", ""):gsub("%.tar$", "") rockspec.source.dir = rockspec.source.dir or rockspec.source.module @@ -197,19 +200,19 @@ end -- 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. +-- @return table or (nil, string, [string]): A table representing the rockspec +-- or nil followed by an error message and optional error code. function load_rockspec(filename) assert(type(filename) == "string") - local name = fs.base_name(filename):match("(.*)%.rockspec") + local name = dir.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) + local filename, err, errcode = fetch_url_at_temp_dir(filename,"luarocks-rockspec-"..name) if not filename then - return nil, err + return nil, err, errcode end return load_local_rockspec(filename) @@ -220,9 +223,9 @@ end -- @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 +-- @return (string, string) or (nil, string, [string]): The absolute pathname of -- the fetched source tarball and the temporary directory created to --- store it; or nil and an error message. +-- store it; or nil and an error message and optional error code. function get_sources(rockspec, extract, dest_dir) assert(type(rockspec) == "table") assert(type(extract) == "boolean") @@ -231,17 +234,17 @@ function get_sources(rockspec, extract, dest_dir) local url = rockspec.source.url local name = rockspec.name.."-"..rockspec.version local filename = rockspec.source.file - local source_file, dir, err + local source_file, store_dir, err, errcode if dest_dir then fs.change_dir(dest_dir) - source_file, err = fetch_url(url, filename) + source_file, err, errcode = fetch_url(url, filename) fs.pop_dir() - dir = dest_dir + store_dir = dest_dir else - source_file, dir = fetch_url_at_temp_dir(url, "luarocks-source-"..name, filename) + source_file, store_dir, errcode = fetch_url_at_temp_dir(url, "luarocks-source-"..name, filename) end if not source_file then - return nil, err or dir + return nil, err or store_dir, errcode end if rockspec.source.md5 then if not fs.check_md5(source_file, rockspec.source.md5) then @@ -249,11 +252,11 @@ function get_sources(rockspec, extract, dest_dir) end end if extract then - fs.change_dir(dir) + fs.change_dir(store_dir) fs.unpack_archive(rockspec.source.file) fs.pop_dir() end - return source_file, dir + return source_file, store_dir end --- Download sources for building a rock, calling the appropriate protocol method. diff --git a/src/luarocks/fetch/cvs.lua b/src/luarocks/fetch/cvs.lua index 291388cb..c2957c09 100644 --- a/src/luarocks/fetch/cvs.lua +++ b/src/luarocks/fetch/cvs.lua @@ -3,6 +3,7 @@ module("luarocks.fetch.cvs", package.seeall) local fs = require("luarocks.fs") +local dir = require("luarocks.dir") local util = require("luarocks.util") --- Download sources for building a rock, using CVS. @@ -17,27 +18,27 @@ function get_sources(rockspec, extract, dest_dir) 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 module = rockspec.source.module or dir.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 + local store_dir if not dest_dir then - dir = fs.make_temp_dir(name_version) - if not dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then return nil, "Failed creating temporary directory." end - util.schedule_function(fs.delete, dir) + util.schedule_function(fs.delete, store_dir) else - dir = dest_dir + store_dir = dest_dir end - fs.change_dir(dir) + fs.change_dir(store_dir) if not fs.execute(unpack(command)) then return nil, "Failed fetching files from CVS." end fs.pop_dir() - return module, dir + return module, store_dir end diff --git a/src/luarocks/fetch/git.lua b/src/luarocks/fetch/git.lua index d0d204a9..d2420ef8 100644 --- a/src/luarocks/fetch/git.lua +++ b/src/luarocks/fetch/git.lua @@ -3,6 +3,7 @@ module("luarocks.fetch.git", package.seeall) local fs = require("luarocks.fs") +local dir = require("luarocks.dir") local util = require("luarocks.util") --- Download sources for building a rock, using git. @@ -17,7 +18,7 @@ function get_sources(rockspec, extract, dest_dir) assert(type(dest_dir) == "string" or not dest_dir) local name_version = rockspec.name .. "-" .. rockspec.version - local module = fs.base_name(rockspec.source.url) + local module = dir.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} @@ -26,17 +27,17 @@ function get_sources(rockspec, extract, dest_dir) if tag_or_branch then checkout_command = {"git", "checkout", tag_or_branch} end - local dir + local store_dir if not dest_dir then - dir = fs.make_temp_dir(name_version) - if not dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then return nil, "Failed creating temporary directory." end - util.schedule_function(fs.delete, dir) + util.schedule_function(fs.delete, store_dir) else - dir = dest_dir + store_dir = dest_dir end - fs.change_dir(dir) + fs.change_dir(store_dir) if not fs.execute(unpack(command)) then return nil, "Failed fetching files from GIT while cloning." end @@ -48,6 +49,6 @@ function get_sources(rockspec, extract, dest_dir) fs.pop_dir() end fs.pop_dir() - return module, dir + return module, store_dir end diff --git a/src/luarocks/fetch/sscm.lua b/src/luarocks/fetch/sscm.lua index a2c70b96..539014c2 100644 --- a/src/luarocks/fetch/sscm.lua +++ b/src/luarocks/fetch/sscm.lua @@ -3,6 +3,7 @@ module("luarocks.fetch.sscm", package.seeall) local fs = require("luarocks.fs") +local dir = require("luarocks.dir") --- Download sources via Surround SCM Server for building a rock. -- @param rockspec table: The rockspec table @@ -15,7 +16,7 @@ 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 module = rockspec.source.module or dir.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." diff --git a/src/luarocks/fs.lua b/src/luarocks/fs.lua index 7027167a..a905755d 100644 --- a/src/luarocks/fs.lua +++ b/src/luarocks/fs.lua @@ -21,14 +21,12 @@ for _, platform in ipairs(cfg.platforms) do end end +local fs_lua = require("luarocks.fs.lua") 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 + local impl = fs_lua[k] or fs_impl[k] or fs_unix[k] rawset(t, k, impl) return impl end @@ -36,6 +34,7 @@ local fs_mt = { setmetatable(luarocks.fs, fs_mt) +fs_lua.init_fs_functions(luarocks.fs) fs_unix.init_fs_functions(luarocks.fs) if fs_impl then fs_impl.init_fs_functions(luarocks.fs) diff --git a/src/luarocks/fs/lua.lua b/src/luarocks/fs/lua.lua new file mode 100644 index 00000000..00efd4f4 --- /dev/null +++ b/src/luarocks/fs/lua.lua @@ -0,0 +1,565 @@ + +local assert, type, table, io, package, math, os, ipairs = + assert, type, table, io, package, math, os, ipairs + +--- Native Lua implementation of filesystem and platform abstractions, +-- using LuaFileSystem, LZLib, MD5 and LuaCurl. +module("luarocks.fs.lua", package.seeall) + +local cfg = require("luarocks.cfg") +local dir = require("luarocks.dir") + +local zip_ok, zip = pcall(require, "luarocks.tools.zip") +local lfs_ok, lfs = pcall(require, "lfs") +local curl_ok, curl = pcall(require, "luacurl") +local md5_ok, md5 = pcall(require, "md5") + +local tar = require("luarocks.tools.tar") +local patch = require("luarocks.tools.patch") + +local dir_stack = {} + +math.randomseed(os.time()) + +dir_separator = "/" + +local fs_absolute_name, + fs_copy, + fs_current_dir, + fs_dir_stack, + fs_execute, + fs_execute_string, + fs_exists, + fs_find, + fs_is_dir, + fs_is_file, + fs_make_dir, + fs_set_time, + fs_Q + +function init_fs_functions(impl) + fs_absolute_name = impl.absolute_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_exists = impl.exists + fs_find = impl.find + fs_is_dir = impl.is_dir + fs_is_file = impl.is_file + fs_make_dir = impl.make_dir + fs_set_time = impl.set_time + fs_Q = impl.Q +end + +--- 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") + + -- FIXME Unix-specific + return "'" .. arg:gsub("\\", "\\\\"):gsub("'", "'\\''") .. "'" +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(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 + +--- Test is file/dir is writable. +-- Warning: testing if a file/dir is writable does not guarantee +-- that it will remain writable and therefore it is no replacement +-- for checking the result of subsequent operations. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function is_writable(file) + assert(file) + local result + if fs_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 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") .. "/luarocks_" .. name:gsub(dir.separator, "_") .. "-" .. tostring(math.floor(math.random() * 10000)) + if fs_make_dir(temp_dir) then + return temp_dir + else + return nil + 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 + +--- 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 + +--------------------------------------------------------------------- +-- LuaFileSystem functions +--------------------------------------------------------------------- + +if lfs_ok then + +--- Obtain current directory. +-- Uses the module's internal dir stack. +-- @return string: the absolute pathname of the current directory. +function current_dir() + return lfs.currentdir() +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 d string: The directory to switch to. +function change_dir(d) + table.insert(dir_stack, lfs.currentdir()) + lfs.chdir(d) +--local x="CHDIR: " for _,d in ipairs(dir_stack) do x=x.." "..d end print(x) +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(dir_stack, lfs.currentdir()) + -- TODO Does this work on Windows? + lfs.chdir("/") +--local x="CHDIR ROOT: " for _,d in ipairs(dir_stack) do x=x.." "..d end print(x) +end + +--- Change working directory to the previous in the dir stack. +-- @return true if a pop ocurred, false if the stack was empty. +function pop_dir() + local d = table.remove(dir_stack) +--local x="POP DIR: " for _,d in ipairs(dir_stack) do x=x.." "..d end print(x) + if d then + lfs.chdir(d) + return true + else + return false + end +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 directory string: pathname of directory to create. +-- @return boolean: true on success, false on failure. +function make_dir(directory) + assert(type(directory) == "string") + local path = nil + for d in directory:gmatch("[^"..dir.separator.."]*"..dir.separator.."*") do + path = path and path..d or d + local mode = lfs.attributes(path, "mode") + if not mode then + if not lfs.mkdir(path) then + return false + end + elseif mode ~= "directory" then + return false + end + end + return true +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 d string: pathname of directory to remove. +function remove_dir_if_empty(d) + assert(d) + lfs.rmdir(d) +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) + local destmode = lfs.attributes(dest, "mode") + if destmode == "directory" then + dest = dir.path(dest, dir.base_name(src)) + end + local src_h, err = io.open(src, "r") + if not src_h then return nil, err end + local dest_h, err = io.open(dest, "w+") + if not dest_h then src_h:close() return nil, err end + while true do + local block = src_h:read(8192) + if not block then break end + dest_h:write(block) + end + src_h:close() + dest_h:close() + return true +end + +--- Implementation function for recursive copy of directory contents. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure +local function recursive_copy(src, dest) + local srcmode = lfs.attributes(src, "mode") + + if srcmode == "file" then + local ok = fs_copy(src, dest) + if not ok then return false end + elseif srcmode == "directory" then + local subdir = dir.path(dest, dir.base_name(src)) + fs_make_dir(subdir) + for file in lfs.dir(src) do + if file ~= "." and file ~= ".." then + local ok = recursive_copy(dir.path(src, file), subdir) + if not ok then return false end + end + end + end + return true +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) + assert(lfs.attributes(src, "mode") == "directory") + + for file in lfs.dir(src) do + local ok = recursive_copy(dir.path(src, file), dest) + if not ok then + return false, "Failed copying "..src.." to "..dest + end + end + return true +end + +--- Implementation function for recursive removal of directories. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, +-- or nil and an error message on failure. +local function recursive_delete(src) + local srcmode = lfs.attributes(src, "mode") + + if srcmode == "file" then + return os.remove(src) + elseif srcmode == "directory" then + for file in lfs.dir(src) do + if file ~= "." and file ~= ".." then + local ok, err = recursive_delete(dir.path(src, file)) + if not ok then return nil, err end + end + end + local ok, err = lfs.rmdir(src) + if not ok then return nil, err end + end + return true +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 recursive_delete(arg) or false +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 list_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 = {} + for file in lfs.dir(at) do + if file ~= "." and file ~= ".." then + table.insert(result, file) + end + end + return result +end + +--- Implementation function for recursive find. +-- @param cwd string: Current working directory in recursion. +-- @param prefix string: Auxiliary prefix string to form pathname. +-- @param result table: Array of strings where results are collected. +local function recursive_find(cwd, prefix, result) + for file in lfs.dir(cwd) do + if file ~= "." and file ~= ".." then + local item = prefix .. file + table.insert(result, item) + if lfs.attributes(item, "mode") == "directory" then + recursive_find(item, item..dir_separator, result) + end + end + end +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 = {} + recursive_find(lfs.currentdir(), "", result) + return result +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 type(lfs.attributes(file)) == "table" +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 lfs.attributes(file, "mode") == "directory" +end + +--- Test is pathname is a regular file. +-- @param file string: pathname to test +-- @return boolean: true if it is a file, false otherwise. +function is_file(file) + assert(file) + return lfs.attributes(file, "mode") == "file" +end + +function set_time(file, time) + return lfs.touch(file, time) +end + +end + +--------------------------------------------------------------------- +-- LuaZip functions +--------------------------------------------------------------------- + +if zip_ok then + +local function zip(zipfile, ...) + return zip.zip(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) + -- FIXME!!!! + return fs_execute("unzip", zipfile) +end + +end + +--------------------------------------------------------------------- +-- LuaCurl functions +--------------------------------------------------------------------- + +if curl_ok then + +--- 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) + + filename = filename or dir.base_name(url) + + local c = curl.new() + if not c then return false end + local file = io.open(filename, "wb") + if not file then return false end + local ok = c:setopt(curl.OPT_WRITEFUNCTION, function (stream, buffer) + stream:write(buffer) + return string.len(buffer) + end) + ok = ok and c:setopt(curl.OPT_WRITEDATA, file) + ok = ok and c:setopt(curl.OPT_BUFFERSIZE, 5000) + ok = ok and c:setopt(curl.OPT_HTTPHEADER, "Connection: Keep-Alive") + ok = ok and c:setopt(curl.OPT_URL, url) + ok = ok and c:setopt(curl.OPT_CONNECTTIMEOUT, 15) + ok = ok and c:perform() + ok = ok and c:close() + file:close() + return ok +end + +end + +--------------------------------------------------------------------- +-- MD5 functions +--------------------------------------------------------------------- + +if md5_ok then + +--- 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 file = io.open(file, "r") + if not file then return false end + local computed = md5.sumhexa(file:read("*a")) + file:close() + if not computed then + return false + end + if computed:match("^"..md5sum) then + return true + else + return false + end +end + +end + +--- Apply a patch. +-- @param patchname string: The filename of the patch. +function apply_patch(patchname, patchdata) + local p, all_ok = patch.read_patch(patchname, patchdata) + if not all_ok then + return nil, "Failed reading patch "..patchname + end + if p then + return patch.apply_patch(p, 1) + end +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 diff --git a/src/luarocks/fs/unix.lua b/src/luarocks/fs/unix.lua index bf72988f..11378854 100644 --- a/src/luarocks/fs/unix.lua +++ b/src/luarocks/fs/unix.lua @@ -6,43 +6,40 @@ local assert, type, table, io, package, math, os, ipairs = module("luarocks.fs.unix", package.seeall) local cfg = require("luarocks.cfg") +local dir = require("luarocks.dir") 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_is_file, 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_is_file = impl.is_file 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. @@ -63,8 +60,8 @@ function current_dir() current = pipe:read("*l") pipe:close() end - for _, dir in ipairs(fs_dir_stack) do - current = fs_absolute_name(dir, current) + for _, d in ipairs(fs_dir_stack) do + current = fs_absolute_name(d, current) end return current end @@ -103,10 +100,10 @@ end -- 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) +-- @param d string: The directory to switch to. +function change_dir(d) + assert(type(d) == "string") + table.insert(fs_dir_stack, d) end --- Change directory to root. @@ -118,26 +115,27 @@ end --- Change working directory to the previous in the dir stack. function pop_dir() - table.remove(fs_dir_stack) + local d = table.remove(fs_dir_stack) + return d ~= nil 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. +-- @param d 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) +function make_dir(d) + assert(d) + return fs_execute("mkdir -p", d) 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") +function remove_dir_if_empty(d) + assert(d) + fs_execute_string("rmdir "..fs_Q(d).." 1> /dev/null 2> /dev/null") end --- Copy a file. @@ -183,7 +181,7 @@ end -- directory if none is given). -- @return table: an array of strings with the filenames representing -- the contents of a directory. -function dir(at) +function list_dir(at) assert(type(at) == "string" or not at) if not at then at = fs_current_dir() @@ -263,6 +261,14 @@ function is_dir(file) return fs_execute("test -d", file) end +--- Test is pathname is a regular file. +-- @param file string: pathname to test +-- @return boolean: true if it is a regular file, false otherwise. +function is_file(file) + assert(file) + return fs_execute("test -f", file) +end + --- Download a remote file. -- @param url string: URL to be fetched. -- @param filename string or nil: this function attempts to detect the @@ -275,39 +281,17 @@ function download(url, filename) assert(type(filename) == "string" or not filename) if cfg.downloader == "wget" then - if filename 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) + filename = filename or dir.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. @@ -315,7 +299,7 @@ end function make_temp_dir(name) assert(type(name) == "string") - local temp_dir = (os.getenv("TMP") or "/tmp") .. "/" .. name .. "-" .. tostring(math.floor(math.random() * 10000)) + local temp_dir = (os.getenv("TMP") or "/tmp") .. "/luarocks_" .. name:gsub(dir.separator, "_") .. "-" .. tostring(math.floor(math.random() * 10000)) if fs_make_dir(temp_dir) then return temp_dir else @@ -323,9 +307,13 @@ function make_temp_dir(name) end end +function chmod(pathname, mode) + return fs_execute("chmod "..mode, pathname) +end + --- Apply a patch. -- @param patchname string: The filename of the patch. -function patch(patchname) +function apply_patch(patchname) return fs_execute("patch -p1 -f -i ", patchname) end @@ -417,27 +405,6 @@ function absolute_name(pathname, relative_to) 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. @@ -466,7 +433,7 @@ function wrap_script(file, dest) assert(type(file) == "string") assert(type(dest) == "string") - local base = fs_base_name(file) + local base = dir.base_name(file) local wrapname = dest.."/"..base local wrapper = io.open(wrapname, "w") if not wrapper then @@ -476,7 +443,7 @@ function wrap_script(file, dest) 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:write('exec "'..dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)..'" -lluarocks.require "'..file..'" "$@"\n') wrapper:close() if fs_execute("chmod +x",wrapname) then return true diff --git a/src/luarocks/fs/win32.lua b/src/luarocks/fs/win32.lua index 5702a3aa..cafdca7d 100644 --- a/src/luarocks/fs/win32.lua +++ b/src/luarocks/fs/win32.lua @@ -4,24 +4,21 @@ module("luarocks.fs.win32", package.seeall) local cfg = require("luarocks.cfg") +local dir = require("luarocks.dir") -local fs_base_name, - fs_copy, +local 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 @@ -39,9 +36,9 @@ function Q(arg) return '"' .. arg:gsub('"', '\\"') .. '"' end -local function command_at(dir, cmd) - local drive = dir:match("^([A-Za-z]:)") - cmd = "cd " .. fs_Q(dir) .. " & " .. cmd +local function command_at(directory, cmd) + local drive = directory:match("^([A-Za-z]:)") + cmd = "cd " .. fs_Q(directory) .. " & " .. cmd if drive then cmd = drive .. " & " .. cmd end @@ -78,6 +75,14 @@ function is_dir(file) return fs_execute("chdir /D " .. fs_Q(file) .. " 2>NUL 1>NUL") end +--- Test is pathname is a regular file. +-- @param file string: pathname to test +-- @return boolean: true if it is a regular file, false otherwise. +function is_dir(file) + assert(file) + return fs_execute("test -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. @@ -102,21 +107,21 @@ 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. +-- @param d 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") +function make_dir(d) + assert(d) + fs_execute("mkdir "..fs_Q(d).." 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") +-- @param d string: pathname of directory to remove. +function remove_dir_if_empty(d) + assert(d) + fs_execute_string("rmdir "..fs_Q(d).." 1> NUL 2> NUL") end --- Copy a file. @@ -164,7 +169,7 @@ end -- directory if none is given). -- @return table: an array of strings with the filenames representing -- the contents of a directory. -function dir(at) +function list_dir(at) assert(type(at) == "string" or not at) if not at then at = fs_current_dir() @@ -226,16 +231,6 @@ function download(url, 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 last extension of a filename. -- Example: "foo.tar.gz" becomes "foo.tar". -- If filename has no dots, returns it unchanged. @@ -323,7 +318,7 @@ function wrap_script(file, dest) assert(type(file) == "string") assert(type(dest) == "string") - local base = fs_base_name(file) + local base = dir.base_name(file) local wrapname = dest.."/"..base..".bat" local wrapper = io.open(wrapname, "w") if not wrapper then @@ -333,7 +328,7 @@ function wrap_script(file, dest) 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('"'..dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)..'" -lluarocks.require "'..file..'" %*\n') wrapper:write("endlocal\n") wrapper:close() return true @@ -353,7 +348,7 @@ function copy_binary(filename, dest) return nil, err end local exe_pattern = "%.[Ee][Xx][Ee]$" - local base = fs_base_name(filename) + local base = dir.base_name(filename) if base:match(exe_pattern) then base = base:gsub(exe_pattern, ".lua") local helpname = dest.."/"..base diff --git a/src/luarocks/install.lua b/src/luarocks/install.lua index b115a8a8..3ef32c99 100644 --- a/src/luarocks/install.lua +++ b/src/luarocks/install.lua @@ -23,15 +23,17 @@ 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. +-- @return boolean or (nil, string, [string]): True if succeeded or +-- nil and an error message and an optional error code. function install_binary_rock(rock_file) + assert(type(rock_file) == "string") + 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 + return nil, "Incompatible architecture "..arch, "arch" end if rep.is_installed(name, version) then rep.delete_version(name, version) @@ -40,23 +42,23 @@ function install_binary_rock(rock_file) 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 + local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, path.install_dir(name, version)) + if not ok then return nil, err, errcode end ok, err = rep.install_bins(name, version) - local rockspec, err = fetch.load_rockspec(path.rockspec_file(name, version)) + local rockspec, err, errcode = fetch.load_rockspec(path.rockspec_file(name, version)) if err then - return nil, "Failed loading rockspec for installed package: "..err + return nil, "Failed loading rockspec for installed package: "..err, errcode end - ok, err = deps.check_external_deps(rockspec, "install") + ok, err, errcode = deps.check_external_deps(rockspec, "install") if err then - return nil, err + return nil, err, errcode end - ok, err = deps.fulfill_dependencies(rockspec) + ok, err, errcode = deps.fulfill_dependencies(rockspec) if err then - return nil, err + return nil, err, errcode end ok, err = rep.run_hook(rockspec, "post_install") diff --git a/src/luarocks/list.lua b/src/luarocks/list.lua index 1251653c..f918640c 100644 --- a/src/luarocks/list.lua +++ b/src/luarocks/list.lua @@ -6,7 +6,7 @@ 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") +local dir = require("luarocks.dir") help_summary = "Lists currently installed rocks." @@ -24,7 +24,7 @@ function run(...) 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) + search.manifest_search(results, dir.path(tree, "rocks"), query) end print() print("Installed rocks:") diff --git a/src/luarocks/make.lua b/src/luarocks/make.lua index 1c116cdb..1365cf13 100644 --- a/src/luarocks/make.lua +++ b/src/luarocks/make.lua @@ -32,7 +32,7 @@ function run(...) assert(type(rockspec) == "string" or not rockspec) if not rockspec then - local files = fs.dir(fs.current_dir()) + local files = fs.list_dir(fs.current_dir()) for _, file in pairs(files) do if file:match(".rockspec$") then if rockspec then diff --git a/src/luarocks/manif.lua b/src/luarocks/manif.lua index 98772319..3e5790ae 100644 --- a/src/luarocks/manif.lua +++ b/src/luarocks/manif.lua @@ -10,98 +10,35 @@ 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 +local dir = require("luarocks.dir") +local manif_core = require("luarocks.manif_core") --- 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. +-- @return table or (nil, string, [string]): A table representing the manifest, +-- or nil followed by an error message and an optional error code. function load_manifest(repo_url) assert(type(repo_url) == "string") - if manifest_cache[repo_url] then - return manifest_cache[repo_url] + if manif_core.manifest_cache[repo_url] then + return manif_core.manifest_cache[repo_url] end local protocol, pathname = fs.split_url(repo_url) if protocol == "file" then - pathname = fs.make_path(pathname, "manifest") + pathname = dir.path(pathname, "manifest") else - local url = fs.make_path(repo_url, "manifest") + local url = dir.path(repo_url, "manifest") local name = repo_url:gsub("[/:]","_") - local file, dir = fetch.fetch_url_at_temp_dir(url, "luarocks-manifest-"..name) + local file, err, errcode = fetch.fetch_url_at_temp_dir(url, "luarocks-manifest-"..name) if not file then - return nil, "Failed fetching manifest for "..repo_url + return nil, "Failed fetching manifest for "..repo_url, errcode 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) + return manif_core.manifest_loader(pathname, repo_url) end --- Sort function for ordering rock identifiers in a manifest's @@ -183,7 +120,7 @@ local function save_manifest(repo, manifest) assert(type(repo) == "string") assert(type(manifest) == "table") - local filename = fs.make_path(repo, "manifest") + local filename = dir.path(repo, "manifest") return persist.save_from_table(filename, manifest) end @@ -302,7 +239,7 @@ function make_manifest(repo) local results = search.disk_search(repo, query) local manifest = { repository = {}, modules = {}, commands = {} } - manifest_cache[repo] = manifest + manif_core.manifest_cache[repo] = manifest store_results(results, manifest) return save_manifest(repo, manifest) end @@ -388,7 +325,7 @@ function make_index(repo) end local manifest = load_manifest(repo) files = fs.find(repo) - local out = io.open(fs.make_path(repo, "index.html"), "w") + local out = io.open(dir.path(repo, "index.html"), "w") out:write(index_header) for package, version_list in util.sortedpairs(manifest.repository) do local latest_rockspec = nil @@ -414,7 +351,7 @@ function make_index(repo) end output = output .. index_package_end if latest_rockspec then - local rockspec = persist.load_into_table(fs.make_path(repo, latest_rockspec)) + local rockspec = persist.load_into_table(dir.path(repo, latest_rockspec)) local vars = { anchor = package, package = rockspec.package, diff --git a/src/luarocks/manif_core.lua b/src/luarocks/manif_core.lua new file mode 100644 index 00000000..941c569d --- /dev/null +++ b/src/luarocks/manif_core.lua @@ -0,0 +1,73 @@ + +--- Core functions for querying manifest files. +module("luarocks.manif_core", package.seeall) + +local persist = require("luarocks.persist") +local type_check = require("luarocks.type_check") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local cfg = require("luarocks.cfg") + +manifest_cache = {} + +--- 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. +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 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 = dir.path(repo_url, "manifest") + + return manifest_loader(pathname, repo_url, true) +end + +--- 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 diff --git a/src/luarocks/pack.lua b/src/luarocks/pack.lua index 3f64b1c0..7b1c68dd 100644 --- a/src/luarocks/pack.lua +++ b/src/luarocks/pack.lua @@ -9,6 +9,7 @@ local fetch = require("luarocks.fetch") local fs = require("luarocks.fs") local cfg = require("luarocks.cfg") local util = require("luarocks.util") +local dir = require("luarocks.dir") help_summary = "Create a rock, packing sources or binaries." help_arguments = "{| []}" @@ -38,15 +39,15 @@ local function pack_source_rock(rockspec_file) 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) + local source_file, source_dir = fetch.fetch_sources(rockspec, false) if not source_file then - return nil, dir + return nil, source_dir end - fs.change_dir(dir) + fs.change_dir(source_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 + fs.copy(rockspec_file, source_dir) + if not fs.zip(rock_file, dir.base_name(rockspec_file), dir.base_name(source_file)) then return nil, "Failed packing "..rock_file end fs.pop_dir() @@ -87,7 +88,7 @@ local function pack_binary_rock(name, version) rock_file = rock_file:gsub("%."..cfg.arch:gsub("%-","%%-").."%.", ".all.") end fs.delete(rock_file) - if not fs.zip(rock_file, unpack(fs.dir())) then + if not fs.zip(rock_file, unpack(fs.list_dir())) then return nil, "Failed packing "..rock_file end fs.pop_dir() diff --git a/src/luarocks/path.lua b/src/luarocks/path.lua index e236db01..ae9c7fae 100644 --- a/src/luarocks/path.lua +++ b/src/luarocks/path.lua @@ -4,7 +4,7 @@ -- point where the layout of the local installation is defined in LuaRocks. module("luarocks.path", package.seeall) -local fs = require("luarocks.fs") +local dir = require("luarocks.dir") local cfg = require("luarocks.cfg") --- Infer rockspec filename from a rock filename. @@ -12,7 +12,7 @@ local cfg = require("luarocks.cfg") -- @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) + local base_name = dir.base_name(rock_name) return base_name:match("(.*)%.[^.]*.rock") .. ".rockspec" end @@ -25,7 +25,7 @@ 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) + return dir.path(repo or cfg.rocks_dir, name) end --- Get the local installation directory (prefix) for a package. @@ -39,7 +39,7 @@ function install_dir(name, version, repo) assert(type(version) == "string") assert(not repo or type(repo) == "string") - return fs.make_path(repo or cfg.rocks_dir, name, version) + return dir.path(repo or cfg.rocks_dir, name, version) end --- Get the local filename of the rockspec of an installed rock. @@ -53,7 +53,7 @@ function rockspec_file(name, version, repo) 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") + return dir.path(repo or cfg.rocks_dir, name, version, name.."-"..version..".rockspec") end --- Get the local installation directory for C libraries of a package. @@ -67,7 +67,7 @@ function lib_dir(name, version, repo) assert(type(version) == "string") assert(not repo or type(repo) == "string") - return fs.make_path(repo or cfg.rocks_dir, name, version, "lib") + return dir.path(repo or cfg.rocks_dir, name, version, "lib") end --- Get the local installation directory for Lua modules of a package. @@ -81,7 +81,7 @@ function lua_dir(name, version, repo) assert(type(version) == "string") assert(not repo or type(repo) == "string") - return fs.make_path(repo or cfg.rocks_dir, name, version, "lua") + return dir.path(repo or cfg.rocks_dir, name, version, "lua") end --- Get the local installation directory for documentation of a package. @@ -95,7 +95,7 @@ function doc_dir(name, version, repo) assert(type(version) == "string") assert(not repo or type(repo) == "string") - return fs.make_path(repo or cfg.rocks_dir, name, version, "doc") + return dir.path(repo or cfg.rocks_dir, name, version, "doc") end --- Get the local installation directory for configuration files of a package. @@ -109,7 +109,7 @@ function conf_dir(name, version, repo) assert(type(version) == "string") assert(not repo or type(repo) == "string") - return fs.make_path(repo or cfg.rocks_dir, name, version, "conf") + return dir.path(repo or cfg.rocks_dir, name, version, "conf") end --- Get the local installation directory for command-line scripts @@ -124,7 +124,7 @@ function bin_dir(name, version, repo) assert(type(version) == "string") assert(not repo or type(repo) == "string") - return fs.make_path(repo or cfg.rocks_dir, name, version, "bin") + return dir.path(repo or cfg.rocks_dir, name, version, "bin") end --- Extract name, version and arch of a rock filename. @@ -133,7 +133,7 @@ end -- 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$") + return dir.base_name(rock_file):match("(.*)-([^-]+-%d+)%.([^.]+)%.rock$") end --- Extract name and version of a rockspec filename. @@ -142,7 +142,7 @@ end -- 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)") + return dir.base_name(rockspec_file):match("(.*)-([^-]+-%d+)%.(rockspec)") end --- Make a rockspec or rock URL. @@ -159,13 +159,13 @@ function make_url(pathname, name, version, arch) local filename = name.."-"..version if arch == "installed" then - filename = fs.make_path(name, version, filename..".rockspec") + filename = dir.path(name, version, filename..".rockspec") elseif arch == "rockspec" then filename = filename..".rockspec" else filename = filename.."."..arch..".rock" end - return fs.make_path(pathname, filename) + return dir.path(pathname, filename) end --- Convert a pathname to a module identifier. @@ -180,7 +180,7 @@ function path_to_module(file) local name = file:match("(.*)%."..cfg.lua_extension.."$") if name then - name = name:gsub(fs.dir_separator, ".") + name = name:gsub(dir.separator, ".") local init = name:match("(.*)%.init$") if init then name = init @@ -188,7 +188,7 @@ function path_to_module(file) else name = file:match("(.*)%."..cfg.lib_extension.."$") if name then - name = name:gsub(fs.dir_separator, ".") + name = name:gsub(dir.separator, ".") end end return name @@ -200,7 +200,7 @@ end -- @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)) + return (mod:gsub("[^.]*$", ""):gsub("%.", dir.separator)) end --- Set up path-related variables for a given rock. diff --git a/src/luarocks/rep.lua b/src/luarocks/rep.lua index b5797803..304832f1 100644 --- a/src/luarocks/rep.lua +++ b/src/luarocks/rep.lua @@ -6,6 +6,7 @@ local fs = require("luarocks.fs") local path = require("luarocks.path") local cfg = require("luarocks.cfg") local util = require("luarocks.util") +local dir = require("luarocks.dir") --- Get all installed versions of a package. -- @param name string: a package name. @@ -14,7 +15,7 @@ local util = require("luarocks.util") function get_versions(name) assert(type(name) == "string") - local dirs = fs.dir(path.versions_dir(name)) + local dirs = fs.list_dir(path.versions_dir(name)) return (dirs and #dirs > 0) and dirs or nil end @@ -41,7 +42,7 @@ function delete_version(name, version) fs.delete(path.install_dir(name, version)) if not get_versions(name) then - fs.delete(fs.make_path(cfg.rocks_dir, name)) + fs.delete(dir.path(cfg.rocks_dir, name)) end end @@ -50,7 +51,7 @@ end function delete_bin(command) assert(type(command) == "string") - fs.delete(fs.make_path(cfg.scripts_dir, command)) + fs.delete(dir.path(cfg.scripts_dir, command)) end --- Install bin entries in the repository bin dir. @@ -71,9 +72,9 @@ function install_bins(name, version, single_file) if not ok then return nil, "Could not create "..cfg.scripts_dir end - local files = single_file and {single_file} or fs.dir(bindir) + local files = single_file and {single_file} or fs.list_dir(bindir) for _, file in pairs(files) do - local fullname = fs.make_path(bindir, file) + local fullname = dir.path(bindir, file) local match = file:match("%.lua$") local file if not match then @@ -140,7 +141,7 @@ function package_commands(package, version) local bins = fs.find(bindir) for _, file in ipairs(bins) do if file then - result[file] = fs.make_path(bindir, file) + result[file] = dir.path(bindir, file) end end return result @@ -159,7 +160,7 @@ function is_binary_rock(name, version) 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 + if fs.is_actual_binary(dir.path(bin_dir, name)) then return true end end diff --git a/src/luarocks/search.lua b/src/luarocks/search.lua index a3ac68f3..eae3809f 100644 --- a/src/luarocks/search.lua +++ b/src/luarocks/search.lua @@ -4,6 +4,7 @@ module("luarocks.search", package.seeall) local fs = require("luarocks.fs") +local dir = require("luarocks.dir") local path = require("luarocks.path") local manif = require("luarocks.manif") local deps = require("luarocks.deps") @@ -133,14 +134,14 @@ function disk_search(repo, query, results) end query_arch_as_table(query) - for _, name in pairs(fs.dir(repo)) do - local pathname = fs.make_path(repo, name) + for _, name in pairs(fs.list_dir(repo)) do + local pathname = dir.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 + for _, version in pairs(fs.list_dir(pathname)) do if version:match("-%d+$") then store_if_match(results, repo, name, version, "installed", query) end diff --git a/src/luarocks/tools/patch.lua b/src/luarocks/tools/patch.lua new file mode 100644 index 00000000..4fc5c3b5 --- /dev/null +++ b/src/luarocks/tools/patch.lua @@ -0,0 +1,708 @@ +-- patch.lua - Patch utility to apply unified diffs +-- +-- http://lua-users.org/wiki/LuaPatch +-- +-- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license). +-- Code is heavilly based on the Python-based patch.py version 8.06-1 +-- Copyright (c) 2008 rainforce.org, MIT License +-- Project home: http://code.google.com/p/python-patch/ . + +module("luarocks.tools.patch", package.seeall) + +local version = '0.1' + +local io = io +local os = os +local string = string +local table = table +local format = string.format + +-- logging +local debugmode = false +local function debug(s) end +local function info(s) end +local function warning(s) io.stderr:write(s .. '\n') end + +-- Returns boolean whether string s2 starts with string s. +local function startswith(s, s2) + return s:sub(1, #s2) == s2 +end + +-- Returns boolean whether string s2 ends with string s. +local function endswith(s, s2) + return #s >= #s2 and s:sub(#s-#s2+1) == s2 +end + +-- Returns string s after filtering out any new-line characters from end. +local function endlstrip(s) + return s:gsub('[\r\n]+$', '') +end + +-- Returns shallow copy of table t. +local function table_copy(t) + local t2 = {} + for k,v in pairs(t) do t2[k] = v end + return t2 +end + +-- Returns boolean whether array t contains value v. +local function array_contains(t, v) + for _,v2 in ipairs(t) do if v == v2 then return true end end + return false +end + +local function exists(filename) + local fh = io.open(filename) + local result = fh ~= nil + if fh then fh:close() end + return result +end +local function isfile() return true end --FIX? + +local function read_file(filename) + local fh, err, oserr = io.open(filename, 'rb') + if not fh then return fh, err, oserr end + local data, err, oserr = fh:read'*a' + fh:close() + if not data then return nil, err, oserr end + return data +end + +local function write_file(filename, data) + local fh, err, oserr = io.open(filename 'wb') + if not fh then return fh, err, oserr end + local status, err, oserr = fh:write(data) + fh:close() + if not status then return nil, err, oserr end + return true +end + +local function file_copy(src, dest) + local data, err, oserr = read_file(src) + if not data then return data, err, oserr end + local status, err, oserr = write_file(dest) + if not status then return status, err, oserr end + return true +end + +local function string_as_file(s) + return { + at = 0, + str = s, + len = #s, + eof = false, + read = function(self, n) + if self.eof then return nil end + local chunk = self.str:sub(at, at+n) + self.at = self.at + n + if at > self.len then + self.eof = true + end + end, + close = function(self) + self.eof = true + end, + } +end + +-- +-- file_lines(f) is similar to f:lines() for file f. +-- The main difference is that read_lines includes +-- new-line character sequences ("\n", "\r\n", "\r"), +-- if any, at the end of each line. Embedded "\0" are also handled. +-- Caution: The newline behavior can depend on whether f is opened +-- in binary or ASCII mode. +-- (file_lines - version 20080913) +-- +local function file_lines(f) + local CHUNK_SIZE = 1024 + local buffer = "" + local pos_beg = 1 + return function() + local pos, chars + while 1 do + pos, chars = buffer:match('()([\r\n].)', pos_beg) + if pos or not f then + break + elseif f then + local chunk = f:read(CHUNK_SIZE) + if chunk then + buffer = buffer:sub(pos_beg) .. chunk + pos_beg = 1 + else + f = nil + end + end + end + if not pos then + pos = #buffer + elseif chars == '\r\n' then + pos = pos + 1 + end + local line = buffer:sub(pos_beg, pos) + pos_beg = pos + 1 + if #line > 0 then + return line + end + end +end + +local function match_linerange(line) + local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)") + if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end + if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end + if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end + return m1, m2, m3, m4 +end + +function read_patch(filename, data) + -- define possible file regions that will direct the parser flow + local state = 'header' + -- 'header' - comments before the patch body + -- 'filenames' - lines starting with --- and +++ + -- 'hunkhead' - @@ -R +R @@ sequence + -- 'hunkbody' + -- 'hunkskip' - skipping invalid hunk mode + + local all_ok = true + local lineends = {lf=0, crlf=0, cr=0} + local files = {source={}, target={}, hunks={}, fileends={}, hunkends={}} + local nextfileno = 0 + local nexthunkno = 0 --: even if index starts with 0 user messages + -- number hunks from 1 + + -- hunkinfo holds parsed values, hunkactual - calculated + local hunkinfo = { + startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil, + invalid=false, text={} + } + local hunkactual = {linessrc=nil, linestgt=nil} + + info(format("reading patch %s", filename)) + + local fp + if data then + fp = string_as_file(data) + else + fp = filename == '-' and io.stdin or assert(io.open(filename, "rb")) + end + local lineno = 0 + for line in file_lines(fp) do + lineno = lineno + 1 + if state == 'header' then + if startswith(line, "--- ") then + state = 'filenames' + end + -- state is 'header' or 'filenames' + end + if state == 'hunkbody' then + -- skip hunkskip and hunkbody code until definition of hunkhead read + + -- process line first + if line:match"^[- +\\]" or line:match"^[\r\n]*$" then + -- gather stats about line endings + local he = files.hunkends[nextfileno] + if endswith(line, "\r\n") then + he.crlf = he.crlf + 1 + elseif endswith(line, "\n") then + he.lf = he.lf + 1 + elseif endswith(line, "\r") then + he.cr = he.cr + 1 + end + if startswith(line, "-") then + hunkactual.linessrc = hunkactual.linessrc + 1 + elseif startswith(line, "+") then + hunkactual.linestgt = hunkactual.linestgt + 1 + elseif startswith(line, "\\") then + -- nothing + else + hunkactual.linessrc = hunkactual.linessrc + 1 + hunkactual.linestgt = hunkactual.linestgt + 1 + end + table.insert(hunkinfo.text, line) + -- todo: handle \ No newline cases + else + warning(format("invalid hunk no.%d at %d for target file %s", + nexthunkno, lineno, files.target[nextfileno])) + -- add hunk status node + table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) + files.hunks[nextfileno][nexthunkno].invalid = true + all_ok = false + state = 'hunkskip' + end + + -- check exit conditions + if hunkactual.linessrc > hunkinfo.linessrc or + hunkactual.linestgt > hunkinfo.linestgt + then + warning(format("extra hunk no.%d lines at %d for target %s", + nexthunkno, lineno, files.target[nextfileno])) + -- add hunk status node + table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) + files.hunks[nextfileno][nexthunkno].invalid = true + state = 'hunkskip' + elseif hunkinfo.linessrc == hunkactual.linessrc and + hunkinfo.linestgt == hunkactual.linestgt + then + table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) + state = 'hunkskip' + + -- detect mixed window/unix line ends + local ends = files.hunkends[nextfileno] + if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) + + (ends.lf~=0 and 1 or 0) > 1 + then + warning(format("inconsistent line ends in patch hunks for %s", + files.source[nextfileno])) + end + if debugmode then + local debuglines = {crlf=ends.crlf, lf=ends.lf, cr=ends.cr, + file=files.target[nextfileno], hunk=nexthunkno} + debug(format("crlf: %(crlf)d lf: %(lf)d cr: %(cr)d\t " .. + "- file: %(file)s hunk: %(hunk)d", debuglines)) + end + end + -- state is 'hunkbody' or 'hunkskip' + end + + if state == 'hunkskip' then + if match_linerange(line) then + state = 'hunkhead' + elseif startswith(line, "--- ") then + state = 'filenames' + if debugmode and #files.source > 0 then + debug(format("- %2d hunks for %s", #files.hunks[nextfileno], + files.source[nextfileno])) + end + end + -- state is 'hunkskip', 'hunkhead', or 'filenames' + end + local advance + if state == 'filenames' then + if startswith(line, "--- ") then + if array_contains(files.source, nextfileno) then + all_ok = false + warning(format("skipping invalid patch for %s", + files.source[nextfileno+1])) + table.remove(files.source, nextfileno+1) + -- double source filename line is encountered + -- attempt to restart from this second line + end + -- Accept a space as a terminator, like GNU patch does. + -- Breaks patches containing filenames with spaces... + -- FIXME Figure out what does GNU patch do in those cases. + local match = line:match("^--- ([^\t ]+)") + if not match then + all_ok = false + warning(format("skipping invalid filename at line %d", lineno+1)) + state = 'header' + else + table.insert(files.source, match) + end + elseif not startswith(line, "+++ ") then + if array_contains(files.source, nextfileno) then + all_ok = false + warning(format("skipping invalid patch with no target for %s", + files.source[nextfileno+1])) + table.remove(files.source, nextfileno+1) + else + -- this should be unreachable + warning("skipping invalid target patch") + end + state = 'header' + else + if array_contains(files.target, nextfileno) then + all_ok = false + warning(format("skipping invalid patch - double target at line %d", + lineno+1)) + table.remove(files.source, nextfileno+1) + table.remove(files.target, nextfileno+1) + nextfileno = nextfileno - 1 + -- double target filename line is encountered + -- switch back to header state + state = 'header' + else + -- Accept a space as a terminator, like GNU patch does. + -- Breaks patches containing filenames with spaces... + -- FIXME Figure out what does GNU patch do in those cases. + local re_filename = "^%+%+%+ ([^ \t]+)" + local match = line:match(re_filename) + if not match then + all_ok = false + warning(format( + "skipping invalid patch - no target filename at line %d", + lineno+1)) + state = 'header' + else + table.insert(files.target, match) + nextfileno = nextfileno + 1 + nexthunkno = 0 + table.insert(files.hunks, {}) + table.insert(files.hunkends, table_copy(lineends)) + table.insert(files.fileends, table_copy(lineends)) + state = 'hunkhead' + advance = true + end + end + end + -- state is 'filenames', 'header', or ('hunkhead' with advance) + end + if not advance and state == 'hunkhead' then + local m1, m2, m3, m4 = match_linerange(line) + if not m1 then + if not array_contains(files.hunks, nextfileno-1) then + all_ok = false + warning(format("skipping invalid patch with no hunks for file %s", + files.target[nextfileno])) + end + state = 'header' + else + hunkinfo.startsrc = tonumber(m1) + hunkinfo.linessrc = tonumber(m2 or 1) + hunkinfo.starttgt = tonumber(m3) + hunkinfo.linestgt = tonumber(m4 or 1) + hunkinfo.invalid = false + hunkinfo.text = {} + + hunkactual.linessrc = 0 + hunkactual.linestgt = 0 + + state = 'hunkbody' + nexthunkno = nexthunkno + 1 + end + -- state is 'header' or 'hunkbody' + end + end + if state ~= 'hunkskip' then + warning(format("patch file incomplete - %s", filename)) + all_ok = false + -- os.exit(?) + else + -- duplicated message when an eof is reached + if debugmode and #files.source > 0 then + debug(format("- %2d hunks for %s", #files.hunks[nextfileno], + files.source[nextfileno])) + end + end + + local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end + info(format("total files: %d total hunks: %d", #files.source, sum)) + fp:close() + return files, all_ok +end + +local function find_hunk(file, h, hno) + for fuzz=0,2 do + local lineno = h.startsrc + for i=0,#file do + local found = true + local location = lineno + local total = #h.text - fuzz + for l, hline in ipairs(h.text) do + if l > fuzz then + -- todo: \ No newline at the end of file + if startswith(hline, " ") or startswith(hline, "-") then + local line = file[lineno] + lineno = lineno + 1 + if not line or #line == 0 then + found = false + break + end + if endlstrip(line) ~= endlstrip(hline:sub(2)) then + found = false + break + end + end + end + end + if found then + local offset = location - h.startsrc - fuzz + if offset ~= 0 then + warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz))) + end + h.startsrc = location + h.starttgt = h.starttgt + offset + for i=1,fuzz do + table.remove(h.text, 1) + table.remove(h.text, #h.text) + end + return true + end + lineno = i + end + end + return false +end + +local function load_file(filename) + local fp = assert(io.open(filename)) + local file = {} + local readline = file_lines(fp) + while true do + local line = readline() + if not line then break end + table.insert(file, line) + end + fp:close() + return file +end + +local function find_hunks(file, hunks) + local matched = true + local lineno = 1 + local hno = nil + for hno, h in ipairs(hunks) do + find_hunk(file, h, hno) + end +end + +local function check_patched(file, hunks) + local matched = true + local lineno = 1 + local hno = nil + local ok, err = pcall(function() + if #file == 0 then + error 'nomatch' + end + for hno, h in ipairs(hunks) do + -- skip to line just before hunk starts + if #file < h.starttgt then + error 'nomatch' + end + lineno = h.starttgt +print() for k,v in pairs(h) do print(k,v) end print() + for _, hline in ipairs(h.text) do + -- todo: \ No newline at the end of file + if not startswith(hline, "-") and not startswith(hline, "\\") then + local line = file[lineno] + lineno = lineno + 1 + if #line == 0 then + error 'nomatch' + end + if endlstrip(line) ~= endlstrip(hline:sub(2)) then + warning(format("file is not patched - failed hunk: %d", hno)) + error 'nomatch' + end + end + end + end + end) + if err == 'nomatch' then + matched = false + end + -- todo: display failed hunk, i.e. expected/found + + return matched +end + +local function patch_hunks(srcname, tgtname, hunks) + local src = assert(io.open(srcname, "rb")) + local tgt = assert(io.open(tgtname, "wb")) + + local src_readline = file_lines(src) + + -- todo: detect linefeeds early - in apply_files routine + -- to handle cases when patch starts right from the first + -- line and no lines are processed. At the moment substituted + -- lineends may not be the same at the start and at the end + -- of patching. Also issue a warning about mixed lineends + + local srclineno = 1 + local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0} + for hno, h in ipairs(hunks) do + debug(format("processing hunk %d for file %s", hno, tgtname)) + -- skip to line just before hunk starts + while srclineno < h.startsrc do + local line = src_readline() + -- Python 'U' mode works only with text files + if endswith(line, "\r\n") then + lineends["\r\n"] = lineends["\r\n"] + 1 + elseif endswith(line, "\n") then + lineends["\n"] = lineends["\n"] + 1 + elseif endswith(line, "\r") then + lineends["\r"] = lineends["\r"] + 1 + end + tgt:write(line) + srclineno = srclineno + 1 + end + + for _,hline in ipairs(h.text) do + -- todo: check \ No newline at the end of file + if startswith(hline, "-") or startswith(hline, "\\") then + src_readline() + srclineno = srclineno + 1 + else + if not startswith(hline, "+") then + src_readline() + srclineno = srclineno + 1 + end + local line2write = hline:sub(2) + -- detect if line ends are consistent in source file + local sum = 0 + for k,v in pairs(lineends) do if v > 0 then sum=sum+1 end end + if sum == 1 then + local newline + for k,v in pairs(lineends) do if v ~= 0 then newline = k end end + tgt:write(endlstrip(line2write) .. newline) + else -- newlines are mixed or unknown + tgt:write(line2write) + end + end + end + end + for line in src_readline do + tgt:write(line) + end + tgt:close() + src:close() + return true +end + +local function strip_dirs(filename, strip) + if strip == nil then return filename end + for i=1,strip do + filename=filename:gsub("^[^/]*/", "") + end + return filename +end + +function apply_patch(patch, strip) + local all_ok = true + local total = #patch.source + for fileno, filename in ipairs(patch.source) do + filename = strip_dirs(filename, strip) + local continue + local f2patch = filename + if not exists(f2patch) then + f2patch = strip_dirs(patch.target[fileno], strip) + if not exists(f2patch) then --FIX:if f2patch nil + warning(format("source/target file does not exist\n--- %s\n+++ %s", + filename, f2patch)) + all_ok = false + continue = true + end + end + if not continue and not isfile(f2patch) then + warning(format("not a file - %s", f2patch)) + all_ok = false + continue = true + end + if not continue then + + filename = f2patch + + info(format("processing %d/%d:\t %s", fileno, total, filename)) + + -- validate before patching + local hunks = patch.hunks[fileno] + local file = load_file(filename) + local hunkno = 1 + local hunk = hunks[hunkno] + local hunkfind = {} + local hunkreplace = {} + local validhunks = 0 + local canpatch = false + local hunklineno + local isbreak + local lineno = 0 + + find_hunks(file, hunks) + + for _, line in ipairs(file) do + lineno = lineno + 1 + local continue + if not hunk or lineno < hunk.startsrc then + continue = true + elseif lineno == hunk.startsrc then + hunkfind = {} + for _,x in ipairs(hunk.text) do + if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then + hunkfind[#hunkfind+1] = endlstrip(x:sub(2)) + end end + hunkreplace = {} + for _,x in ipairs(hunk.text) do + if x:sub(1,1) == ' ' or x:sub(1,1) == '+' then + hunkreplace[#hunkreplace+1] = endlstrip(x:sub(2)) + end end + --pprint(hunkreplace) + hunklineno = 1 + + -- todo \ No newline at end of file + end + -- check hunks in source file + if not continue and lineno < hunk.startsrc + #hunkfind - 1 then + if endlstrip(line) == hunkfind[hunklineno] then + hunklineno = hunklineno + 1 + else + debug(format("hunk no.%d doesn't match source file %s", + hunkno, filename)) + -- file may be already patched, but check other hunks anyway + hunkno = hunkno + 1 + if hunkno <= #hunks then + hunk = hunks[hunkno] + continue = true + else + isbreak = true; break + end + end + end + -- check if processed line is the last line + if not continue and lineno == hunk.startsrc + #hunkfind - 1 then + debug(format("file %s hunk no.%d -- is ready to be patched", + filename, hunkno)) + hunkno = hunkno + 1 + validhunks = validhunks + 1 + if hunkno <= #hunks then + hunk = hunks[hunkno] + else + if validhunks == #hunks then + -- patch file + canpatch = true + isbreak = true; break + end + end + end + end + if not isbreak then + if hunkno <= #hunks then + warning(format("premature end of source file %s at hunk %d", + filename, hunkno)) + all_ok = false + end + end + if validhunks < #hunks then + if check_patched(file, hunks) then + warning(format("already patched %s", filename)) + else + warning(format("source file is different - %s", filename)) + all_ok = false + end + end + if canpatch then + local backupname = filename .. ".orig" + if exists(backupname) then + warning(format("can't backup original file to %s - aborting", + backupname)) + all_ok = false + else + assert(os.rename(filename, backupname)) + if patch_hunks(backupname, filename, hunks) then + warning(format("successfully patched %s", filename)) + assert(os.remove(backupname)) + else + warning(format("error patching file %s", filename)) + assert(file_copy(filename, filename .. ".invalid")) + warning(format("invalid version is saved to %s", + filename .. ".invalid")) + -- todo: proper rejects + assert(os.rename(backupname, filename)) + all_ok = false + end + end + end + + end -- if not continue + end -- for + -- todo: check for premature eof + return all_ok +end diff --git a/src/luarocks/tools/tar.lua b/src/luarocks/tools/tar.lua new file mode 100644 index 00000000..0a3e07a4 --- /dev/null +++ b/src/luarocks/tools/tar.lua @@ -0,0 +1,143 @@ + +module("luarocks.tools.tar", package.seeall) + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") + +local blocksize = 512 + +local function get_typeflag(flag) + if flag == "0" or flag == "\0" then return "file" + elseif flag == "1" then return "link" + elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU + elseif flag == "3" then return "character" + elseif flag == "4" then return "block" + elseif flag == "5" then return "directory" + elseif flag == "6" then return "fifo" + elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU + elseif flag == "x" then return "next file" + elseif flag == "g" then return "global extended header" + elseif flag == "L" then return "long name" + elseif flag == "K" then return "long link name" + end + return "unknown" +end + +local function octal_to_number(octal) + local exp = 0 + local number = 0 + for i = #octal,1,-1 do + local digit = tonumber(octal:sub(i,i)) + if not digit then break end + number = number + (digit * 8^exp) + exp = exp + 1 + end + return number +end + +local function checksum_header(block) + local sum = 256 + for i = 1,148 do + sum = sum + block:byte(i) + end + for i = 157,500 do + sum = sum + block:byte(i) + end + return sum +end + +local function nullterm(s) + return s:match("^[^%z]*") +end + +local function read_header_block(block) + local header = {} + header.name = nullterm(block:sub(1,100)) + header.mode = nullterm(block:sub(101,108)) + header.uid = octal_to_number(nullterm(block:sub(109,116))) + header.gid = octal_to_number(nullterm(block:sub(117,124))) + header.size = octal_to_number(nullterm(block:sub(125,136))) + header.mtime = octal_to_number(nullterm(block:sub(137,148))) + header.chksum = octal_to_number(nullterm(block:sub(149,156))) + header.typeflag = get_typeflag(block:sub(157,157)) + header.linkname = nullterm(block:sub(158,257)) + header.magic = block:sub(258,263) + header.version = block:sub(264,265) + header.uname = nullterm(block:sub(266,297)) + header.gname = nullterm(block:sub(298,329)) + header.devmajor = octal_to_number(nullterm(block:sub(330,337))) + header.devminor = octal_to_number(nullterm(block:sub(338,345))) + header.prefix = block:sub(346,500) + if header.magic ~= "ustar " and header.magic ~= "ustar\0" then + return false, "Invalid header magic "..header.magic + end + if header.version ~= "00" and header.version ~= " \0" then + return false, "Unknown version "..header.version + end + if not checksum_header(block) == header.chksum then + return false, "Failed header checksum" + end + return header +end + +function untar(filename, destdir) + assert(type(filename) == "string") + assert(type(destdir) == "string") + + local tar_handle = io.open(filename, "r") + if not tar_handle then return nil, "Error opening file "..filename end + + local long_name, long_link_name + while true do + local block + repeat + block = tar_handle:read(blocksize) + until (not block) or checksum_header(block) > 256 + if not block then break end + local header, err = read_header_block(block) + if not header then + print(err) + end + + local file_data = tar_handle:read(math.ceil(header.size / blocksize) * blocksize):sub(1,header.size) + + if header.typeflag == "long name" then + long_name = nullterm(file_data) + elseif header.typeflag == "long link name" then + long_link_name = nullterm(file_data) + else + if long_name then + header.name = long_name + long_name = nil + end + if long_link_name then + header.name = long_link_name + long_link_name = nil + end + end + local pathname = dir.path(destdir, header.name) + if header.typeflag == "directory" then + fs.make_dir(pathname) + elseif header.typeflag == "file" then + local dirname = dir.dir_name(pathname) + if dirname ~= "" then + fs.make_dir(dirname) + end + local file_handle = io.open(pathname, "wb") + file_handle:write(file_data) + file_handle:close() + fs.set_time(pathname, header.mtime) + if fs.chmod then + fs.chmod(pathname, header.mode) + end + end + print(pathname) + --[[ + for k,v in pairs(header) do + print("[\""..tostring(k).."\"] = "..(type(v)=="number" and v or "\""..v:gsub("%z", "\\0").."\"")) + end + print() + --]] + end + return true +end diff --git a/src/luarocks/tools/zip.lua b/src/luarocks/tools/zip.lua new file mode 100644 index 00000000..621beae1 --- /dev/null +++ b/src/luarocks/tools/zip.lua @@ -0,0 +1,230 @@ + +module("luarocks.tools.zip", package.seeall) + +local zlib = require "zlib" + +local function number_to_bytestring(number, nbytes) + local out = {} + for i = 1, nbytes do + local byte = number % 256 + table.insert(out, string.char(byte)) + number = (number - byte) / 256 + end + return table.concat(out) +end + +--- Return a zip handle open for writing. +-- @param name filename of the zipfile to be created. +-- @return a zip handle, or nil in case of error. +function write_open(name) + + local zf = {} + + zf.ziphandle = io.open(name, "w") + if not zf.ziphandle then + return nil + end + zf.files = {} + zf.in_open_file = false + + return zf +end + +--- Begin a new file to be stored inside the zipfile. +-- @param zf handle of the zipfile being written. +-- @param filename filenome of the file to be added to the zipfile. +-- @return true if succeeded, nil in case of failure. +function write_open_new_file_in_zip(zf, filename) + if zf.in_open_file then + close_file_in_zip(zf) + return nil + end + local lfh = {} + zf.local_file_header = lfh + lfh.last_mod_file_time = 0 -- TODO + lfh.last_mod_file_date = 0 -- TODO + lfh.crc32 = 0 -- initial value + lfh.compressed_size = 0 -- unknown yet + lfh.uncompressed_size = 0 -- unknown yet + lfh.file_name_length = #filename + lfh.extra_field_length = 0 + lfh.file_name = filename:gsub("\\", "/") + zf.in_open_file = true + zf.data = {} + return true +end + +--- Write data to the file currently being stored in the zipfile. +-- @param zf handle of the zipfile being written. +-- @param buf string containing data to be written. +-- @return true if succeeded, nil in case of failure. +function write_in_file_in_zip(zf, buf) + if not zf.in_open_file then + return nil + end + local lfh = zf.local_file_header + local cbuf = zlib.compress(buf):sub(3, -5) + lfh.crc32 = zlib.crc32(lfh.crc32, buf) + lfh.compressed_size = lfh.compressed_size + #cbuf + lfh.uncompressed_size = lfh.uncompressed_size + #buf + table.insert(zf.data, cbuf) + return true +end + +--- Complete the writing of a file stored in the zipfile. +-- @param zf handle of the zipfile being written. +-- @return true if succeeded, nil in case of failure. +function write_close_file_in_zip(zf) + local zh = zf.ziphandle + + if not zf.in_open_file then + return nil + end + + -- Local file header + local lfh = zf.local_file_header + lfh.offset = zh:seek() + zh:write(number_to_bytestring(0x04034b50, 4)) -- signature + zh:write(number_to_bytestring(20, 2)) -- version needed to extract: 2.0 + zh:write(number_to_bytestring(0, 2)) -- general purpose bit flag + zh:write(number_to_bytestring(8, 2)) -- compression method: deflate + zh:write(number_to_bytestring(lfh.last_mod_file_time, 2)) + zh:write(number_to_bytestring(lfh.last_mod_file_date, 2)) + zh:write(number_to_bytestring(lfh.crc32, 4)) + zh:write(number_to_bytestring(lfh.compressed_size, 4)) + zh:write(number_to_bytestring(lfh.uncompressed_size, 4)) + zh:write(number_to_bytestring(lfh.file_name_length, 2)) + zh:write(number_to_bytestring(lfh.extra_field_length, 2)) + zh:write(lfh.file_name) + + -- File data + for _, cbuf in ipairs(zf.data) do + zh:write(cbuf) + end + + -- Data descriptor + zh:write(number_to_bytestring(lfh.crc32, 4)) + zh:write(number_to_bytestring(lfh.compressed_size, 4)) + zh:write(number_to_bytestring(lfh.uncompressed_size, 4)) + + table.insert(zf.files, lfh) + zf.in_open_file = false + + return true +end + +--- Complete the writing of the zipfile. +-- @param zf handle of the zipfile being written. +-- @return true if succeeded, nil in case of failure. +function write_close(zf) + local zh = zf.ziphandle + + local central_directory_offset = zh:seek() + + local size_of_central_directory = 0 + -- Central directory structure + for _, lfh in ipairs(zf.files) do + zh:write(number_to_bytestring(0x02014b50, 4)) -- signature + zh:write(number_to_bytestring(3, 2)) -- version made by: UNIX + zh:write(number_to_bytestring(20, 2)) -- version needed to extract: 2.0 + zh:write(number_to_bytestring(0, 2)) -- general purpose bit flag + zh:write(number_to_bytestring(8, 2)) -- compression method: deflate + zh:write(number_to_bytestring(lfh.last_mod_file_time, 2)) + zh:write(number_to_bytestring(lfh.last_mod_file_date, 2)) + zh:write(number_to_bytestring(lfh.crc32, 4)) + zh:write(number_to_bytestring(lfh.compressed_size, 4)) + zh:write(number_to_bytestring(lfh.uncompressed_size, 4)) + zh:write(number_to_bytestring(lfh.file_name_length, 2)) + zh:write(number_to_bytestring(lfh.extra_field_length, 2)) + zh:write(number_to_bytestring(0, 2)) -- file comment length + zh:write(number_to_bytestring(0, 2)) -- disk number start + zh:write(number_to_bytestring(0, 2)) -- internal file attributes + zh:write(number_to_bytestring(0, 4)) -- external file attributes + zh:write(number_to_bytestring(lfh.offset, 4)) -- relative offset of local header + zh:write(lfh.file_name) + size_of_central_directory = size_of_central_directory + 46 + lfh.file_name_length + end + + -- End of central directory record + zh:write(number_to_bytestring(0x06054b50, 4)) -- signature + zh:write(number_to_bytestring(0, 2)) -- number of this disk + zh:write(number_to_bytestring(0, 2)) -- number of disk with start of central directory + zh:write(number_to_bytestring(#zf.files, 2)) -- total number of entries in the central dir on this disk + zh:write(number_to_bytestring(#zf.files, 2)) -- total number of entries in the central dir + zh:write(number_to_bytestring(size_of_central_directory, 4)) + zh:write(number_to_bytestring(central_directory_offset, 4)) + zh:write(number_to_bytestring(0, 2)) -- zip file comment length + zh:close() + + return true +end + +-- @return boolean or (boolean, string): true on success, +-- false and an error message on failure. +local function add_to_zip(zf, file) + local fin + local ok, err = write_open_new_file_in_zip(zf, file) + if not ok then + err = "error in opening "..file.." in zipfile" + else + fin = io.open(file, "rb") + if not fin then + ok = false + err = "error opening "..file.." for reading" + end + end + while ok do + local buf = fin:read(size_buf) + if not buf then + break + end + ok = write_in_file_in_zip(zf, buf) + if not ok then + err = "error in writing "..file.." in the zipfile" + end + end + if fin then + fin:close() + end + if ok then + ok = write_close_file_in_zip(zf) + if not ok then + err = "error in writing "..file.." in the zipfile" + end + end + return ok == true, err +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 or (boolean, string): true on success, +-- false and an error message on failure. +function zip(zipfile, ...) + local zf = write_open(filename) + if not zf then + return nil, "error opening "..filename + end + + local ok, err + for _, file in pairs({...}) do + if fs_is_dir(file) then + for _, file in pairs(fs_find(file)) do + if fs_is_file(file) then + ok, err = add_to_zip(file) + if not ok then break end + end + end + else + ok, err = add_to_zip(file) + if not ok then break end + end + end + + local ok = write_close(zf) + if not ok then + return false, "error closing "..filename + end + return ok, err +end diff --git a/src/luarocks/type_check.lua b/src/luarocks/type_check.lua index ec9d6c5c..0e4a73af 100644 --- a/src/luarocks/type_check.lua +++ b/src/luarocks/type_check.lua @@ -40,6 +40,7 @@ rockspec_types = { dir = "string", tag = "string", branch = "string", + module = "string", cvs_tag = "string", cvs_module = "string" }, @@ -187,7 +188,6 @@ end 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 diff --git a/src/luarocks/unpack.lua b/src/luarocks/unpack.lua index 60bc1295..c73264d0 100644 --- a/src/luarocks/unpack.lua +++ b/src/luarocks/unpack.lua @@ -7,6 +7,7 @@ local fetch = require("luarocks.fetch") local fs = require("luarocks.fs") local util = require("luarocks.util") local build = require("luarocks.build") +local dir = require("luarocks.dir") help_summary = "Unpack the contents of a rock." help_arguments = "{| []}" @@ -50,9 +51,9 @@ 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) + local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, dir_name) if not ok then - return nil, "Failed unzipping rock "..rock_file + return nil, "Failed unzipping rock "..rock_file, errcode end fs.change_dir(dir_name) local rockspec_file = dir_name..".rockspec" @@ -83,7 +84,7 @@ end local function run_unpacker(file) assert(type(file) == "string") - local base_name = fs.base_name(file) + local base_name = dir.base_name(file) local dir_name, kind, extension = base_name:match("(.*)%.([^.]+)%.(rock)$") if not extension then dir_name, extension = base_name:match("(.*)%.(rockspec)$") @@ -117,7 +118,7 @@ local function run_unpacker(file) end print() print("Done. You may now enter directory ") - print(fs.make_path(dir_name, rockspec.source.dir)) + print(dir.path(dir_name, rockspec.source.dir)) print("and type 'luarocks make' to build.") end util.remove_scheduled_function(rollback) diff --git a/src/luarocks/validate.lua b/src/luarocks/validate.lua new file mode 100644 index 00000000..1bf001c7 --- /dev/null +++ b/src/luarocks/validate.lua @@ -0,0 +1,159 @@ + +module("luarocks.validate", package.seeall) + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local cfg = require("luarocks.cfg") +local build = require("luarocks.build") +local install = require("luarocks.install") +local util = require("luarocks.util") + +help_summary = "Sandboxed test of build/install of all packages in a repository." + +help = [[ +, if given, is a local repository pathname. +]] + +local function save_settings(repo) + local protocol, path = fs.split_url(repo) + table.insert(cfg.rocks_servers, 1, protocol.."://"..path) + return { + root_dir = cfg.root_dir, + rocks_dir = cfg.rocks_dir, + scripts_dir = cfg.scripts_dir, + } +end + +local function restore_settings(settings) + cfg.root_dir = settings.root_dir + cfg.rocks_dir = settings.rocks_dir + cfg.scripts_dir = settings.scripts_dir + cfg.variables.ROCKS_TREE = settings.root_dir + cfg.variables.SCRIPTS_DIR = settings.scripts_dir + table.remove(cfg.rocks_servers, 1) +end + +local function prepare_sandbox(file) + local root_dir = fs.make_temp_dir(file):gsub("/+$", "") + cfg.root_dir = root_dir + cfg.rocks_dir = root_dir.."/rocks" + cfg.scripts_dir = root_dir.."/bin" + cfg.variables.ROCKS_TREE = cfg.root_dir + cfg.variables.SCRIPTS_DIR = cfg.scripts_dir + return root_dir +end + +local function validate_rockspec(file) + local ok, err, errcode = build.build_rockspec(file, true) + if not ok then + print(err) + end + return ok, err, errcode +end + +local function validate_src_rock(file) + local ok, err, errcode = build.build_rock(file, false) + if not ok then + print(err) + end + return ok, err, errcode +end + +local function validate_rock(file) + local ok, err, errcode = install.install_binary_rock(file) + if not ok then + print(err) + end + return ok, err, errcode +end + +local function validate(repo, flags) + local results = { + ok = {} + } + local settings = save_settings(repo) + local sandbox + if flags["quick"] then + sandbox = prepare_sandbox("luarocks_validate") + end + if not fs.exists(repo) then + return nil, repo.." is not a local repository." + end + for _, file in pairs(fs.list_dir(repo)) do for _=1,1 do + if file == "manifest" or file == "index.html" then + break -- continue for + end + local pathname = fs.absolute_name(dir.path(repo, file)) + if not flags["quick"] then + sandbox = prepare_sandbox(file) + end + local ok, err, errcode + print() + print("Verifying "..pathname) + if file:match("%.rockspec$") then + ok, err, errcode = validate_rockspec(pathname) + elseif file:match("%.src%.rock$") then + ok, err, errcode = validate_src_rock(pathname) + elseif file:match("%.rock$") then + ok, err, errcode = validate_rock(pathname) + end + if ok then + table.insert(results.ok, {file=file} ) + else + if not errcode then + errcode = "misc" + end + if not results[errcode] then + results[errcode] = {} + end + table.insert(results[errcode], {file=file, err=err} ) + end + util.run_scheduled_functions() + if not flags["quick"] then + fs.delete(sandbox) + end + repeat until not fs.pop_dir() + end end + if flags["quick"] then + fs.delete(sandbox) + end + restore_settings(settings) + print() + print("Results:") + print("--------") + print("OK: "..tostring(#results.ok)) + for _, entry in ipairs(results.ok) do + print(entry.file) + end + for errcode, errors in pairs(results) do + if errcode ~= "ok" then + print() + print(errcode.." errors: "..tostring(#errors)) + for _, entry in ipairs(errors) do + print(entry.file, entry.err) + end + end + end + + print() + print("Summary:") + print("--------") + local total = 0 + for errcode, errors in pairs(results) do + print(errcode..": "..tostring(#errors)) + total = total + #errors + end + print("Total: "..total) + return true +end + +function run(...) + local flags, repo = util.parse_flags(...) + assert(type(repo) == "string" or not repo) + repo = repo or cfg.rocks_dir + + print("Verifying contents of "..repo) + + return validate(repo, flags) +end + -- cgit v1.2.3-55-g6feb