Compatible Android

This commit is contained in:
Andros Fenollosa
2016-11-03 00:05:36 +01:00
parent 7cb6af1390
commit 8ec8327e5e
1793 changed files with 440698 additions and 7 deletions

View File

@ -0,0 +1,112 @@
-- Luadist configuration
module ("dist.config", package.seeall)
local sys = require "dist.sys"
local utils = require "dist.utils"
local win = (os.getenv('WINDIR') or (os.getenv('OS') or ''):match('[Ww]indows'))
and not (os.getenv('OSTYPE') or ''):match('cygwin') -- exclude cygwin
-- System information ------------------------------------------------
version = "0.2.7" -- Current LuaDist version
-- set initial architecture as it's important for path separators
arch = win and "Windows" or "Linux" -- Host architecture
type = "x86" -- Host type
-- Directories -------------------------------------------------------
root_dir = os.getenv("DIST_ROOT") or utils.get_luadist_location() or sys.path_separator()
temp_dir = "tmp"
cache_dir = sys.make_path(temp_dir, "cache")
distinfos_dir = sys.make_path("share", "luadist-git", "dists")
test_dir = sys.make_path("share", "luadist-git", "test")
-- Files -------------------------------------------------------------
manifest_file = sys.make_path(cache_dir, ".gitmodules")
dep_cache_file = sys.make_path(cache_dir, ".depcache")
log_file = sys.make_path(temp_dir, "luadist.log")
cache_file = ""
-- Repositories ------------------------------------------------------
repos = {
"git://github.com/LuaDist/Repository.git",
}
upload_url = "git@github.com:LuaDist" -- must not contain trailing '/'
-- Settings ----------------------------------------------------------
debug = false -- Use debug mode.
verbose = false -- Print verbose output.
simulate = false -- Only simulate installation of packages.
binary = true -- Use binary version of modules.
source = true -- Use source version of modules.
test = false -- Run CTest before install.
cache = true -- Use cache.
cache_timeout = 3 * 60 * 60 -- Cache timeout in seconds.
dep_cache = true -- Use cache for dependency information (tree functionality).
-- Components (of modules) that will be installed.
components = {
"Runtime", "Library", "Header", "Data", "Documentation", "Example", "Test", "Other", "Unspecified"
}
-- Available log levels are: DEBUG, INFO, WARN, ERROR, FATAL (see dist.logger for more information).
print_log_level = "WARN" -- Minimum level for log messages to be printed (nil to disable).
write_log_level = "INFO" -- Minimum level for log messages to be logged (nil to disable).
-- CMake variables ---------------------------------------------------
variables = {
--- Install defaults
INSTALL_BIN = "bin",
INSTALL_LIB = "lib",
INSTALL_INC = "include",
INSTALL_ETC = "etc",
INSTALL_LMOD = "lib/lua",
INSTALL_CMOD = "lib/lua",
--- LuaDist specific variables
DIST_VERSION = version,
DIST_ARCH = arch,
DIST_TYPE = type,
-- CMake specific setup
CMAKE_GENERATOR = win and "MinGW Makefiles" or "Unix Makefiles",
CMAKE_BUILD_TYPE = "MinSizeRel",
-- RPath functionality
CMAKE_SKIP_BUILD_RPATH = "FALSE",
CMAKE_BUILD_WITH_INSTALL_RPATH = "FALSE",
CMAKE_INSTALL_RPATH = "$ORIGIN/../lib",
CMAKE_INSTALL_RPATH_USE_LINK_PATH = "TRUE",
CMAKE_INSTALL_NAME_DIR = "@executable_path/../lib",
-- OSX specific
CMAKE_OSX_ARCHITECTURES = "",
}
-- Building ----------------------------------------------------------
cmake = "cmake"
ctest = "ctest"
cache_command = cmake .. " -C cache.cmake"
build_command = cmake .. " --build . --clean-first"
install_component_command = " -DCOMPONENT=#COMPONENT# -P cmake_install.cmake"
test_command = ctest .. " -V ."
strip_option = " -DCMAKE_INSTALL_DO_STRIP=true"
cache_debug_options = "-DCMAKE_VERBOSE_MAKEFILE=true -DCMAKE_BUILD_TYPE=Debug"
build_debug_options = ""
-- Add -j option to make in case of unix makefiles to speed up builds
if (variables.CMAKE_GENERATOR == "Unix Makefiles") then
build_command = build_command .. " -- -j6"
end
-- Add -j option to make in case of MinGW makefiles to speed up builds
if (variables.CMAKE_GENERATOR == "MinGW Makefiles") then
build_command = "set SHELL=cmd.exe && " .. build_command .. " -- -j"
end

View File

@ -0,0 +1,271 @@
-- Note: the code of this module is borrowed from the original LuaDist project
--- LuaDist version constraints functions
-- Peter Drahoš, LuaDist Project, 2010
-- Original Code borrowed from LuaRocks Project
--- Version constraints handling functions.
-- Dependencies are represented in LuaDist through strings with
-- a dist name followed by a comma-separated list of constraints.
-- Each constraint consists of an operator and a version number.
-- In this string format, version numbers are represented as
-- naturally as possible, like they are used by upstream projects
-- (e.g. "2.0beta3"). Internally, LuaDist converts them to a purely
-- numeric representation, allowing comparison following some
-- "common sense" heuristics. The precise specification of the
-- comparison criteria is the source code of this module, but the
-- test/test_deps.lua file included with LuaDist provides some
-- insights on what these criteria are.
module ("dist.constraints", package.seeall)
local operators = {
["=="] = "==",
["~="] = "~=",
[">"] = ">",
["<"] = "<",
[">="] = ">=",
["<="] = "<=",
["~>"] = "~>",
-- plus some convenience translations
[""] = "==",
["-"] = "==",
["="] = "==",
["!="] = "~="
}
local deltas = {
scm = -100,
rc = -1000,
pre = -10000,
beta = -100000,
alpha = -1000000,
work = -10000000,
}
local version_mt = {
--- Equality comparison for versions.
-- All version numbers must be equal.
-- If both versions have revision numbers, they must be equal;
-- otherwise the revision number is ignored.
-- @param v1 table: version table to compare.
-- @param v2 table: version table to compare.
-- @return boolean: true if they are considered equivalent.
__eq = function(v1, v2)
if #v1 ~= #v2 then
return false
end
for i = 1, #v1 do
if v1[i] ~= v2[i] then
return false
end
end
if v1.revision and v2.revision then
return (v1.revision == v2.revision)
end
return true
end,
--- Size comparison for versions.
-- All version numbers are compared.
-- If both versions have revision numbers, they are compared;
-- otherwise the revision number is ignored.
-- @param v1 table: version table to compare.
-- @param v2 table: version table to compare.
-- @return boolean: true if v1 is considered lower than v2.
__lt = function(v1, v2)
for i = 1, math.max(#v1, #v2) do
local v1i, v2i = v1[i] or 0, v2[i] or 0
if v1i ~= v2i then
return (v1i < v2i)
end
end
if v1.revision and v2.revision then
return (v1.revision < v2.revision)
end
return false
end
}
local version_cache = {}
setmetatable(version_cache, {
__mode = "kv"
})
--- Parse a version string, converting to table format.
-- A version table contains all components of the version string
-- converted to numeric format, stored in the array part of the table.
-- If the version contains a revision, it is stored numerically
-- in the 'revision' field. The original string representation of
-- the string is preserved in the 'string' field.
-- Returned version tables use a metatable
-- allowing later comparison through relational operators.
-- @param vstring string: A version number in string format.
-- @return table or nil: A version table or nil
-- if the input string contains invalid characters.
function parseVersion(vstring)
if not vstring then return nil end
assert(type(vstring) == "string")
local cached = version_cache[vstring]
if cached then
return cached
end
local version = {}
local i = 1
local function add_token(number)
version[i] = version[i] and version[i] + number/100000 or number
i = i + 1
end
-- trim leading and trailing spaces
vstring = vstring:match("^%s*(.*)%s*$")
version.string = vstring
-- store revision separately if any
local main, revision = vstring:match("(.*)%-(%d+)$")
if revision then
vstring = main
version.revision = tonumber(revision)
end
while #vstring > 0 do
-- extract a number
local token, rest = vstring:match("^(%d+)[%.%-%_]*(.*)")
if token then
add_token(tonumber(token))
else
-- extract a word
token, rest = vstring:match("^(%a+)[%.%-%_]*(.*)")
if not token then
return nil
end
local last = #version
version[i] = deltas[token] or (token:byte() / 1000)
end
vstring = rest
end
setmetatable(version, version_mt)
version_cache[vstring] = version
return version
end
--- Utility function to compare version numbers given as strings.
-- @param a string: one version.
-- @param b string: another version.
-- @return boolean: True if a > b.
function compareVersions(a, b)
return parseVersion(a) > parseVersion(b)
end
--- Consumes a constraint from a string, converting it to table format.
-- For example, a string ">= 1.0, > 2.0" is converted to a table in the
-- format {op = ">=", version={1,0}} and the rest, "> 2.0", is returned
-- back to the caller.
-- @param input string: A list of constraints in string format.
-- @return (table, string) or nil: A table representing the same
-- constraints and the string with the unused input, or nil if the
-- input string is invalid.
local function parseConstraint(input)
assert(type(input) == "string")
local op, version, rest = input:match("^([<>=~!]*)%s*([%w%.%_%-]+)[%s,]*(.*)")
op = operators[op]
version = parseVersion(version)
if not op or not version then return nil end
return { op = op, version = version }, rest
end
--- Convert a list of constraints from string to table format.
-- For example, a string ">= 1.0, < 2.0" is converted to a table in the format
-- {{op = ">=", version={1,0}}, {op = "<", version={2,0}}}.
-- Version tables use a metatable allowing later comparison through
-- relational operators.
-- @param input string: A list of constraints in string format.
-- @return table or nil: A table representing the same constraints,
-- or nil if the input string is invalid.
function parseConstraints(input)
assert(type(input) == "string")
local constraints, constraint = {}, nil
while #input > 0 do
constraint, input = parseConstraint(input)
if constraint then
table.insert(constraints, constraint)
else
return nil
end
end
return constraints
end
--- A more lenient check for equivalence between versions.
-- This returns true if the requested components of a version
-- match and ignore the ones that were not given. For example,
-- when requesting "2", then "2", "2.1", "2.3.5-9"... all match.
-- When requesting "2.1", then "2.1", "2.1.3" match, but "2.2"
-- doesn't.
-- @param version string or table: Version to be tested; may be
-- in string format or already parsed into a table.
-- @param requested string or table: Version requested; may be
-- in string format or already parsed into a table.
-- @return boolean: True if the tested version matches the requested
-- version, false otherwise.
local function partialMatch(version, requested)
assert(type(version) == "string" or type(version) == "table")
assert(type(requested) == "string" or type(version) == "table")
if type(version) ~= "table" then version = parseVersion(version) end
if type(requested) ~= "table" then requested = parseVersion(requested) end
if not version or not requested then return false end
for i = 1, #requested do
if requested[i] ~= version[i] then return false end
end
if requested.revision then
return requested.revision == version.revision
end
return true
end
--- Check if a version satisfies a set of constraints.
-- @param version table: A version in table format
-- @param constraints table: An array of constraints in table format.
-- @return boolean: True if version satisfies all constraints,
-- false otherwise.
function matchConstraints(version, constraints)
assert(type(version) == "table")
assert(type(constraints) == "table")
local ok = true
setmetatable(version, version_mt)
for _, constr in pairs(constraints) do
local constr_version = constr.version
setmetatable(constr.version, version_mt)
if constr.op == "==" then ok = version == constr_version
elseif constr.op == "~=" then ok = version ~= constr_version
elseif constr.op == ">" then ok = version > constr_version
elseif constr.op == "<" then ok = version < constr_version
elseif constr.op == ">=" then ok = version >= constr_version
elseif constr.op == "<=" then ok = version <= constr_version
elseif constr.op == "~>" then ok = partialMatch(version, constr_version)
end
if not ok then break end
end
return ok
end
--- Check if a version string is satisfied by a constraint string.
-- @param version string: A version in string format
-- @param constraints string: Constraints in string format.
-- @return boolean: True if version satisfies all constraints,
-- false otherwise.
function constraint_satisfied(version, constraints)
local const = parseConstraints(constraints)
local ver = parseVersion(version)
if const and ver then
return matchConstraints(ver, const)
end
return nil, "Error parsing versions."
end

View File

@ -0,0 +1,770 @@
-- Utility functions for dependencies
module ("dist.depends", package.seeall)
local cfg = require "dist.config"
local mf = require "dist.manifest"
local sys = require "dist.sys"
local const = require "dist.constraints"
local utils = require "dist.utils"
local package = require "dist.package"
-- Return all packages with specified names from manifest.
-- Names can also contain version constraint (e.g. 'copas>=1.2.3', 'saci-1.0' etc.).
function find_packages(package_names, manifest)
if type(package_names) == "string" then package_names = {package_names} end
manifest = manifest or mf.get_manifest()
assert(type(package_names) == "table", "depends.find_packages: Argument 'package_names' is not a table or string.")
assert(type(manifest) == "table", "depends.find_packages: Argument 'manifest' is not a table.")
local packages_found = {}
-- find matching packages in manifest
for _, pkg_to_find in pairs(package_names) do
local pkg_name, pkg_constraint = split_name_constraint(pkg_to_find)
pkg_name = utils.escape_magic(pkg_name):gsub("%%%*",".*")
for _, repo_pkg in pairs(manifest) do
if string.match(repo_pkg.name, "^" .. pkg_name .. "$") and (not pkg_constraint or satisfies_constraint(repo_pkg.version, pkg_constraint)) then
table.insert(packages_found, repo_pkg)
end
end
end
return packages_found
end
-- Return manifest consisting of packages installed in specified deploy_dir directory
function get_installed(deploy_dir)
deploy_dir = deploy_dir or cfg.root_dir
assert(type(deploy_dir) == "string", "depends.get_installed: Argument 'deploy_dir' is not a string.")
deploy_dir = sys.abs_path(deploy_dir)
local distinfos_path = sys.make_path(deploy_dir, cfg.distinfos_dir)
local manifest = {}
if not sys.is_dir(distinfos_path) then return {} end
-- from all directories of packages installed in deploy_dir
for dir in sys.get_directory(distinfos_path) do
if dir ~= "." and dir ~= ".." and sys.is_dir(sys.make_path(distinfos_path, dir)) then
local pkg_dist_dir = sys.make_path(distinfos_path, dir)
-- load the dist.info file
for file in sys.get_directory(pkg_dist_dir) do
local pkg_dist_file = sys.make_path(pkg_dist_dir, file)
if sys.is_file(pkg_dist_file) then
table.insert(manifest, mf.load_distinfo(pkg_dist_file))
end
end
end
end
return manifest
end
-- If 'pkg.selected' == true then returns 'selected' else 'installed'.
-- Used in error messages.
local function selected_or_installed(pkg)
assert(type(pkg) == "table", "depends.selected_or_installed: Argument 'pkg' is not a table.")
if pkg.selected == true then
return "selected"
else
return "installed"
end
end
-- Return whether the 'package_name' is installed according to the the manifest 'installed_pkgs'
-- If optional 'version_wanted' constraint is specified, then installed packages must
-- also satisfy specified version constraint.
-- If package is installed but doesn't satisfy version constraint, error message
-- is returned as the second value.
function is_installed(package_name, installed_pkgs, version_wanted)
assert(type(package_name) == "string", "depends.is_installed: Argument 'package_name' is not a string.")
assert(type(installed_pkgs) == "table", "depends.is_installed: Argument 'installed_pkgs' is not a table.")
assert(type(version_wanted) == "string" or type(version_wanted) == "nil", "depends.is_installed: Argument 'version_wanted' is not a string or nil.")
local pkg_is_installed, err = false, nil
for _, installed_pkg in pairs(installed_pkgs) do
-- check if package_name is in installed
if package_name == installed_pkg.name then
-- check if package is installed in satisfying version
if not version_wanted or satisfies_constraint(installed_pkg.version, version_wanted) then
pkg_is_installed = true
break
else
err = "Package '" .. package_name .. (version_wanted and " " .. version_wanted or "") .. "' needed, but " .. selected_or_installed(installed_pkg) .. " at version '" .. installed_pkg.version .. "'."
break
end
end
end
return pkg_is_installed, err
end
-- Check whether the package 'pkg' conflicts with 'installed_pkg' and return
-- false or error message.
local function packages_conflicts(pkg, installed_pkg)
assert(type(pkg) == "table", "depends.packages_conflicts: Argument 'pkg' is not a table.")
assert(type(installed_pkg) == "table", "depends.packages_conflicts: Argument 'installed_pkg' is not a table.")
-- check if pkg doesn't provide an already installed_pkg
if pkg.provides then
-- for all of pkg's provides
for _, provided_pkg in pairs(get_provides(pkg)) do
if provided_pkg.name == installed_pkg.name then
return "Package '" .. pkg_full_name(pkg.name, pkg.version, pkg.was_scm_version) .. "' provides '" .. pkg_full_name(provided_pkg.name, provided_pkg.version) .. "' but package '" .. pkg_full_name(installed_pkg.name, installed_pkg.version) .. "' is already " .. selected_or_installed(installed_pkg) .. "."
end
end
end
-- check for conflicts of package to install with installed package
if pkg.conflicts then
for _, conflict in pairs (pkg.conflicts) do
if conflict == installed_pkg.name then
return "Package '" .. pkg_full_name(pkg.name, pkg.version, pkg.was_scm_version) .. "' conflicts with already " .. selected_or_installed(installed_pkg) .. " package '" .. pkg_full_name(installed_pkg.name, installed_pkg.version) .. "'."
end
end
end
-- check for conflicts of installed package with package to install
if installed_pkg.conflicts then
-- direct conflicts with 'pkg'
for _, conflict in pairs (installed_pkg.conflicts) do
if conflict == pkg.name then
return "Already " .. selected_or_installed(installed_pkg) .. " package '" .. pkg_full_name(installed_pkg.name, installed_pkg.version) .. "' conflicts with package '" .. pkg_full_name(pkg.name, pkg.version, pkg.was_scm_version) .. "'."
end
end
-- conflicts with 'provides' of 'pkg' (packages provided by package to install)
if pkg.provides then
for _, conflict in pairs (installed_pkg.conflicts) do
-- for all of pkg's provides
for _, provided_pkg in pairs(get_provides(pkg)) do
if conflict == provided_pkg.name then
return "Already '" .. selected_or_installed(installed_pkg) .. " package '" .. pkg_full_name(installed_pkg.name, installed_pkg.version) .. "' conflicts with package '" .. pkg_full_name(provided_pkg.name, provided_pkg.version) .. "' provided by '" .. pkg_full_name(pkg.name, pkg.version, pkg.was_scm_version) .. "'."
end
end
end
end
end
-- no conflicts found
return false
end
-- Return table of package dependencies 'depends' with OS specific dependencies extracted.
--
-- OS specific dependencies are stored in a subtable with 'arch' as a key.
-- E.g. this table containing OS specific dependencies:
-- depends = {
-- "lua~>5.1",
-- "luadist-git>=0.1",
-- Linux = {
-- "iup>=3.6",
-- "wxlua>=2.8.10.0",
-- },
-- Windows = {
-- "luagd>=2.0.33r2",
-- "luacom>=1.4.1",
-- },
-- }
--
-- ...will be on the 'Linux' architecture (determined by cfg.arch) converted into:
-- depends = {
-- "lua~>5.1",
-- "luadist-git>=0.1",
-- "iup>=3.6",
-- "wxlua>=2.8.10.0",
-- }
function extract_os_specific_depends(depends)
assert(type(depends) == "table", "depends.extract_os_specific_depends: Argument 'depends' is not a table.")
local extracted = {}
for k, depend in pairs(depends) do
-- if 'depend' is a table, then it must be a table of OS specific
-- dependencies, so extract it if it's for this architecture
if type(depend) == "table" then
if k == cfg.arch then
for _, os_specific_depend in pairs(depend) do
table.insert(extracted, os_specific_depend)
end
end
else
table.insert(extracted, depend)
end
end
return extracted
end
-- Return all packages needed in order to install package 'pkg'
-- and with specified 'installed' packages in the system using 'manifest'.
-- 'pkg' can also contain version constraint (e.g. 'copas>=1.2.3', 'saci-1.0' etc.).
--
-- This function also downloads packages to get information about their dependencies.
-- Directory where the package was downloaded is stored in 'download_dir' attribute
-- of that package in the table of packages returned by this function.
--
-- Optional argument 'dependency_manifest' is a table of dependencies examined
-- from previous installations etc. It can be used to speed-up the dependency
-- resolving procedure for example.
--
-- When optional 'force_no_download' parameter is set to true, then information
-- about packages won't be downloaded during dependency resolving, assuming that
-- entries in the provided manifest are already complete.
--
-- When optional 'suppress_printing' parameter is set to true, then messages
-- for the user won't be printed during dependency resolving.
--
-- Optional argument 'deploy_dir' is used just as a temporary place to place
-- the downloaded packages into.
--
-- 'dependency_parents' is table of all packages encountered so far when resolving dependencies
-- and is used to detect and deal with circular dependencies. Leave it 'nil'
-- and it will do its job just fine :-).
--
-- 'tmp_installed' is internal table used in recursion and should be left 'nil' when
-- calling this function from other context. It is used for passing the changes
-- in installed packages between the recursive calls of this function.
--
-- TODO: refactor this spaghetti code!
local function get_packages_to_install(pkg, installed, manifest, dependency_manifest, force_no_download, suppress_printing, deploy_dir, dependency_parents, tmp_installed)
manifest = manifest or mf.get_manifest()
dependency_manifest = dependency_manifest or {}
force_no_download = force_no_download or false
suppress_printing = suppress_printing or false
deploy_dir = deploy_dir or cfg.root_dir
dependency_parents = dependency_parents or {}
-- set helper table 'tmp_installed'
tmp_installed = tmp_installed or utils.deepcopy(installed)
assert(type(pkg) == "string", "depends.get_packages_to_install: Argument 'pkg' is not a string.")
assert(type(installed) == "table", "depends.get_packages_to_install: Argument 'installed' is not a table.")
assert(type(manifest) == "table", "depends.get_packages_to_install: Argument 'manifest' is not a table.")
assert(type(dependency_manifest) == "table", "depends.get_packages_to_install: Argument 'dependency_manifest' is not a table.")
assert(type(force_no_download) == "boolean", "depends.get_packages_to_install: Argument 'force_no_download' is not a boolean.")
assert(type(suppress_printing) == "boolean", "depends.get_packages_to_install: Argument 'suppress_printing' is not a boolean.")
assert(type(deploy_dir) == "string", "depends.get_packages_to_install: Argument 'deploy_dir' is not a string.")
assert(type(dependency_parents) == "table", "depends.get_packages_to_install: Argument 'dependency_parents' is not a table.")
assert(type(tmp_installed) == "table", "depends.get_packages_to_install: Argument 'tmp_installed' is not a table.")
deploy_dir = sys.abs_path(deploy_dir)
--[[ for future debugging:
print('resolving: '.. pkg)
print(' installed: ', utils.table_tostring(installed))
print(' tmp_installed: ', utils.table_tostring(tmp_installed))
--]]
-- check if package is already installed
local pkg_name, pkg_constraint = split_name_constraint(pkg)
local pkg_is_installed, err = is_installed(pkg_name, tmp_installed, pkg_constraint)
if pkg_is_installed then return {} end
if err then return nil, err end
-- table of packages needed to be installed (will be returned)
local to_install = {}
-- find out available versions of 'pkg' and insert them into manifest
if not force_no_download then
local versions, err = package.retrieve_versions(pkg, manifest, suppress_printing)
if not versions then return nil, err end
for _, version in pairs(versions) do
table.insert(manifest, version)
end
end
-- find candidates & sort them
local candidates_to_install = find_packages(pkg, manifest)
if #candidates_to_install == 0 then
return nil, "No suitable candidate for '" .. pkg .. "' found."
end
candidates_to_install = sort_by_versions(candidates_to_install)
for _, pkg in pairs(candidates_to_install) do
--[[ for future debugging:
print(' candidate: '.. pkg.name..'-'..pkg.version)
print(' installed: ', utils.table_tostring(installed))
print(' tmp_installed: ', utils.table_tostring(tmp_installed))
print(' to_install: ', utils.table_tostring(to_install))
print(' -is installed: ', is_installed(pkg.name, tmp_installed, pkg_constraint))
--]]
-- if there's an error from the previous candidate, print the reason for trying another one
if not suppress_printing and err then print(" - trying another candidate due to: " .. err) end
-- clear the state from the previous candidate
pkg_is_installed, err = false, nil
-- check whether this package has already been added to 'tmp_installed' by another of its candidates
pkg_is_installed, err = is_installed(pkg.name, tmp_installed, pkg_constraint)
if pkg_is_installed then break end
-- preserve information about the 'scm' version, because pkg.version
-- will be rewritten by information taken from pkg's dist.info file
local was_scm_version = (pkg.version == "scm")
-- Try to obtain cached dependency information from the dependency manifest
if dependency_manifest[pkg.name .. "-" .. pkg.version] and cfg.dep_cache then
pkg = dependency_manifest[pkg.name .. "-" .. pkg.version]
else
-- download info about the package if not already downloaded and downloading not prohibited
if not (pkg.download_dir or force_no_download) then
local path_or_err
pkg, path_or_err = package.retrieve_pkg_info(pkg, deploy_dir, suppress_printing)
if not pkg then
err = "Error when resolving dependencies: " .. path_or_err
else
-- set path to downloaded package - used to indicate that the
-- package was already downloaded, to delete unused but downloaded
-- packages and also to install choosen packages
pkg.download_dir = path_or_err
end
end
end
if pkg and was_scm_version then pkg.was_scm_version = true end
-- check arch & type
if not err then
if not (pkg.arch == "Universal" or pkg.arch == cfg.arch) or
not (pkg.type == "all" or pkg.type == "source" or pkg.type == cfg.type) then
err = "Package '" .. pkg_full_name(pkg.name, pkg.version) .. "' doesn't have required arch and type."
end
end
-- checks for conflicts with other installed (or previously selected) packages
if not err then
for _, installed_pkg in pairs(tmp_installed) do
err = packages_conflicts(pkg, installed_pkg)
if err then break end
end
end
-- if pkg passed all of the above tests
if not err then
-- check if pkg's dependencies are satisfied
if pkg.depends then
-- insert pkg into the stack of circular dependencies detection
table.insert(dependency_parents, pkg.name)
-- extract all OS specific dependencies of pkg
pkg.depends = extract_os_specific_depends(pkg.depends)
-- for all dependencies of pkg
for _, depend in pairs(pkg.depends) do
local dep_name = split_name_constraint(depend)
-- detect circular dependencies using 'dependency_parents'
local is_circular_dependency = false
for _, parent in pairs(dependency_parents) do
if dep_name == parent then
is_circular_dependency = true
break
end
end
-- if circular dependencies not detected
if not is_circular_dependency then
-- recursively call this function on the candidates of this pkg's dependency
local depends_to_install, dep_err = get_packages_to_install(depend, installed, manifest, dependency_manifest, force_no_download, suppress_printing, deploy_dir, dependency_parents, tmp_installed)
-- if any suitable dependency packages were found, insert them to the 'to_install' table
if depends_to_install then
for _, depend_to_install in pairs(depends_to_install) do
-- add some meta information
if not depend_to_install.selected_by then
depend_to_install.selected_by = pkg.name .. "-" .. pkg.version
end
table.insert(to_install, depend_to_install)
table.insert(tmp_installed, depend_to_install)
table.insert(installed, depend_to_install)
end
else
err = "Error getting dependency of '" .. pkg_full_name(pkg.name, pkg.version) .. "': " .. dep_err
break
end
-- if circular dependencies detected
else
err = "Error getting dependency of '" .. pkg_full_name(pkg.name, pkg.version) .. "': '" .. dep_name .. "' is a circular dependency."
break
end
end
-- remove last package from the stack of circular dependencies detection
table.remove(dependency_parents)
end
-- if no error occured
if not err then
-- add pkg and it's provides to the fake table of installed packages, with
-- property 'selected' set, indicating that the package isn't
-- really installed in the system, just selected to be installed (this is used e.g. in error messages)
pkg.selected = true
table.insert(tmp_installed, pkg)
if pkg.provides then
for _, provided_pkg in pairs(get_provides(pkg)) do
provided_pkg.selected = true
table.insert(tmp_installed, provided_pkg)
end
end
-- add pkg to the table of packages to install
table.insert(to_install, pkg)
-- if some error occured
else
-- delete the downloaded package
if pkg.download_dir and not cfg.debug then sys.delete(pkg.download_dir) end
-- set tables of 'packages to install' and 'installed packages' to their original state
to_install = {}
tmp_installed = utils.deepcopy(installed)
-- add provided packages to installed ones
for _, installed_pkg in pairs(tmp_installed) do
for _, pkg in pairs(get_provides(installed_pkg)) do
table.insert(tmp_installed, pkg)
end
end
end
-- if error occured
else
-- delete the downloaded package
if pkg and pkg.download_dir and not cfg.debug then sys.delete(pkg.download_dir) end
-- if pkg is already installed, skip checking its other candidates
if pkg_is_installed then break end
end
end
-- if package is not installed and no suitable candidates were found, return the last error
if #to_install == 0 and not pkg_is_installed then
return nil, err
else
return to_install
end
end
-- Resolve dependencies and return all packages needed in order to install
-- 'packages' into the system with already 'installed' packages, using 'manifest'.
-- Also return the table of the dependencies determined during the process
-- as the second return value.
--
-- Optional argument 'dependency_manifest' is a table of dependencies examined
-- from previous installations etc. It can be used to speed-up the dependency
-- resolving procedure for example.
--
-- Optional argument 'deploy_dir' is used as a temporary place to place the
-- downloaded packages into.
--
-- When optional 'force_no_download' parameter is set to true, then information
-- about packages won't be downloaded during dependency resolving, assuming that
-- entries in manifest are complete.
--
-- When optional 'suppress_printing' parameter is set to true, then messages
-- for the user won't be printed during dependency resolving.
function get_depends(packages, installed, manifest, dependency_manifest, deploy_dir, force_no_download, suppress_printing)
if not packages then return {} end
manifest = manifest or mf.get_manifest()
dependency_manifest = dependency_manifest or {}
deploy_dir = deploy_dir or cfg.root_dir
force_no_download = force_no_download or false
suppress_printing = suppress_printing or false
if type(packages) == "string" then packages = {packages} end
assert(type(packages) == "table", "depends.get_depends: Argument 'packages' is not a table or string.")
assert(type(installed) == "table", "depends.get_depends: Argument 'installed' is not a table.")
assert(type(manifest) == "table", "depends.get_depends: Argument 'manifest' is not a table.")
assert(type(dependency_manifest) == "table", "depends.get_depends: Argument 'dependency_manifest' is not a table.")
assert(type(deploy_dir) == "string", "depends.get_depends: Argument 'deploy_dir' is not a string.")
assert(type(force_no_download) == "boolean", "depends.get_depends: Argument 'force_no_download' is not a boolean.")
assert(type(suppress_printing) == "boolean", "depends.get_depends: Argument 'suppress_printing' is not a boolean.")
deploy_dir = sys.abs_path(deploy_dir)
local tmp_installed = utils.deepcopy(installed)
-- add provided packages to installed ones
for _, installed_pkg in pairs(tmp_installed) do
for _, pkg in pairs(get_provides(installed_pkg)) do
table.insert(tmp_installed, pkg)
end
end
-- If 'pkg' contains valid (architecture specific) path separator,
-- it is treated like a path to already downloaded package and
-- we assume that user wants to use this specific version of the
-- module to be installed. Hence, we will add information about
-- this version into the manifest and also remove references to
-- any other versions of this module from the manifest. This will
-- enforce the version of the module required by the user.
for k, pkg in pairs(packages) do
if pkg:find(sys.path_separator()) then
local pkg_dir = sys.abs_path(pkg)
local pkg_info, err = mf.load_distinfo(sys.make_path(pkg_dir, "dist.info"))
if not pkg_info then return nil, err end
-- add information about location of the package, also to prevent downloading it again
pkg_info.download_dir = pkg_dir
-- mark package to skip deleting its directory after installation
pkg_info.preserve_pkg_dir = true
-- set default arch/type if not explicitly stated and package is of source type
if package.is_source_type(pkg_dir) then
pkg_info = package.ensure_source_arch_and_type(pkg_info)
elseif not (pkg_info.arch and pkg_info.type) then
return nil, pkg_dir .. ": binary package missing arch or type in 'dist.info'."
end
-- update manifest
manifest = utils.filter(manifest, function(p) return p.name ~= pkg_info.name and true end)
table.insert(manifest, pkg_info)
-- update packages to install
pkg = pkg_info.name .. "-" .. pkg_info.version
packages[k] = pkg
end
end
local to_install = {}
-- get packages needed to satisfy the dependencies
for _, pkg in pairs(packages) do
local needed_to_install, err = get_packages_to_install(pkg, tmp_installed, manifest, dependency_manifest, force_no_download, suppress_printing, deploy_dir)
-- if everything's fine
if needed_to_install then
for _, needed_pkg in pairs(needed_to_install) do
-- TODO: why not to use 'installed' instead of 'tmp_installed'?
-- It's because provides aren't searched for by find()
-- function inside the update_dependency_manifest().
dependency_manifest = update_dependency_manifest(needed_pkg, tmp_installed, needed_to_install, dependency_manifest)
table.insert(to_install, needed_pkg)
table.insert(tmp_installed, needed_pkg)
-- add provides of needed_pkg to installed ones
for _, provided_pkg in pairs(get_provides(needed_pkg)) do
-- copy 'selected' property
provided_pkg.selected = needed_pkg.selected
table.insert(tmp_installed, provided_pkg)
end
end
-- if error occured
else
-- delete already downloaded packages
for _, pkg in pairs(to_install) do
if pkg.download_dir and not cfg.debug then sys.delete(pkg.download_dir) end
end
return nil, "Cannot resolve dependencies for '" .. pkg .. "': ".. err
end
end
return to_install, dependency_manifest
end
-- Return table of packages provided by specified package (from it's 'provides' field)
function get_provides(package)
assert(type(package) == "table", "depends.get_provides: Argument 'package' is not a table.")
if not package.provides then return {} end
local provided = {}
for _, provided_name in pairs(package.provides) do
local pkg = {}
pkg.name, pkg.version = split_name_constraint(provided_name)
pkg.type = package.type
pkg.arch = package.arch
pkg.provided = package.name .. "-" .. package.version
table.insert(provided, pkg)
end
return provided
end
-- Return package name and version constraint from full package version constraint specification
-- E. g.:
-- for 'luaexpat-1.2.3' return: 'luaexpat' , '1.2.3'
-- for 'luajit >= 1.2' return: 'luajit' , '>=1.2'
function split_name_constraint(version_constraint)
assert(type(version_constraint) == "string", "depends.split_name_constraint: Argument 'version_constraint' is not a string.")
local split = version_constraint:find("[%s=~<>-]+%d") or version_constraint:find("[%s=~<>-]+scm")
if split then
return version_constraint:sub(1, split - 1), version_constraint:sub(split):gsub("[%s-]", "")
else
return version_constraint, nil
end
end
-- Return only packages that can be installed on the specified architecture and type
function filter_packages_by_arch_and_type(packages, req_arch, req_type)
assert(type(packages) == "table", "depends.filter_packages_by_arch_and_type: Argument 'packages' is not a table.")
assert(type(req_arch) == "string", "depends.filter_packages_by_arch_and_type: Argument 'req_arch' is not a string.")
assert(type(req_type) == "string", "depends.filter_packages_by_arch_and_type: Argument 'pkg_type' is not a string.")
return utils.filter(packages,
function (pkg)
return (pkg.arch == "Universal" or pkg.arch == req_arch) and
(pkg.type == "all" or pkg.type == "source" or pkg.type == req_type)
end)
end
-- Return only packages that contain one of the specified strings in their 'name-version'.
-- Case is ignored. If no strings are specified, return all the packages.
-- Argument 'search_in_desc' specifies if search also in description of packages.
function filter_packages_by_strings(packages, strings, search_in_desc)
if type(strings) == "string" then strings = {strings} end
assert(type(packages) == "table", "depends.filter_packages_by_strings: Argument 'packages' is not a table.")
assert(type(strings) == "table", "depends.filter_packages_by_strings: Argument 'strings' is not a string or table.")
if #strings ~= 0 then
return utils.filter(packages,
function (pkg)
for _,str in pairs(strings) do
local name = pkg.name .. "-" .. pkg.version
if search_in_desc then
name = name .. " " .. (pkg.desc or "")
end
if string.find(string.lower(name), string.lower(str), 1 ,true) ~= nil then return true end
end
end)
else
return packages
end
end
-- Return full package name and version string (e.g. 'luajit-2.0'). When version
-- is nil or '' then return only name (e.g. 'luajit') and when name is nil or ''
-- then return '<unknown>'. Optional 'was_scm_version' argument is a boolean,
-- stating whether the package was originally selected for installation as a 'scm' version.
function pkg_full_name(name, version, was_scm_version)
name = name or ""
version = version or ""
was_scm_version = was_scm_version or false
if type(version) == "number" then version = tostring(version) end
assert(type(name) == "string", "depends.pkg_full_name: Argument 'name' is not a string.")
assert(type(version) == "string", "depends.pkg_full_name: Argument 'version' is not a string.")
if was_scm_version then version = version .. " [scm version]" end
if name == "" then
return "<unknown>"
else
return name .. ((version ~= "") and "-" .. version or "")
end
end
-- Return table of packages, sorted descendingly by versions (newer ones are moved to the top).
function sort_by_versions(packages)
assert(type(packages) == "table", "depends.sort_by_versions: Argument 'packages' is not a table.")
return utils.sort(packages, function (a, b) return compare_versions(a.version, b.version) end)
end
-- Return table of packages, sorted alphabetically by name and then descendingly by version.
function sort_by_names(packages)
assert(type(packages) == "table", "depends.sort_by_names: Argument 'packages' is not a table.")
return utils.sort(packages, function (a, b)
if a.name == b.name then
return compare_versions(a.version, b.version)
else
return a.name < b.name
end
end)
end
-- Return if version satisfies the specified constraint
function satisfies_constraint(version, constraint)
assert(type(version) == "string", "depends.satisfies_constraint: Argument 'version' is not a string.")
assert(type(constraint) == "string", "depends.satisfies_constraint: Argument 'constraint' is not a string.")
return const.constraint_satisfied(version, constraint)
end
-- For package versions, return whether: 'version_a' > 'version_b'
function compare_versions(version_a, version_b)
assert(type(version_a) == "string", "depends.compare_versions: Argument 'version_a' is not a string.")
assert(type(version_b) == "string", "depends.compare_versions: Argument 'version_b' is not a string.")
return const.compareVersions(version_a, version_b)
end
-- Returns 'dep_manifest' updated with information about the 'pkg'.
-- 'installed' is table with installed packages
-- 'to_install' is table with packages that are selected for installation
-- Packages satisfying the dependencies will be searched for in these two tables.
function update_dependency_manifest(pkg, installed, to_install, dep_manifest)
dep_manifest = dep_manifest or {}
assert(type(pkg) == "table", "depends.update_dependency_manifest: Argument 'pkg' is not a table.")
assert(type(installed) == "table", "depends.update_dependency_manifest: Argument 'installed' is not a table.")
assert(type(to_install) == "table", "depends.update_dependency_manifest: Argument 'to_install' is not a table.")
assert(type(dep_manifest) == "table", "depends.update_dependency_manifest: Argument 'dep_manifest' is not a table.")
local name_ver = pkg.name .. "-" .. (pkg.was_scm_version and "scm" or pkg.version)
-- add to manifest
if not dep_manifest[name_ver] then
dep_manifest[name_ver] = {}
dep_manifest[name_ver].name = pkg.name
dep_manifest[name_ver].version = pkg.version
dep_manifest[name_ver].was_scm_version = pkg.was_scm_version
dep_manifest[name_ver].arch = pkg.arch
dep_manifest[name_ver].type = pkg.type
dep_manifest[name_ver].path = pkg.path
dep_manifest[name_ver].depends = pkg.depends
dep_manifest[name_ver].conflicts = pkg.conflicts
dep_manifest[name_ver].provides = pkg.provides
dep_manifest[name_ver].license = pkg.license
dep_manifest[name_ver].desc = pkg.desc
dep_manifest[name_ver].url = pkg.url
dep_manifest[name_ver].author = pkg.author
dep_manifest[name_ver].maintainer = pkg.maintainer
-- add information which dependency is satisfied by which package
if pkg.depends then
-- TODO: Won't it be better to add OS-specific 'satisfied_by' metadata in a format like OS-specific 'depends' ?
local all_deps = extract_os_specific_depends(pkg.depends)
dep_manifest[name_ver].satisfied_by = {}
for _, depend in pairs(all_deps) do
-- find package satisfying the dependency
local satisfying = find_packages(depend, installed)[1] or find_packages(depend, to_install)[1]
satisfying = satisfying.name .. "-" .. satisfying.version
dep_manifest[name_ver].satisfied_by[depend] = satisfying
-- check whether the satisfying package isn't provided by other one
local provided_by = utils.filter(installed, function(pkg)
return pkg.provides and utils.contains(pkg.provides, satisfying)
end)
if #provided_by == 0 then
provided_by = utils.filter(to_install, function(pkg)
return pkg.provides and utils.contains(pkg.provides, satisfying)
end)
end
if #provided_by ~= 0 then
if not dep_manifest[name_ver].satisfying_provided_by then
dep_manifest[name_ver].satisfying_provided_by = {}
end
dep_manifest[name_ver].satisfying_provided_by[satisfying] = provided_by[1].name .. "-" .. provided_by[1].version
end
end
end
end
return dep_manifest
end

View File

@ -0,0 +1,306 @@
-- Encapsulated Git functionality
module ("dist.git", package.seeall)
require "git"
local sys = require "dist.sys"
local cfg = require "dist.config"
-- Clone the repository from url to dest_dir
function clone(repository_url, dest_dir, depth, branch)
assert(type(repository_url) == "string", "git.clone: Argument 'repository_url' is not a string.")
assert(type(dest_dir) == "string", "git.clone: Argument 'dest_dir' is not a string.")
dest_dir = sys.abs_path(dest_dir)
local command = "git clone " .. repository_url
if depth then
assert(type(depth) == "number", "git.clone: Argument 'depth' is not a number.")
command = command .. " --depth " .. depth
end
if branch then
assert(type(branch) == "string", "git.clone: Argument 'branch' is not a string.")
command = command .. " -b " .. branch
end
command = command .. " " .. sys.quote(dest_dir)
if sys.exists(dest_dir) then sys.delete(dest_dir) end
sys.make_dir(dest_dir)
-- change the current working directory to dest_dir
local prev_current_dir = sys.current_dir()
sys.change_dir(dest_dir)
-- execute git clone
if not cfg.debug then command = command .. " -q " end
local ok, err = sys.exec(command)
-- change the current working directory back
sys.change_dir(prev_current_dir)
return ok, err
end
-- Return table of all refs of the remote repository at the 'git_url'. Ref_type can be "tags" or "heads".
local function get_remote_refs(git_url, ref_type)
assert(type(git_url) == "string", "git.get_remote_refs: Argument 'git_url' is not a string.")
assert(type(ref_type) == "string", "git.get_remote_refs: Argument 'ref_type' is not a string.")
assert(ref_type == "tags" or ref_type == "heads", "git.get_remote_refs: Argument 'ref_type' is not \"tags\" or \"heads\".")
local refs = {}
local ok, refs_or_err = pcall(git.protocol.remotes, git_url)
if not ok then return nil, "Error getting refs of the remote repository '" .. git_url .. "': " .. refs_or_err end
for ref, sha in pairs(refs_or_err) do
if ref:match("%S+/" .. ref_type .. "/%S+") and not ref:match("%^{}") then
table.insert(refs, ref:match("%S+/" .. ref_type .. "/(%S+)"))
end
end
return refs
end
-- Return table of all tags of the repository at the 'git_url'
function get_remote_tags(git_url)
return get_remote_refs(git_url, "tags")
end
-- Return table of all branches of the repository at the 'git_url'
function get_remote_branches(git_url)
return get_remote_refs(git_url, "heads")
end
-- Checkout specified ref in specified git_repo_dir
function checkout_ref(ref, git_repo_dir, orphaned)
git_repo_dir = git_repo_dir or sys.current_dir()
orphaned = orphaned or false
assert(type(ref) == "string", "git.checkout_ref: Argument 'ref' is not a string.")
assert(type(git_repo_dir) == "string", "git.checkout_ref: Argument 'git_repo_dir' is not a string.")
assert(type(orphaned) == "boolean", "git.checkout_ref: Argument 'orphaned' is not a boolean.")
git_repo_dir = sys.abs_path(git_repo_dir)
local command = "git checkout "
if orphaned then command = command .. " --orphan " end
command = command .. " " .. ref .. " -f"
if not cfg.debug then command = command .. " -q " end
local ok, err
if git_repo_dir ~= sys.current_dir() then
local prev_current_dir = sys.current_dir()
sys.change_dir(git_repo_dir)
ok, err = sys.exec(command)
sys.change_dir(prev_current_dir)
else
ok, err = sys.exec(command)
end
return ok, err
end
-- Checkout specified sha in specified git_repo_dir
function checkout_sha(sha, git_repo_dir)
git_repo_dir = git_repo_dir or sys.current_dir()
assert(type(sha) == "string", "git.checkout_sha: Argument 'sha' is not a string.")
assert(type(git_repo_dir) == "string", "git.checkout_sha: Argument 'git_repo_dir' is not a string.")
git_repo_dir = sys.abs_path(git_repo_dir)
local dir_changed, prev_current_dir
if git_repo_dir ~= sys.current_dir() then
prev_current_dir = sys.current_dir()
sys.change_dir(git_repo_dir)
dir_changed = true
end
local ok, repo_or_err = pcall(git.repo.open, git_repo_dir)
if not ok then return nil, "Error when opening the git repository '" .. git_repo_dir .. "': " .. repo_or_err end
local err
ok, err = pcall(repo_or_err.checkout, repo_or_err, sha, git_repo_dir)
if not ok then return nil, "Error when checking out the sha '" .. sha .. "' in the git repository '" .. git_repo_dir .. "': " .. err end
repo_or_err:close()
if dir_changed then sys.change_dir(prev_current_dir) end
return true
end
-- Create an empty git repository in given directory.
function init(dir)
dir = dir or sys.current_dir()
assert(type(dir) == "string", "git.init: Argument 'dir' is not a string.")
dir = sys.abs_path(dir)
-- create the 'dir' first, since it causes 'git init' to fail on Windows
-- when the parent directory of 'dir' doesn't exist
local ok, err = sys.make_dir(dir)
if not ok then return nil, err end
local command = "git init " .. sys.quote(dir)
if not cfg.debug then command = command .. " -q " end
return sys.exec(command)
end
-- Add all files in the 'repo_dir' to the git index. The 'repo_dir' must be
-- in the initialized git repository.
function add_all(repo_dir)
repo_dir = repo_dir or sys.current_dir()
assert(type(repo_dir) == "string", "git.add_all: Argument 'repo_dir' is not a string.")
repo_dir = sys.abs_path(repo_dir)
local ok, prev_dir, msg
ok, prev_dir = sys.change_dir(repo_dir);
if not ok then return nil, err end
ok, msg = sys.exec("git add -A -f " .. sys.quote(repo_dir))
sys.change_dir(prev_dir)
return ok, msg
end
-- Commit all indexed files in 'repo_dir' with the given commit 'message'.
-- The 'repo_dir' must be in the initialized git repository.
function commit(message, repo_dir)
repo_dir = repo_dir or sys.current_dir()
message = message or "commit by luadist-git"
assert(type(message) == "string", "git.commit: Argument 'message' is not a string.")
assert(type(repo_dir) == "string", "git.commit: Argument 'repo_dir' is not a string.")
repo_dir = sys.abs_path(repo_dir)
local ok, prev_dir, msg
ok, prev_dir = sys.change_dir(repo_dir);
if not ok then return nil, err end
local command = "git commit -m " .. sys.quote(message)
if not cfg.debug then command = command .. " -q " end
ok, msg = sys.exec(command)
sys.change_dir(prev_dir)
return ok, msg
end
-- Rename branch 'old_name' to 'new_name'. -- The 'repo_dir' must be
-- in the initialized git repository and the branch 'new_name' must
-- not already exist in that repository.
function rename_branch(old_name, new_name, repo_dir)
repo_dir = repo_dir or sys.current_dir()
assert(type(old_name) == "string", "git.rename_branch: Argument 'old_name' is not a string.")
assert(type(new_name) == "string", "git.rename_branch: Argument 'new_name' is not a string.")
assert(type(repo_dir) == "string", "git.rename_branch: Argument 'repo_dir' is not a string.")
repo_dir = sys.abs_path(repo_dir)
local ok, prev_dir, msg
ok, prev_dir = sys.change_dir(repo_dir);
if not ok then return nil, err end
ok, msg = sys.exec("git branch -m " .. old_name .. " " .. new_name)
sys.change_dir(prev_dir)
return ok, msg
end
-- Push the ref 'ref_name' from the 'repo_dir' to the remote git
-- repository 'git_repo_url'. If 'all_tags' is set to true, all tags
-- will be pushed, in addition to the explicitly given ref.
-- If 'delete' is set to 'true' then the explicitly given remote ref
-- will be deleted, not pushed.
function push_ref(repo_dir, ref_name, git_repo_url, all_tags, delete)
repo_dir = repo_dir or sys.current_dir()
all_tags = all_tags or false
delete = delete or false
assert(type(repo_dir) == "string", "git.push_ref: Argument 'repo_dir' is not a string.")
assert(type(git_repo_url) == "string", "git.push_ref: Argument 'git_repo_url' is not a string.")
assert(type(ref_name) == "string", "git.push_ref: Argument 'ref_name' is not a string.")
assert(type(all_tags) == "boolean", "git.push_ref: Argument 'all_tags' is not a boolean.")
assert(type(delete) == "boolean", "git.push_ref: Argument 'delete' is not a boolean.")
repo_dir = sys.abs_path(repo_dir)
local ok, prev_dir, msg
ok, prev_dir = sys.change_dir(repo_dir);
if not ok then return nil, err end
local command = "git push " .. git_repo_url
if all_tags then command = command .. " --tags " end
if delete then command = command .. " --delete " end
command = command .. " " .. ref_name .. " -f "
if not cfg.debug then command = command .. " -q " end
ok, msg = sys.exec(command)
sys.change_dir(prev_dir)
return ok, msg
end
-- Creates the tag 'tag_name' in given 'repo_dir', which must be
-- in the initialized git repository
function create_tag(repo_dir, tag_name)
repo_dir = repo_dir or sys.current_dir()
assert(type(repo_dir) == "string", "git.create_tag: Argument 'repo_dir' is not a string.")
assert(type(tag_name) == "string", "git.create_tag: Argument 'tag_name' is not a string.")
repo_dir = sys.abs_path(repo_dir)
local ok, prev_dir, msg
ok, prev_dir = sys.change_dir(repo_dir);
if not ok then return nil, err end
ok, msg = sys.exec("git tag " .. tag_name .. " -f ")
sys.change_dir(prev_dir)
return ok, msg
end
-- Fetch given 'ref_name' from the remote 'git_repo_url' to the local repository
-- 'repo_dir' and return its sha. 'ref_type' can be "tag" or "head".
local function fetch_ref(repo_dir, git_repo_url, ref_name, ref_type)
repo_dir = repo_dir or sys.current_dir()
assert(type(repo_dir) == "string", "git.fetch_ref: Argument 'repo_dir' is not a string.")
assert(type(git_repo_url) == "string", "git.fetch_ref: Argument 'git_repo_url' is not a string.")
assert(type(ref_name) == "string", "git.fetch_ref: Argument 'ref_name' is not a string.")
assert(type(ref_type) == "string", "git.fetch_ref: Argument 'ref_type' is not a string.")
assert(ref_type == "tag" or ref_type == "head", "git.get_remote_refs: Argument 'ref_type' is not \"tag\" or \"head\".")
repo_dir = sys.abs_path(repo_dir)
local refstring = "refs/" .. ref_type .. "s/" .. ref_name
local suppress_fetch_progress = not cfg.debug
local ok, repo_or_err = pcall(git.repo.open, repo_dir)
if not ok then return nil, "Error when opening the git repository '" .. repo_dir .. "': " .. repo_or_err end
local ok, pack_or_err, sha = pcall(git.protocol.fetch, git_repo_url, repo_or_err, refstring, suppress_fetch_progress)
if not ok then return nil, "Error when fetching ref '" .. refstring .. "' from git repository '" .. git_repo_url .. "': " .. pack_or_err end
repo_or_err:close()
pack_or_err:close()
return sha
end
-- Fetch given 'tag_name' from the remote 'git_repo_url' to the local repository
-- 'repo_dir' and save it as a tag with the same 'tag_name'.
function fetch_tag(repo_dir, git_repo_url, tag_name)
return fetch_ref(repo_dir, git_repo_url, tag_name, "tag")
end
-- Fetch given 'branch_name' from the remote 'git_repo_url' to the local repository
-- 'repo_dir' and save it as a branch with the same 'branch_name'.
function fetch_branch(repo_dir, git_repo_url, branch_name)
return fetch_ref(repo_dir, git_repo_url, branch_name, "head")
end
-- Create the git repository and return the repo object (which can be used in checkout_sha etc.)
-- If the 'dir' exists, it's deleted prior to creating the git repository.
function create_repo(dir)
assert(type(dir) == "string", "git.create_repo: Argument 'dir' is not a string.")
if sys.exists(dir) then sys.delete(dir) end
local ok, repo_or_err = pcall(git.repo.create, dir)
if not ok then return nil, "Error when creating the git repository '" .. dir .. "': " .. repo_or_err end
repo_or_err:close()
return true
end

View File

@ -0,0 +1,349 @@
-- main API of LuaDist
module ("dist", package.seeall)
local cfg = require "dist.config"
local depends = require "dist.depends"
local git = require "dist.git"
local sys = require "dist.sys"
local package = require "dist.package"
local mf = require "dist.manifest"
local utils = require "dist.utils"
-- Return the deployment directory.
function get_deploy_dir()
return sys.abs_path(cfg.root_dir)
end
-- Return packages deployed in 'deploy_dir' also with their provides.
function get_deployed(deploy_dir)
deploy_dir = deploy_dir or cfg.root_dir
assert(type(deploy_dir) == "string", "dist.get_deployed: Argument 'deploy_dir' is not a string.")
deploy_dir = sys.abs_path(deploy_dir)
local deployed = depends.get_installed(deploy_dir)
local provided = {}
for _, pkg in pairs(deployed) do
for _, provided_pkg in pairs(depends.get_provides(pkg)) do
provided_pkg.provided_by = pkg.name .. "-" .. pkg.version
table.insert(provided, provided_pkg)
end
end
for _, provided_pkg in pairs(provided) do
table.insert(deployed, provided_pkg)
end
deployed = depends.sort_by_names(deployed)
return deployed
end
-- Download new 'manifest_file' from repository and returns it.
-- Return nil and error message on error.
function update_manifest(deploy_dir)
deploy_dir = deploy_dir or cfg.root_dir
assert(type(deploy_dir) == "string", "dist.update_manifest: Argument 'deploy_dir' is not a string.")
deploy_dir = sys.abs_path(deploy_dir)
-- TODO: use 'deploy_dir' argument in manifest functions
-- retrieve the new manifest (forcing no cache use)
local manifest, err = mf.get_manifest(nil, true)
if manifest then
return manifest
else
return nil, err
end
end
-- Install 'package_names' to 'deploy_dir', using optional CMake 'variables'.
function install(package_names, deploy_dir, variables)
if not package_names then return true end
deploy_dir = deploy_dir or cfg.root_dir
if type(package_names) == "string" then package_names = {package_names} end
assert(type(package_names) == "table", "dist.install: Argument 'package_names' is not a table or string.")
assert(type(deploy_dir) == "string", "dist.install: Argument 'deploy_dir' is not a string.")
deploy_dir = sys.abs_path(deploy_dir)
-- find installed packages
local installed = depends.get_installed(deploy_dir)
-- get manifest
local manifest, err = mf.get_manifest()
if not manifest then return nil, "Error getting manifest: " .. err end
-- get dependency manifest
-- TODO: Is it good that dep_manifest is deploy_dir-specific?
-- Probably it'd be better not to be specific, but then there're
-- problems with 'provides'. E.g. What to do if there's a module
-- installed, that is provided by two different modules in two deploy_dirs?
local dep_manifest_file = sys.abs_path(sys.make_path(deploy_dir, cfg.dep_cache_file))
local dep_manifest, status = {}
if sys.exists(dep_manifest_file) and not utils.cache_timeout_expired(cfg.cache_timeout, dep_manifest_file) then
status, dep_manifest = mf.load_manifest(dep_manifest_file)
if not dep_manifest then return nil, status end
end
-- resolve dependencies
local dependencies, dep_manifest_or_err = depends.get_depends(package_names, installed, manifest, dep_manifest, deploy_dir, false, false)
if not dependencies then return nil, dep_manifest_or_err end
if #dependencies == 0 then return nil, "No packages to install." end
-- save updated dependency manifest
local ok, err = sys.make_dir(sys.parent_dir(dep_manifest_file))
if not ok then return nil, err end
ok, err = mf.save_manifest(dep_manifest_or_err, dep_manifest_file)
if not ok then return nil, err end
-- fetch the packages from repository
local fetched_pkgs = {}
for _, pkg in pairs(dependencies) do
local fetched_pkg, err = package.fetch_pkg(pkg, sys.make_path(deploy_dir, cfg.temp_dir))
if not fetched_pkg then return nil, err end
table.insert(fetched_pkgs, fetched_pkg)
end
-- install fetched packages
for _, pkg in pairs(fetched_pkgs) do
local ok, err = package.install_pkg(pkg.download_dir, deploy_dir, variables, pkg.preserve_pkg_dir)
if not ok then return nil, err end
end
return true
end
-- Manually deploy packages from 'package_dirs' to 'deploy_dir', using optional
-- CMake 'variables'. The 'package_dirs' are preserved (will not be deleted).
function make(deploy_dir, package_dirs, variables)
deploy_dir = deploy_dir or cfg.root_dir
package_dirs = package_dirs or {}
assert(type(deploy_dir) == "string", "dist.make: Argument 'deploy_dir' is not a string.")
assert(type(package_dirs) == "table", "dist.make: Argument 'package_dirs' is not a table.")
deploy_dir = sys.abs_path(deploy_dir)
for _, dir in pairs(package_dirs) do
local ok, err = package.install_pkg(sys.abs_path(dir), deploy_dir, variables, true)
if not ok then return nil, err end
end
return true
end
-- Remove 'package_names' from 'deploy_dir' and return the number of removed
-- packages.
function remove(package_names, deploy_dir)
deploy_dir = deploy_dir or cfg.root_dir
if type(package_names) == "string" then package_names = {package_names} end
assert(type(package_names) == "table", "dist.remove: Argument 'package_names' is not a string or table.")
assert(type(deploy_dir) == "string", "dist.remove: Argument 'deploy_dir' is not a string.")
deploy_dir = sys.abs_path(deploy_dir)
local pkgs_to_remove = {}
local installed = depends.get_installed(deploy_dir)
-- find packages to remove
if #package_names == 0 then
pkgs_to_remove = installed
else
pkgs_to_remove = depends.find_packages(package_names, installed)
end
-- remove them
for _, pkg in pairs(pkgs_to_remove) do
local pkg_distinfo_dir = sys.make_path(cfg.distinfos_dir, pkg.name .. "-" .. pkg.version)
local ok, err = package.remove_pkg(pkg_distinfo_dir, deploy_dir)
if not ok then return nil, err end
end
return #pkgs_to_remove
end
-- Download 'pkg_names' to 'fetch_dir' and return the table of their directories.
function fetch(pkg_names, fetch_dir)
fetch_dir = fetch_dir or sys.current_dir()
assert(type(pkg_names) == "table", "dist.fetch: Argument 'pkg_names' is not a string or table.")
assert(type(fetch_dir) == "string", "dist.fetch: Argument 'fetch_dir' is not a string.")
fetch_dir = sys.abs_path(fetch_dir)
local manifest = mf.get_manifest()
local pkgs_to_fetch = {}
for _, pkg_name in pairs(pkg_names) do
-- retrieve available versions
local versions, err = package.retrieve_versions(pkg_name, manifest)
if not versions then return nil, err end
for _, version in pairs(versions) do
table.insert(manifest, version)
end
local packages = depends.find_packages(pkg_name, manifest)
if #packages == 0 then return nil, "No packages found for '" .. pkg_name .. "'." end
packages = depends.sort_by_versions(packages)
table.insert(pkgs_to_fetch, packages[1])
end
local fetched_dirs = {}
for _, pkg in pairs(pkgs_to_fetch) do
local fetched_pkg, err = package.fetch_pkg(pkg, fetch_dir)
if not fetched_pkg then return nil, err end
table.insert(fetched_dirs, fetched_pkg.download_dir)
end
return fetched_dirs
end
-- Upload binary version of given modules installed in the specified
-- 'deploy_dir' to the repository specified by provided base url.
-- Return the number of uploaded packages.
--
-- Organization of uploaded modules and their repositories is subject
-- to the following conventions:
-- - destination repository is: 'DEST_GIT_BASE_URL/MODULE_NAME'
-- - module will be uploaded to the branch: 'ARCH-TYPE' according
-- to the arch and type of the user's machine
-- - the module will be tagged as: 'VERSION-ARCH-TYPE' (if the tag already
-- exists, it will be overwritten)
--
-- E.g. assume that the module 'lua-5.1.4' is installed on the 32bit Linux
-- system (Linux-i686). When this function is called with the module name
-- 'lua' and base url 'git@github.com:LuaDist', then the binary version
-- of the module 'lua', that is installed on the machine, will be uploaded
-- to the branch 'Linux-i686' of the repository 'git@github.com:LuaDist/lua.git'
-- and tagged as '5.1.4-Linux-i686'.
function upload_modules(deploy_dir, module_names, dest_git_base_url)
deploy_dir = deploy_dir or cfg.root_dir
if type(module_names) == "string" then module_names = {module_names} end
assert(type(deploy_dir) == "string", "dist.upload_module: Argument 'deploy_dir' is not a string.")
assert(type(module_names) == "table", "dist.upload_module: Argument 'module_name' is not a string or table.")
assert(type(dest_git_base_url) == "string", "dist.upload_module: Argument 'dest_git_base_url' is not a string.")
deploy_dir = sys.abs_path(deploy_dir)
local modules_to_upload = {}
local installed = depends.get_installed(deploy_dir)
-- find modules to upload
if #module_names == 0 then
modules_to_upload = installed
else
modules_to_upload = depends.find_packages(module_names, installed)
end
for _, installed_module in pairs(modules_to_upload) do
-- set names
local branch_name = cfg.arch .. "-" .. cfg.type
local tag_name = installed_module.version .. "-" .. branch_name
local full_name = installed_module.name .. "-" .. tag_name
local tmp_dir = sys.make_path(deploy_dir, cfg.temp_dir, full_name .. "-to-upload")
local dest_git_url = dest_git_base_url .. "/" .. installed_module.name .. ".git"
local distinfo_file = sys.make_path(deploy_dir, cfg.distinfos_dir, installed_module.name .. "-" .. installed_module.version, "dist.info")
-- create temporary directory (delete previous if already exists)
if sys.exists(tmp_dir) then sys.delete(tmp_dir) end
local ok, err = sys.make_dir(tmp_dir)
if not ok then return nil, err end
-- copy the module files for all enabled components
for _, component in ipairs(cfg.components) do
if installed_module.files[component] then
for _, file in ipairs(installed_module.files[component]) do
local file_path = sys.make_path(deploy_dir, file)
local dest_dir = sys.parent_dir(sys.make_path(tmp_dir, file))
if sys.is_file(file_path) then
sys.make_dir(dest_dir)
sys.copy(file_path, dest_dir)
end
end
end
end
-- add module's dist.info file
sys.copy(distinfo_file, tmp_dir)
-- create git repo
ok, err = git.init(tmp_dir)
if not ok then return nil, "Error initializing empty git repository in '" .. tmp_dir .. "': " .. err end
-- add all files
ok, err = git.add_all(tmp_dir)
if not ok then return nil, "Error adding all files to the git index in '" .. tmp_dir .. "': " .. err end
-- create commit
ok, err = git.commit("[luadist-git] add " .. full_name .. " [ci skip]", tmp_dir)
if not ok then return nil, "Error commiting changes in '" .. tmp_dir .. "': " .. err end
-- rename branch
ok, err = git.rename_branch("master", branch_name, tmp_dir)
if not ok then return nil, "Error renaming branch 'master' to '" .. branch_name .. "' in '" .. tmp_dir .. "': " .. err end
-- create tag
ok, err = git.create_tag(tmp_dir, tag_name)
if not ok then return nil, "Error creating tag '" .. tag_name .. "' in '" .. tmp_dir .. "': " .. err end
print("Uploading " .. full_name .. " to " .. dest_git_url .. "...")
-- push to the repository
ok, err = git.push_ref(tmp_dir, branch_name, dest_git_url, true)
if not ok then return nil, "Error when pushing branch '" .. branch_name .. "' and tag '" .. tag_name .. "' to '" .. dest_git_url .. "': " .. err end
-- delete temporary directory (if not in debug mode)
if not cfg.debug then sys.delete(tmp_dir) end
end
return #modules_to_upload
end
-- Returns table with information about module's dependencies, using the cache.
function dependency_info(module, deploy_dir)
cache_file = cache_file or sys.abs_path(sys.make_path(cfg.root_dir, cfg.dep_cache_file))
assert(type(module) == "string", "dist.dependency_info: Argument 'module' is not a string.")
assert(type(deploy_dir) == "string", "dist.dependency_info: Argument 'deploy_dir' is not a string.")
-- get manifest
local manifest, err = mf.get_manifest()
if not manifest then return nil, "Error getting manifest: " .. err end
-- get dependency manifest
-- TODO: Is it good that dep_manifest is deploy_dir-specific?
-- Probably it'd be better not to be specific, but then there're
-- problems with 'provides'. E.g. What to do if there's a module
-- installed, that is provided by two different modules in two deploy_dirs?
local dep_manifest_file = sys.abs_path(sys.make_path(deploy_dir, cfg.dep_cache_file))
local dep_manifest, status = {}
if sys.exists(dep_manifest_file) and cfg.cache and not utils.cache_timeout_expired(cfg.cache_timeout, dep_manifest_file) then
status, dep_manifest = mf.load_manifest(dep_manifest_file)
if not dep_manifest then return nil, status end
end
-- force getting the dependency information
local installed = {}
-- resolve dependencies
local dependencies, dep_manifest_or_err = depends.get_depends(module, installed, manifest, dep_manifest, deploy_dir, false, true and not cfg.debug)
if not dependencies then return nil, dep_manifest_or_err end
-- save updated dependency manifest
local ok, err = sys.make_dir(sys.parent_dir(dep_manifest_file))
if not ok then return nil, err end
ok, err = mf.save_manifest(dep_manifest_or_err, dep_manifest_file)
if not ok then return nil, err end
-- collect just relevant dependencies from dependency manifest
local relevant_deps = {}
for _, dep in pairs(dependencies) do
local name_ver = dep.name .. "-" .. (dep.was_scm_version and "scm" or dep.version)
if dep_manifest_or_err[name_ver] then
table.insert(relevant_deps, dep_manifest_or_err[name_ver])
else
return nil, "Error: dependency information for '" .. name_ver .. "' not found in dependency manifest."
end
end
return relevant_deps
end

View File

@ -0,0 +1,64 @@
-- Simple logger for LuaDist.
module ("dist.logger", package.seeall)
local cfg = require "dist.config"
local sys = require "dist.sys"
-- Open 'log_file' and return a log, or nil and error msg on error.
local function get_log(log_file)
log_file = log_file or cfg.log_file
assert(type(log_file) == "string", "log.get_log: Argument 'log_file' is not a string.")
log_file = sys.abs_path(log_file)
sys.make_dir(sys.parent_dir(log_file))
local log, err = io.open(log_file, "a")
if not log then
return nil, "Error: can't open a logfile '" .. log_file .. "': " .. err
else
return log
end
end
-- Set the default log.
local log_file = get_log(cfg.log_file)
-- Log levels used.
local log_levels = {
DEBUG = 0, -- Fine-grained informational events that are most useful to debug an application.
INFO = 1, -- Informational messages that highlight the progress of the application at coarse-grained level.
WARN = 2, -- Potentially harmful situations.
ERROR = 3, -- Error events that might still allow the application to continue running.
FATAL = 4, -- Very severe error events that would presumably lead the application to abort.
}
-- Write 'message' with 'level' to 'log'.
local function write(level, ...)
assert(type(level) == "string", "log.write: Argument 'level' is not a string.")
assert(#arg > 0, "log.write: No message arguments provided.")
assert(type(log_levels[level]) == "number", "log.write: Unknown log level used: '" .. level .. "'.")
level = level:upper()
local message = table.concat(arg, " ")
-- Check if writing for this log level is enabled.
if cfg.write_log_level and log_levels[level] >= log_levels[cfg.write_log_level] then
log_file:write(os.date("%Y-%m-%d %H:%M:%S") .. " [" .. level .. "]\t" .. message .. "\n")
log_file:flush()
end
-- Check if printing for this log level is enabled.
if cfg.print_log_level and log_levels[level] >= log_levels[cfg.print_log_level] then
print(message)
end
end
-- Functions with defined log levels for simple use.
function debug(...) return write("DEBUG", ...) end
function info(...) return write("INFO", ...) end
function warn(...) return write("WARN", ...) end
function error(...) return write("ERROR", ...) end
function fatal(...) return write("FATAL", ...) end
-- Function with explicitly specified log level.
function log(level, ...) return write(level, ...) end

View File

@ -0,0 +1,248 @@
-- Working with manifest and dist.info files
module ("dist.manifest", package.seeall)
local cfg = require "dist.config"
local git = require "dist.git"
local sys = require "dist.sys"
local utils = require "dist.utils"
-- Return the manifest table from 'manifest_file'. If the manifest is in cache,
-- then the cached version is used. You can set the cache timeout value in
-- 'config.cache_timeout' variable.
-- If optional 'force_no_cache' parameter is true, then the cache is not used.
function get_manifest(manifest_file, force_no_cache)
manifest_file = manifest_file or sys.make_path(cfg.root_dir, cfg.manifest_file)
force_no_cache = force_no_cache or false
assert(type(manifest_file) == "string", "manifest.get_manifest: Argument 'manifest_file' is not a string.")
assert(type(force_no_cache) == "boolean", "manifest.get_manifest: Argument 'force_no_cache' is not a boolean.")
manifest_file = sys.abs_path(manifest_file)
-- download new manifest to the cache if not present or cache not used or cache expired
if not sys.exists(manifest_file) or force_no_cache or not cfg.cache or utils.cache_timeout_expired(cfg.cache_timeout, manifest_file) then
local manifest_dest = sys.parent_dir(manifest_file) or sys.current_dir()
local ok, err = download_manifest(manifest_dest, cfg.repos)
if not ok then return nil, "Error when downloading manifest: " .. err end
end
-- load manifest from cache
local status, ret = load_manifest(manifest_file)
if not status then return nil, "Error when loading manifest: " .. ret end
return ret
end
-- Download manifest from the table of git 'repository_urls' to 'dest_dir' and return true on success
-- and nil and error message on error.
function download_manifest(dest_dir, repository_urls)
dest_dir = dest_dir or sys.make_path(cfg.root_dir, cfg.cache_dir)
repository_urls = repository_urls or cfg.repos
if type(repository_urls) == "string" then repository_urls = {repository_urls} end
assert(type(dest_dir) == "string", "manifest.download_manifest: Argument 'dest_dir' is not a string.")
assert(type(repository_urls) == "table", "manifest.download_manifest: Argument 'repository_urls' is not a table or string.")
dest_dir = sys.abs_path(dest_dir)
-- define used files and directories
local manifest_filename = sys.extract_name(cfg.manifest_file)
local manifest_file = sys.make_path(dest_dir, manifest_filename)
local temp_dir = sys.make_path(cfg.root_dir, cfg.temp_dir)
-- ensure that destination directory exists
local ok, err = sys.make_dir(dest_dir)
if not ok then return nil, err end
-- retrieve manifests from repositories and collect them into one manifest table
local manifest = {}
if #repository_urls == 0 then return nil, "No repository url specified." end
print("Downloading repository information...")
for k, repo in pairs(repository_urls) do
local clone_dir = sys.make_path(temp_dir, "repository_" .. tostring(k))
-- clone the repo and add its '.gitmodules' file to the manifest table
ok, err = git.create_repo(clone_dir)
local sha
if ok then sha, err = git.fetch_branch(clone_dir, repo, "master") end
if sha then ok, err = git.checkout_sha(sha, clone_dir) end
if not (ok and sha) then
if not cfg.debug then sys.delete(clone_dir) end
return nil, "Error when downloading the manifest from repository with url: '" .. repo .. "': " .. err
else
for _, pkg in pairs(load_gitmodules(sys.make_path(clone_dir, ".gitmodules"))) do
table.insert(manifest, pkg)
end
end
if not cfg.debug then sys.delete(clone_dir) end
end
-- save the new manifest table to the file
ok, err = save_manifest(manifest, manifest_file)
if not ok then return nil, err end
return true
end
-- A secure loadfile function
-- If file code chunk has upvalues, the first upvalue is set to the given
-- environement, if that parameter is given, or to the value of the global environment.
local function secure_loadfile(file, env)
assert(type(file) == "string", "secure_loadfile: Argument 'file' is not a string.")
-- use the given (or create a new) restricted environment
local env = env or {}
-- load the file and run in a protected call with the restricted env
-- setfenv is deprecated in lua 5.2 in favor of giving env in arguments
-- the additional loadfile arguments are simply ignored for previous lua versions
local f, err = loadfile(file, 'bt', env)
if f then
if setfenv ~= nil then
setfenv(f, env)
end
return pcall(f)
else
return nil, err
end
end
-- Load and return manifest table from the manifest file.
-- If manifest file not present, return nil.
function load_manifest(manifest_file)
manifest_file = manifest_file or sys.make_path(cfg.root_dir, cfg.manifest_file)
return secure_loadfile(sys.abs_path(manifest_file))
end
-- Load '.gitmodules' file and returns manifest table.
-- If the file is not present, return nil.
function load_gitmodules(gitmodules_file)
gitmodules_file = gitmodules_file or sys.make_path(cfg.root_dir, cfg.manifest_file)
assert(type(gitmodules_file) == "string", "manifest.load_gitmodules: Argument 'gitmodules_file' is not a string.")
gitmodules_file = sys.abs_path(gitmodules_file)
if sys.exists(gitmodules_file) then
-- load the .gitmodules file
local file, err = io.open(gitmodules_file, "r")
if not file then return nil, "Error when opening the .gitmodules file '" .. gitmodules_file .. "':" .. err end
local mf_text = file:read("*a")
file:close()
if not mf_text then return nil, "Error when reading the .gitmodules file '" .. gitmodules_file .. "':" .. err end
manifest = {}
for url in mf_text:gmatch("git://%S+/%S+") do
pkg = {name = url:match("git://%S+/(%S+)%.git") or url:match("git://%S+/(%S+)"), version = "scm", path = url}
table.insert(manifest, pkg)
end
return manifest
else
return nil, "Error when loading the .gitmodules: file '" .. gitmodules_file .. "' doesn't exist."
end
end
-- Save manifest table to the 'file'
function save_manifest(manifest_table, file)
assert(type(manifest_table) == "table", "manifest.save_distinfo: Argument 'manifest_table' is not a table.")
assert(type(file) == "string", "manifest.save_distinfo: Argument 'file' is not a string.")
file = sys.abs_path(file)
-- Print table 'tbl' to io stream 'file'.
local function print_table(file, tbl, in_nested_table)
for k, v in pairs(tbl) do
-- print key
if in_nested_table then file:write("\t\t") end
if type(k) ~= "number" then
file:write("['" .. k .. "']" .. " = ")
end
-- print value
if type(v) == "table" then
file:write("{\n")
print_table(file, v, true)
if in_nested_table then file:write("\t") end
file:write("\t}")
else
if in_nested_table then file:write("\t") end
if type(v) == "string" then
file:write('[[' .. v .. ']]')
else
file:write(tostring(v))
end
end
file:write(",\n")
end
end
local manifest_file = io.open(file, "w")
if not manifest_file then return nil, "Error when saving manifest: cannot open the file '" .. file .. "'." end
manifest_file:write('return {\n')
print_table(manifest_file, manifest_table)
manifest_file:write('},\ntrue')
manifest_file:close()
return true
end
-- Load and return package info table from the distinfo_file file.
-- If file not present, return nil.
function load_distinfo(distinfo_file)
assert(type(distinfo_file) == "string", "manifest.load_distinfo: Argument 'distinfo_file' is not a string.")
distinfo_file = sys.abs_path(distinfo_file)
-- load the distinfo file
local distinfo_env = {}
local status, ret = secure_loadfile(distinfo_file, distinfo_env)
if not status then return nil, "Error when loading package info: " .. ret end
return distinfo_env
end
-- Save distinfo table to the 'file'
function save_distinfo(distinfo_table, file)
assert(type(distinfo_table) == "table", "manifest.save_distinfo: Argument 'distinfo_table' is not a table.")
assert(type(file) == "string", "manifest.save_distinfo: Argument 'file' is not a string.")
file = sys.abs_path(file)
-- Print table 'tbl' to io stream 'file'.
local function print_table(file, tbl, in_nested_table)
for k, v in pairs(tbl) do
-- print key
if type(k) ~= "number" then
file:write(k .. " = ")
end
-- print value
if type(v) == "table" then
file:write("{\n")
print_table(file, v, true)
file:write("}\n")
elseif type(v) == "string" then
if in_nested_table then
file:write('[[' .. v .. ']]')
else
file:write('"' .. v .. '"')
end
else
file:write(v)
end
if in_nested_table then
file:write(",")
end
file:write("\n")
end
end
local distinfo_file = io.open(file, "w")
if not distinfo_file then return nil, "Error when saving dist-info table: cannot open the file '" .. file .. "'." end
print_table(distinfo_file, distinfo_table)
distinfo_file:close()
return true
end

View File

@ -0,0 +1,596 @@
-- Package functions
module ("dist.package", package.seeall)
local cfg = require "dist.config"
local git = require "dist.git"
local sys = require "dist.sys"
local mf = require "dist.manifest"
local utils = require "dist.utils"
local depends = require "dist.depends"
-- Return whether the package in given 'pkg_dir' is of a source type.
function is_source_type(pkg_dir)
assert(type(pkg_dir) == "string", "package.is_source_type: Argument 'pkg_dir' is not a string.")
pkg_dir = sys.abs_path(pkg_dir)
return utils.to_boolean(sys.exists(sys.make_path(pkg_dir, "CMakeLists.txt")))
end
-- Ensure proper arch and type for the given source 'dist_info' table and return it.
-- WARNING: this function should be used only for 'dist_info' tables of modules that are of a source type!
function ensure_source_arch_and_type(dist_info)
assert(type(dist_info) == "table", "package.ensure_source_arch_and_type: Argument 'dist_info' is not a table.")
dist_info.arch = dist_info.arch or "Universal"
dist_info.type = dist_info.type or "source"
return dist_info
end
-- Remove package from 'pkg_distinfo_dir' of 'deploy_dir'.
function remove_pkg(pkg_distinfo_dir, deploy_dir)
deploy_dir = deploy_dir or cfg.root_dir
assert(type(pkg_distinfo_dir) == "string", "package.remove_pkg: Argument 'pkg_distinfo_dir' is not a string.")
assert(type(deploy_dir) == "string", "package.remove_pkg: Argument 'deploy_dir' is not a string.")
deploy_dir = sys.abs_path(deploy_dir)
local abs_pkg_distinfo_dir = sys.make_path(deploy_dir, pkg_distinfo_dir)
-- check for 'dist.info'
local info, err = mf.load_distinfo(sys.make_path(abs_pkg_distinfo_dir, "dist.info"))
if not info then return nil, "Error removing package from '" .. pkg_distinfo_dir .. "' - it doesn't contain valid 'dist.info' file." end
if not info.files then return nil, "File '" .. sys.make_path(pkg_distinfo_dir, "dist.info") .."' doesn't contain list of installed files." end
-- remove files installed as components of this package
for _, component in ipairs(cfg.components) do
if info.files[component] then
for i = #info.files[component], 1, -1 do
local f = info.files[component][i]
f = sys.make_path(deploy_dir,f)
if sys.is_file(f) then
sys.delete(f)
elseif sys.is_dir(f) then
local dir_files, err = sys.get_file_list(f)
if not dir_files then return nil, "Error removing package in '" .. abs_pkg_distinfo_dir .. "': " .. err end
if #dir_files == 0 then sys.delete(f) end
end
-- delete also all parent directories if empty
local parents = sys.parents_up_to(f, deploy_dir)
for _, parent in ipairs(parents) do
if sys.is_dir(parent) then
local dir_files, err = sys.get_file_list(parent)
if not dir_files then return nil, "Error removing package in '" .. abs_pkg_distinfo_dir .. "': " .. err end
if #dir_files == 0 then
sys.delete(parent)
end
end
end
end
end
end
-- remove removed components also from 'dist.info'
for _, component in ipairs(cfg.components) do
info.files[component] = nil
end
-- delete the package information from deploy_dir
local ok = sys.delete(abs_pkg_distinfo_dir)
if not ok then return nil, "Error removing package in '" .. abs_pkg_distinfo_dir .. "'." end
-- if the package was not completely removed (e.g. some components remain),
-- save the new version of its 'dist.info'
local comp_num = 0
for _, _ in pairs(info.files) do comp_num = comp_num + 1 end
if comp_num ~= 0 then
sys.make_dir(abs_pkg_distinfo_dir)
local ok, err = mf.save_distinfo(info, sys.make_path(abs_pkg_distinfo_dir, "dist.info"))
if not ok then return nil, "Error resaving the 'dist.info': " .. err end
end
return ok
end
-- Install package from 'pkg_dir' to 'deploy_dir', using optional CMake 'variables'.
-- Optional 'preserve_pkg_dir' argument specified whether to preserve the 'pkg_dir'.
function install_pkg(pkg_dir, deploy_dir, variables, preserve_pkg_dir)
deploy_dir = deploy_dir or cfg.root_dir
variables = variables or {}
preserve_pkg_dir = preserve_pkg_dir or false
assert(type(pkg_dir) == "string", "package.install_pkg: Argument 'pkg_dir' is not a string.")
assert(type(deploy_dir) == "string", "package.install_pkg: Argument 'deploy_dir' is not a string.")
assert(type(variables) == "table", "package.install_pkg: Argument 'variables' is not a table.")
assert(type(preserve_pkg_dir) == "boolean", "package.install_pkg: Argument 'preserve_pkg_dir' is not a boolean.")
pkg_dir = sys.abs_path(pkg_dir)
deploy_dir = sys.abs_path(deploy_dir)
-- check for dist.info
local info, err = mf.load_distinfo(sys.make_path(pkg_dir, "dist.info"))
if not info then return nil, "Error installing: the directory '" .. pkg_dir .. "' doesn't exist or doesn't contain valid 'dist.info' file." end
-- check if the package is source
if is_source_type(pkg_dir) then info = ensure_source_arch_and_type(info) end
-- check package's architecture
if not (info.arch == "Universal" or info.arch == cfg.arch) then
return nil, "Error installing '" .. info.name .. "-" .. info.version .. "': architecture '" .. info.arch .. "' is not suitable for this machine."
end
-- check package's type
if not (info.type == "all" or info.type == "source" or info.type == cfg.type) then
return nil, "Error installing '" .. info.name .. "-" .. info.version .. "': architecture type '" .. info.type .. "' is not suitable for this machine."
end
local ok, err
-- if package is of binary type, just deploy it
if info.type ~= "source" then
ok, err = deploy_binary_pkg(pkg_dir, deploy_dir)
-- else build and then deploy
else
-- check if we have cmake
ok = utils.system_dependency_available("cmake", "cmake --version")
if not ok then return nil, "Error when installing: Command 'cmake' not available on the system." end
-- set cmake variables
local cmake_variables = {}
-- set variables from config file
for k, v in pairs(cfg.variables) do
cmake_variables[k] = v
end
-- set variables specified as argument
for k, v in pairs(variables) do
cmake_variables[k] = v
end
cmake_variables.CMAKE_INCLUDE_PATH = table.concat({cmake_variables.CMAKE_INCLUDE_PATH or "", sys.make_path(deploy_dir, "include")}, ";")
cmake_variables.CMAKE_LIBRARY_PATH = table.concat({cmake_variables.CMAKE_LIBRARY_PATH or "", sys.make_path(deploy_dir, "lib"), sys.make_path(deploy_dir, "bin")}, ";")
cmake_variables.CMAKE_PROGRAM_PATH = table.concat({cmake_variables.CMAKE_PROGRAM_PATH or "", sys.make_path(deploy_dir, "bin")}, ";")
-- build the package and deploy it
ok, err = build_pkg(pkg_dir, deploy_dir, cmake_variables)
if not ok then return nil, err end
end
-- delete directory of fetched package
if not (cfg.debug or preserve_pkg_dir) then sys.delete(pkg_dir) end
return ok, err
end
-- Build and deploy package from 'src_dir' to 'deploy_dir' using 'variables'.
-- Return directory to which the package was built or nil on error.
-- 'variables' is table of optional CMake variables.
function build_pkg(src_dir, deploy_dir, variables)
deploy_dir = deploy_dir or cfg.root_dir
variables = variables or {}
assert(type(src_dir) == "string", "package.build_pkg: Argument 'src_dir' is not a string.")
assert(type(deploy_dir) == "string", "package.build_pkg: Argument 'deploy_dir' is not a string.")
assert(type(variables) == "table", "package.build_pkg: Argument 'variables' is not a table.")
src_dir = sys.abs_path(src_dir)
deploy_dir = sys.abs_path(deploy_dir)
-- check for dist.info
local info, err = mf.load_distinfo(sys.make_path(src_dir, "dist.info"))
if not info then return nil, "Error building package from '" .. src_dir .. "': it doesn't contain valid 'dist.info' file." end
local pkg_name = info.name .. "-" .. info.version
-- set machine information
info.arch = cfg.arch
info.type = cfg.type
-- create CMake build dir
local cmake_build_dir = sys.abs_path(sys.make_path(deploy_dir, cfg.temp_dir, pkg_name .. "-CMake-build"))
sys.make_dir(cmake_build_dir)
-- create cmake cache
variables["CMAKE_INSTALL_PREFIX"] = deploy_dir
local cache_file = io.open(sys.make_path(cmake_build_dir, "cache.cmake"), "w")
if not cache_file then return nil, "Error creating CMake cache file in '" .. cmake_build_dir .. "'" end
-- Fill in cache variables
for k,v in pairs(variables) do
cache_file:write("SET(" .. k .. " " .. sys.quote(v):gsub("\\+", "/") .. " CACHE STRING \"\" FORCE)\n")
end
-- If user cache file is provided then append it
if cfg.cache_file ~= "" then
local user_cache = io.open(sys.abs_path(cfg.cache_file), "r")
if user_cache then
cache_file:write(user_cache:read("*all").."\n")
user_cache:close()
end
end
cache_file:close()
src_dir = sys.abs_path(src_dir)
print("Building " .. sys.extract_name(src_dir) .. "...")
-- set cmake cache command
local cache_command = cfg.cache_command
if cfg.debug then cache_command = cache_command .. " " .. cfg.cache_debug_options end
-- set cmake build command
local build_command = cfg.build_command
if cfg.debug then build_command = build_command .. " " .. cfg.build_debug_options end
-- set the cmake cache
local ok = sys.exec("cd " .. sys.quote(cmake_build_dir) .. " && " .. cache_command .. " " .. sys.quote(src_dir))
if not ok then return nil, "Error preloading the CMake cache script '" .. sys.make_path(cmake_build_dir, "cache.cmake") .. "'" end
-- build with cmake
ok = sys.exec("cd " .. sys.quote(cmake_build_dir) .. " && " .. build_command)
if not ok then return nil, "Error building with CMake in directory '" .. cmake_build_dir .. "'" end
-- if this is only simulation, exit sucessfully, skipping the next actions
if cfg.simulate then
return true, "Simulated build and deployment of package '" .. pkg_name .. "' sucessfull."
end
-- table to collect files installed in the components
info.files = {}
-- install the components
for _, component in ipairs(cfg.components) do
local strip_option = ""
if not cfg.debug and component ~= "Library" then strip_option = cfg.strip_option end
local ok = sys.exec("cd " .. sys.quote(cmake_build_dir) .. " && " .. cfg.cmake .. " " .. strip_option .. " " ..cfg.install_component_command:gsub("#COMPONENT#", component))
if not ok then return nil, "Error when installing the component '" .. component .. "' with CMake in directory '" .. cmake_build_dir .. "'" end
local install_mf = sys.make_path(cmake_build_dir, "install_manifest_" .. component .. ".txt")
local mf, err
local component_files = {}
-- collect files installed in this component
if sys.exists(install_mf) then
mf, err = io.open(install_mf, "r")
if not mf then return nil, "Error when opening the CMake installation manifest '" .. install_mf .. "': " .. err end
for line in mf:lines() do
line = sys.check_separators(line)
local file = line:gsub(utils.escape_magic(deploy_dir .. sys.path_separator()), "")
table.insert(component_files, file)
end
mf:close()
-- add list of component files to the 'dist.info'
if #component_files > 0 then info.files[component] = component_files end
end
end
-- if bookmark == 0 then return nil, "Package did not install any files!" end
-- test with ctest
if cfg.test then
print("Testing " .. sys.extract_name(src_dir) .. " ...")
ok = sys.exec("cd " .. sys.quote(deploy_dir) .. " && " .. cfg.test_command)
if not ok then return nil, "Error when testing the module '" .. pkg_name .. "' with CTest." end
end
-- save modified 'dist.info' file
local pkg_distinfo_dir = sys.make_path(deploy_dir, cfg.distinfos_dir, pkg_name)
sys.make_dir(pkg_distinfo_dir)
ok, err = mf.save_distinfo(info, sys.make_path(pkg_distinfo_dir, "dist.info"))
if not ok then return nil, err end
-- clean up
if not cfg.debug then sys.delete(cmake_build_dir) end
return true, "Package '" .. pkg_name .. "' successfully builded and deployed to '" .. deploy_dir .. "'."
end
-- Deploy binary package from 'pkg_dir' to 'deploy_dir' by copying.
function deploy_binary_pkg(pkg_dir, deploy_dir)
deploy_dir = deploy_dir or cfg.root_dir
assert(type(pkg_dir) == "string", "package.deploy_binary_pkg: Argument 'pkg_dir' is not a string.")
assert(type(deploy_dir) == "string", "package.deploy_binary_pkg: Argument 'deploy_dir' is not a string.")
pkg_dir = sys.abs_path(pkg_dir)
deploy_dir = sys.abs_path(deploy_dir)
-- check for dist.info
local info, err = mf.load_distinfo(sys.make_path(pkg_dir, "dist.info"))
if not info then return nil, "Error deploying package from '" .. pkg_dir .. "': it doesn't contain valid 'dist.info' file." end
local pkg_name = info.name .. "-" .. info.version
-- if this is only simulation, exit sucessfully, skipping the next actions
if cfg.simulate then
return true, "Simulated deployment of package '" .. pkg_name .. "' sucessfull."
end
-- copy all components of the module to the deploy_dir
for _, component in ipairs(cfg.components) do
if info.files[component] then
for _, file in ipairs(info.files[component]) do
local dest_dir = sys.make_path(deploy_dir, sys.parent_dir(file))
local ok, err = sys.make_dir(dest_dir)
if not ok then return nil, "Error when deploying package '" .. pkg_name .. "': cannot create directory '" .. dest_dir .. "': " .. err end
ok, err = sys.copy(sys.make_path(pkg_dir, file), dest_dir)
if not ok then return nil, "Error when deploying package '" .. pkg_name .. "': cannot copy file '" .. file .. "' to the directory '" .. dest_dir .. "': " .. err end
end
end
end
-- copy dist.info to register the module as installed
local pkg_distinfo_dir = sys.make_path(deploy_dir, cfg.distinfos_dir, pkg_name)
sys.make_dir(pkg_distinfo_dir)
ok, err = mf.save_distinfo(info, sys.make_path(pkg_distinfo_dir, "dist.info"))
if not ok then return nil, err end
return true, "Package '" .. pkg_name .. "' successfully deployed to '" .. deploy_dir .. "'."
end
-- Fetch package (table 'pkg') to download_dir. Return the original 'pkg' table
-- with 'pkg.download_dir' containing path to the directory of the
-- downloaded package.
--
-- When optional 'suppress_printing' parameter is set to true, then messages
-- for the user won't be printed during run of this function.
--
-- If the 'pkg' already contains the information about download directory (pkg.download_dir),
-- we assume the package was already downloaded there and won't download it again.
function fetch_pkg(pkg, download_dir, suppress_printing)
download_dir = download_dir or sys.current_dir()
suppress_printing = suppress_printing or false
assert(type(pkg) == "table", "package.fetch_pkg: Argument 'pkg' is not a table.")
assert(type(download_dir) == "string", "package.fetch_pkg: Argument 'download_dir' is not a string.")
assert(type(suppress_printing) == "boolean", "package.fetch_pkg: Argument 'suppress_printing' is not a boolean.")
assert(type(pkg.name) == "string", "package.fetch_pkg: Argument 'pkg.name' is not a string.")
assert(type(pkg.version) == "string", "package.fetch_pkg: Argument 'pkg.version' is not a string.")
-- if the package is already downloaded don't download it again
if pkg.download_dir then return pkg end
assert(type(pkg.path) == "string", "package.fetch_pkg: Argument 'pkg.path' is not a string.")
download_dir = sys.abs_path(download_dir)
local pkg_full_name = pkg.name .. "-" .. pkg.version
local repo_url = pkg.path
local clone_dir = sys.abs_path(sys.make_path(download_dir, pkg_full_name))
pkg.download_dir = clone_dir
-- check if download_dir already exists, assuming the package was already downloaded
if sys.exists(sys.make_path(clone_dir, "dist.info")) then
if cfg.cache and not utils.cache_timeout_expired(cfg.cache_timeout, clone_dir) then
if not suppress_printing then print("'" .. pkg_full_name .. "' already in cache, skipping downloading (use '-cache=false' to force download).") end
return pkg
else
sys.delete(sys.make_path(clone_dir))
end
end
local bin_tag = pkg.version .. "-" .. cfg.arch .. "-" .. cfg.type
local use_binary = false
if cfg.binary then
-- check if binary version of the module for this arch & type available
local avail_tags, err = git.get_remote_tags(repo_url)
if not avail_tags then return nil, err end
if utils.contains(avail_tags, bin_tag) then
use_binary = true
end
end
-- init the git repository
local ok, err = git.create_repo(clone_dir)
if not ok then return nil, err end
-- Fetch the desired ref (from the pkg's remote repo) and checkout into it.
if use_binary then
if not suppress_printing then print("Getting " .. pkg_full_name .. " (binary)...") end
-- We fetch the binary tag.
local sha
if ok then sha, err = git.fetch_tag(clone_dir, repo_url, bin_tag) end
if sha then ok, err = git.checkout_sha(sha, clone_dir) end
elseif cfg.source then
if not suppress_printing then print("Getting " .. pkg_full_name .. " (source)...") end
-- If we want the 'scm' version, we fetch the 'master' branch, otherwise
-- we fetch the tag, matching the desired package version.
if ok and pkg.version ~= "scm" then
local sha
sha, err = git.fetch_tag(clone_dir, repo_url, pkg.version)
if sha then ok, err = git.checkout_sha(sha, clone_dir) end
elseif ok then
local sha
sha, err = git.fetch_branch(clone_dir, repo_url, "master")
if sha then ok, err = git.checkout_sha(sha, clone_dir) end
end
else
ok = false
if cfg.binary then
err = "Binary version of module not available and using source modules disabled."
else
err = "Using both binary and source modules disabled."
end
end
if not ok then
-- clean up
if not cfg.debug then sys.delete(clone_dir) end
return nil, "Error fetching package '" .. pkg_full_name .. "' from '" .. pkg.path .. "' to '" .. download_dir .. "': " .. err
end
-- delete '.git' directory
if not cfg.debug then sys.delete(sys.make_path(clone_dir, ".git")) end
return pkg
end
-- Return table with information about available versions of 'package'.
--
-- When optional 'suppress_printing' parameter is set to true, then messages
-- for the user won't be printed during run of this function.
function retrieve_versions(package, manifest, suppress_printing)
suppress_printing = suppress_printing or false
assert(type(package) == "string", "package.retrieve_versions: Argument 'string' is not a string.")
assert(type(manifest) == "table", "package.retrieve_versions: Argument 'manifest' is not a table.")
assert(type(suppress_printing) == "boolean", "package.retrieve_versions: Argument 'suppress_printing' is not a boolean.")
-- get package table
local pkg_name = depends.split_name_constraint(package)
local tmp_packages = depends.find_packages(pkg_name, manifest)
if #tmp_packages == 0 then
return nil, "No suitable candidate for package '" .. package .. "' found."
else
package = tmp_packages[1]
end
-- if the package's already downloaded, we assume it's desired to install the downloaded version
if package.download_dir then
local pkg_type = "binary"
if is_source_type(package.download_dir) then pkg_type = "source" end
if not suppress_printing then print("Using " .. package.name .. "-" .. package.version .. " (" .. pkg_type .. ") provided by " .. package.download_dir) end
return {package}
end
if not suppress_printing then print("Finding out available versions of " .. package.name .. "...") end
-- get available versions
local tags, err = git.get_remote_tags(package.path)
if not tags then return nil, "Error when retrieving versions of package '" .. package.name .. "': " .. err end
-- filter out tags of binary packages
local versions = utils.filter(tags, function (tag) return tag:match("^[^%-]+%-?[^%-]*$") and true end)
packages = {}
-- create package information
for _, version in pairs(versions) do
pkg = {}
pkg.name = package.name
pkg.version = version
pkg.path = package.path
table.insert(packages, pkg)
end
return packages
end
-- Return table with information from package's dist.info and path to downloaded
-- package. Optional argument 'deploy_dir' is used just as a temporary
-- place to place the downloaded packages into.
--
-- When optional 'suppress_printing' parameter is set to true, then messages
-- for the user won't be printed during the execution of this function.
function retrieve_pkg_info(package, deploy_dir, suppress_printing)
deploy_dir = deploy_dir or cfg.root_dir
assert(type(package) == "table", "package.retrieve_pkg_info: Argument 'package' is not a table.")
assert(type(deploy_dir) == "string", "package.retrieve_pkg_info: Argument 'deploy_dir' is not a string.")
deploy_dir = sys.abs_path(deploy_dir)
local tmp_dir = sys.abs_path(sys.make_path(deploy_dir, cfg.temp_dir))
-- download the package
local fetched_pkg, err = fetch_pkg(package, tmp_dir, suppress_printing)
if not fetched_pkg then return nil, "Error when retrieving the info about '" .. package.name .. "': " .. err end
-- load information from 'dist.info'
local info, err = mf.load_distinfo(sys.make_path(fetched_pkg.download_dir, "dist.info"))
if not info then return nil, err end
-- add other attributes
if package.path then info.path = package.path end
if package.was_scm_version then info.was_scm_version = package.was_scm_version end
-- set default arch/type if not explicitly stated and package is of source type
if is_source_type(fetched_pkg.download_dir) then
info = ensure_source_arch_and_type(info)
elseif not (info.arch and info.type) then
return nil, fetched_pkg.download_dir .. ": binary package missing arch or type in 'dist.info'."
end
return info, fetched_pkg.download_dir
end
-- Return manifest, augmented with info about all available versions
-- of package 'pkg'. Optional argument 'deploy_dir' is used just as a temporary
-- place to place the downloaded packages into.
-- Optional argument 'installed' is manifest of all installed packages. When
-- specified, info from installed packages won't be downloaded from repo,
-- but the dist.info from installed package will be used.
function get_versions_info(pkg, manifest, deploy_dir, installed)
deploy_dir = deploy_dir or cfg.root_dir
assert(type(pkg) == "string", "package.get_versions_info: Argument 'pkg' is not a string.")
assert(type(manifest) == "table", "package.get_versions_info: Argument 'manifest' is not a table.")
assert(type(deploy_dir) == "string", "package.get_versions_info: Argument 'deploy_dir' is not a string.")
deploy_dir = sys.abs_path(deploy_dir)
-- find all available versions of package
local versions, err = retrieve_versions(pkg, manifest)
if not versions then return nil, err end
-- collect info about all retrieved versions
local infos = {}
for _, version in pairs(versions) do
local info, path_or_err
local installed_version = {}
-- find out whether this 'version' is installed so we can use it's dist.info
if type(installed) == "table" then installed_version = depends.find_packages(version.name .. "-" .. version.version, installed) end
-- get info
if #installed_version > 0 then
print("Using dist.info from installed " .. version.name .. "-" .. version.version)
info = installed_version[1]
info.path = version.path
info.from_installed = true -- flag that dist.info of installed package was used
else
info, path_or_err = retrieve_pkg_info(version, deploy_dir)
if not info then return nil, path_or_err end
sys.delete(path_or_err)
end
table.insert(infos, info)
end
-- found and add an implicit 'scm' version
local pkg_name = depends.split_name_constraint(pkg)
local found = depends.find_packages(pkg_name, manifest)
if #found == 0 then return nil, "No suitable candidate for package '" .. pkg .. "' found." end
local scm_info, path_or_err = retrieve_pkg_info({name = pkg_name, version = "scm", path = found[1].path})
if not scm_info then return nil, path_or_err end
sys.delete(path_or_err)
scm_info.version = "scm"
table.insert(infos, scm_info)
local tmp_manifest = utils.deepcopy(manifest)
-- add collected info to the temp. manifest, replacing existing tables
for _, info in pairs(infos) do
local already_in_manifest = false
-- find if this version is already in manifest
for idx, pkg in ipairs(tmp_manifest) do
-- if yes, replace it
if pkg.name == info.name and pkg.version == info.version then
tmp_manifest[idx] = info
already_in_manifest = true
break
end
end
-- if not, just normally add to the manifest
if not already_in_manifest then
table.insert(tmp_manifest, info)
end
end
return tmp_manifest
end

View File

@ -0,0 +1,386 @@
-- System functions
module ("dist.sys", package.seeall)
local cfg = require "dist.config"
local utils = require "dist.utils"
local lfs = require "lfs"
-- Return the path separator according to the platform.
function path_separator()
if cfg.arch == "Windows" then
return "\\"
else
return "/"
end
end
-- Return path with wrong separators replaced with the right ones.
function check_separators(path)
assert(type(path) == "string", "sys.check_separators: Argument 'path' is not a string.")
if cfg.arch == "Windows" then
return path:gsub("/", "\\")
else
return path
end
end
-- Return the path with the unnecessary trailing separator removed.
function remove_trailing(path)
assert(type(path) == "string", "sys.remove_trailing: Argument 'path' is not a string.")
if path:sub(-1) == path_separator() and not is_root(path) then path = path:sub(1,-2) end
return path
end
-- Return the path with the all occurences of '/.' or '\.' (representing
-- the current directory) removed.
function remove_curr_dir_dots(path)
assert(type(path) == "string", "sys.remove_curr_dir_dots: Argument 'path' is not a string.")
while path:match(path_separator() .. "%." .. path_separator()) do -- match("/%./")
path = path:gsub(path_separator() .. "%." .. path_separator(), path_separator()) -- gsub("/%./", "/")
end
return path:gsub(path_separator() .. "%.$", "") -- gsub("/%.$", "")
end
-- Return string argument quoted for a command line usage.
function quote(argument)
assert(type(argument) == "string", "sys.quote: Argument 'argument' is not a string.")
-- TODO: This seems like a not very nice hack. Why is it needed?
-- Wouldn't it be better to fix the problem where it originates?
-- replace '/' path separators for '\' on Windows
if cfg.arch == "Windows" and argument:match("^[%u%U.]?:?[/\\].*") then
argument = argument:gsub("//","\\"):gsub("/","\\")
end
-- Windows doesn't recognize paths starting with two slashes or backslashes
-- so we double every backslash except for the first one
if cfg.arch == "Windows" and argument:match("^[/\\].*") then
local prefix = argument:sub(1,1)
argument = argument:sub(2):gsub("\\", "\\\\")
argument = prefix .. argument
else
argument = argument:gsub("\\", "\\\\")
end
argument = argument:gsub('"', '\\"')
return '"' .. argument .. '"'
end
-- Run the system command (in current directory).
-- Return true on success, nil on fail and log string.
-- When optional 'force_verbose' parameter is true, then the output will be shown
-- even when not in debug or verbose mode.
function exec(command, force_verbose)
force_verbose = force_verbose or false
assert(type(command) == "string", "sys.exec: Argument 'command' is not a string.")
assert(type(force_verbose) == "boolean", "sys.exec: Argument 'force_verbose' is not a boolean.")
if not (cfg.verbose or cfg.debug or force_verbose) then
if cfg.arch == "Windows" then
command = command .. " > NUL 2>&1"
else
command = command .. " > /dev/null 2>&1"
end
end
if cfg.debug then print("Executing the command: " .. command) end
local ok, str, status = os.execute(command)
-- os.execute returned values on failure are:
-- nil or true, "exit", n or true, "signal", n for lua >= 5.2
-- status ~= 0 for lua 5.x < 5.2
if ok == nil or (str == "exit" and status ~= 0) or str == "signal" or (ok ~= 0 and ok ~= true) then
return nil, "Error when running the command: " .. command
else
return true, "Sucessfully executed the command: " .. command
end
end
-- Execute the 'command' and returns its output as a string.
function capture_output(command)
assert(type(command) == "string", "sys.exec: Argument 'command' is not a string.")
local executed, err = io.popen(command, "r")
if not executed then return nil, "Error running the command '" .. command .. "':" .. err end
local captured, err = executed:read("*a")
if not captured then return nil, "Error reading the output of command '" .. command .. "':" .. err end
executed:close()
return captured
end
-- Return whether the path is a root.
function is_root(path)
assert(type(path) == "string", "sys.is_root: Argument 'path' is not a string.")
return utils.to_boolean(path:find("^[a-zA-Z]:[/\\]$") or path:find("^[/\\]$"))
end
-- Return whether the path is absolute.
function is_abs(path)
assert(type(path) == "string", "sys.is_abs: Argument 'path' is not a string.")
return utils.to_boolean(path:find("^[a-zA-Z]:[/\\].*$") or path:find("^[/\\].*$"))
end
-- Return whether the specified file or directory exists.
function exists(path)
assert(type(path) == "string", "sys.exists: Argument 'path' is not a string.")
local attr, err = lfs.attributes(path)
return utils.to_boolean(attr), err
end
-- Return whether the 'file' exists and is a file.
function is_file(file)
assert(type(file) == "string", "sys.is_file: Argument 'file' is not a string.")
return lfs.attributes(file, "mode") == "file"
end
-- Return whether the 'dir' exists and is a directory.
function is_dir(dir)
assert(type(dir) == "string", "sys.is_dir: Argument 'dir' is not a string.")
return lfs.attributes(dir, "mode") == "directory"
end
-- Return the current working directory
function current_dir()
local dir, err = lfs.currentdir()
if not dir then return nil, err end
return dir
end
-- Return an iterator over the directory 'dir'.
-- If 'dir' doesn't exist or is not a directory, return nil and error message.
function get_directory(dir)
dir = dir or current_dir()
assert(type(dir) == "string", "sys.get_directory: Argument 'dir' is not a string.")
if is_dir(dir) then
return lfs.dir(dir)
else
return nil, "Error: '".. dir .. "' is not a directory."
end
end
-- Extract file or directory name from its path.
function extract_name(path)
assert(type(path) == "string", "sys.extract_name: Argument 'path' is not a string.")
if is_root(path) then return path end
path = remove_trailing(path)
path = path:gsub("^.*" .. path_separator(), "")
return path
end
-- Return parent directory of the 'path' or nil if there's no parent directory.
-- If 'path' is a path to file, return the directory the file is in.
function parent_dir(path)
assert(type(path) == "string", "sys.parent_dir: Argument 'path' is not a string.")
path = remove_curr_dir_dots(path)
path = remove_trailing(path)
local dir = path:gsub(utils.escape_magic(extract_name(path)) .. "$", "")
if dir == "" then
return nil
else
return make_path(dir)
end
end
-- Returns the table of all parent directories of 'path' up to the directory
-- specified by 'boundary_path' (exclusive).
function parents_up_to(path, boundary_path)
assert(type(path) == "string", "sys.parents_up_to: Argument 'path' is not a string.")
assert(type(boundary_path) == "string", "sys.parents_up_to: Argument 'boundary_path' is not a string.")
boundary_path = remove_trailing(boundary_path)
-- helper function to recursively collect the parent directories
local function collect_parents(_path, _parents)
local _parent = parent_dir(_path)
if _parent and _parent ~= boundary_path then
table.insert(_parents, _parent)
return collect_parents(_parent, _parents)
else
return _parents
end
end
return collect_parents(path, {})
end
-- Compose path composed from specified parts or current
-- working directory when no part specified.
function make_path(...)
-- arg is deprecated in lua 5.2 in favor of table.pack we mimic here
local arg = {n=select('#',...),...}
local parts = arg
assert(type(parts) == "table", "sys.make_path: Argument 'parts' is not a table.")
local path, err
if parts.n == 0 then
path, err = current_dir()
else
path, err = table.concat(parts, path_separator())
end
if not path then return nil, err end
-- squeeze repeated occurences of a file separator
path = path:gsub(path_separator() .. "+", path_separator())
-- remove unnecessary trailing path separator
path = remove_trailing(path)
return path
end
-- Return absolute path from 'path'
function abs_path(path)
assert(type(path) == "string", "sys.get_abs_path: Argument 'path' is not a string.")
if is_abs(path) then return path end
local cur_dir, err = current_dir()
if not cur_dir then return nil, err end
return make_path(cur_dir, path)
end
-- Returns path to the temporary directory of OS.
function tmp_dir()
return os.getenv("TMPDIR") or os.getenv("TEMP") or os.getenv("TMP") or "/tmp"
end
-- Returns temporary file (or directory) path (with optional prefix).
function tmp_name(prefix)
prefix = prefix or ""
assert(type(prefix) == "string", "sys.tmp_name: Argument 'prefix' is not a string.")
return make_path(tmp_dir(), prefix .. "luadist_" .. utils.rand(10000000000))
end
-- Return table of all paths in 'dir'
function get_file_list(dir)
dir = dir or current_dir()
assert(type(dir) == "string", "sys.get_directory: Argument 'dir' is not a string.")
if not exists(dir) then return nil, "Error getting file list of '" .. dir .. "': directory doesn't exist." end
local function collect(path, all_paths)
for item in get_directory(path) do
local item_path = make_path(path, item)
local _, last = item_path:find(dir .. path_separator(), 1, true)
local path_to_insert = item_path:sub(last + 1)
if is_file(item_path) then
table.insert(all_paths, path_to_insert)
elseif is_dir(item_path) and item ~= "." and item ~= ".." then
table.insert(all_paths, path_to_insert)
collect(item_path, all_paths)
end
end
end
local all_paths = {}
collect(dir, all_paths)
return all_paths
end
-- Return time of the last modification of 'file'.
function last_modification_time(file)
assert(type(file) == "string", "sys.last_modification_time: Argument 'file' is not a string.")
return lfs.attributes(file, "modification")
end
-- Return the current time (in seconds since epoch).
function current_time()
return os.time()
end
-- Change the current working directory and return 'true' and previous working
-- directory on success and 'nil' and error message on error.
function change_dir(dir_name)
assert(type(dir_name) == "string", "sys.change_dir: Argument 'dir_name' is not a string.")
local prev_dir = current_dir()
local ok, err = lfs.chdir(dir_name)
if ok then
return ok, prev_dir
else
return nil, err
end
end
-- Make a new directory, making also all of its parent directories that doesn't exist.
function make_dir(dir_name)
assert(type(dir_name) == "string", "sys.make_dir: Argument 'dir_name' is not a string.")
if exists(dir_name) then
return true
else
local par_dir = parent_dir(dir_name)
if par_dir then
local ok, err = make_dir(par_dir)
if not ok then return nil, err end
end
return lfs.mkdir(dir_name)
end
end
-- Move file (or directory) to the destination directory
function move_to(file_or_dir, dest_dir)
assert(type(file_or_dir) == "string", "sys.move_to: Argument 'file_or_dir' is not a string.")
assert(type(dest_dir) == "string", "sys.move_to: Argument 'dest_dir' is not a string.")
assert(is_dir(dest_dir), "sys.move_to: Destination '" .. dest_dir .."' is not a directory.")
-- Extract file/dir name from its path
local file_or_dir_name = extract_name(file_or_dir)
return os.rename(file_or_dir, make_path(dest_dir, file_or_dir_name))
end
-- rename file (or directory) to the new name.
function rename(file, new_name)
assert(type(file) == "string", "sys.rename: Argument 'file' is not a string.")
assert(type(new_name) == "string", "sys.rename: Argument 'new_name' is not a string.")
assert(not exists(new_name), "sys.rename: desired filename already exists.")
return os.rename(file, new_name)
end
-- Copy 'source' to the destination directory 'dest_dir'.
-- If 'source' is a directory, then recursive copying is used.
-- For non-recursive copying of directories use the make_dir() function.
function copy(source, dest_dir)
assert(type(source) == "string", "sys.copy: Argument 'file_or_dir' is not a string.")
assert(type(dest_dir) == "string", "sys.copy: Argument 'dest_dir' is not a string.")
assert(is_dir(dest_dir), "sys.copy: destination '" .. dest_dir .."' is not a directory.")
if cfg.arch == "Windows" then
if is_dir(source) then
make_dir(make_path(dest_dir, extract_name(source)))
return exec("xcopy /E /I /Y /Q " .. quote(source) .. " " .. quote(dest_dir .. "\\" .. extract_name(source)))
else
return exec("copy /Y " .. quote(source) .. " " .. quote(dest_dir))
end
else
if is_dir(source) then
return exec("cp -fRH " .. quote(source) .. " " .. quote(dest_dir))
else
return exec("cp -fH " .. quote(source) .. " " .. quote(dest_dir))
end
end
end
-- Delete the specified file or directory
function delete(path)
assert(type(path) == "string", "sys.delete: Argument 'path' is not a string.")
assert(is_abs(path), "sys.delete: Argument 'path' is not an absolute path.")
if cfg.arch == "Windows" then
if not exists(path) then
return true
elseif is_file(path) then
return os.remove(path)
else
return exec("rd /S /Q " .. quote(path))
end
else
return exec("rm -rf " .. quote(path))
end
end

View File

@ -0,0 +1,151 @@
-- System functions
module ("dist.utils", package.seeall)
local sys = require "dist.sys"
-- Returns a deep copy of 'table' with reference to the same metadata table.
-- Source: http://lua-users.org/wiki/CopyTable
function deepcopy(object)
local lookup_table = {}
local function _copy(object)
if type(object) ~= "table" then
return object
elseif lookup_table[object] then
return lookup_table[object]
end
local new_table = {}
lookup_table[object] = new_table
for index, value in pairs(object) do
new_table[_copy(index)] = _copy(value)
end
return setmetatable(new_table, getmetatable(object))
end
return _copy(object)
end
-- Return deep copy of table 'array', containing only items for which 'predicate_fn' returns true.
function filter(array, predicate_fn)
assert(type(array) == "table", "utils.filter: Argument 'array' is not a table.")
assert(type(predicate_fn) == "function", "utils.filter: Argument 'predicate_fn' is not a function.")
local filtered = {}
for _,v in pairs(array) do
if predicate_fn(v) == true then table.insert(filtered, deepcopy(v)) end
end
return filtered
end
-- Return deep copy of table 'array', sorted according to the 'compare_fn' function.
function sort(array, compare_fn)
assert(type(array) == "table", "utils.sort: Argument 'array' is not a table.")
assert(type(compare_fn) == "function", "utils.sort: Argument 'compare_fn' is not a function.")
local sorted = deepcopy(array)
table.sort(sorted, compare_fn)
return sorted
end
-- Return whether the 'value' is in the table 'tbl'.
function contains(tbl, value)
assert(type(tbl) == "table", "utils.contains: Argument 'tbl' is not a table.")
for _,v in pairs(tbl) do
if v == value then return true end
end
return false
end
-- Return single line string consisting of values in 'tbl' separated by comma.
-- Used for printing the dependencies/provides/conflicts.
function table_tostring(tbl, label)
assert(type(tbl) == "table", "utils.table_tostring: Argument 'tbl' is not a table.")
local str = ""
for k,v in pairs(tbl) do
if type(v) == "table" then
str = str .. table_tostring(v, k)
else
if label ~= nil then
str = str .. tostring(v) .. " [" .. tostring(label) .. "]" .. ", "
else
str = str .. tostring(v) .. ", "
end
end
end
return str
end
-- Return table made up from values of the string, separated by separator.
function make_table(str, separator)
assert(type(str) == "string", "utils.make_table: Argument 'str' is not a string.")
assert(type(separator) == "string", "utils.make_table: Argument 'separator' is not a string.")
local tbl = {}
for val in str:gmatch("(.-)" .. separator) do
table.insert(tbl, val)
end
local last_val = str:gsub(".-" .. separator, "")
if last_val and last_val ~= "" then
table.insert(tbl, last_val)
end
return tbl
end
-- Return whether the 'cache_timeout' for 'file' has expired.
function cache_timeout_expired(cache_timeout, file)
assert(type(cache_timeout) == "number", "utils.cache_timeout_expired: Argument 'cache_timeout' is not a number.")
assert(type(file) == "string", "utils.cache_timeout_expired: Argument 'file' is not a string.")
return sys.last_modification_time(file) + cache_timeout < sys.current_time()
end
-- Return the string 'str', with all magic (pattern) characters escaped.
function escape_magic(str)
assert(type(str) == "string", "utils.escape: Argument 'str' is not a string.")
local escaped = str:gsub('[%-%.%+%[%]%(%)%^%%%?%*%^%$]','%%%1')
return escaped
end
-- Return the boolean representation of an 'arg'.
function to_boolean(arg)
return not not arg
end
math.randomseed(os.time())
-- Return pseudo-random number in range [0, 1], [1, n] or [n, m].
function rand(...)
return math.random(...)
end
-- Perform check of system dependency, which isn't provided in the LuaDist
-- installation itself and if it is missing, print instructions how
-- to install it. The 'command' is used for testing, 'name' when printing
-- information to the user.
function system_dependency_available(name, command)
assert(type(name) == "string", "utils.system_dependency_available: Argument 'name' is not a string.")
assert(type(command) == "string", "utils.system_dependency_available: Argument 'command' is not a string.")
if not sys.exec(command) then
print("Error: command '" .. name .. "' not found on system. See installation instructions at\nhttps://github.com/LuaDist/Repository/wiki/Installation-of-System-Dependencies")
return false
end
return true
end
-- Obtain LuaDist location by checking available package locations
function get_luadist_location()
local paths = {}
local path = package.path:gsub("([^;]+)", function(c) table.insert(paths, c) end)
for _, path in pairs(paths) do
if (sys.is_abs(path) and path:find("[/\\]lib[/\\]lua[/\\]%?.lua$")) then
-- Remove path to lib/lua
path = path:gsub("[/\\]lib[/\\]lua[/\\]%?.lua$", "")
-- Clean the path up a bit
path = path:gsub("[/\\]bin[/\\]%.[/\\]%.%.", "")
path = path:gsub("[/\\]bin[/\\]%.%.", "")
return path
end
end
return nil
end