--- LuaDist Primary API functions -- Peter Drahos, Peter Kapec, LuaDist Project, 2008 module ("dist", package.seeall) local manifest = require "dist.manifest" local package = require "dist.package" local persist = require "dist.persist" local config = require "dist.config" local sys = require "dist.sys" local dep = require "dist.dep" --- Get contents of all repositories. -- @param repos string or table: Repository to look in for packages or list of repositories. Accepts Curl recognized URLs. -- @return table, err: Table of tables repositories/packages/versions/architectures/types where keys are strings and values are subtables. On failure nil and error message are returned. function getRepositories(urls) local function mygetRepositories(urls) if not urls then urls = config.repositories end if type(urls)=="string" then urls = {urls} end assert(type(urls)=="table", "getRepositories argument 'urls' is not a table or string.") local repos for _,url in pairs(urls) do local repo = manifest.getRepository(url) if repo then repos = repos or {} repos[url]=repo end end if not repos then return nil, "No packages available in repositories." end return repos end local ok, ret, err = pcall(mygetRepositories, urls) if not ok then return nil, "CRITICAL: " .. ret end return ret, err end --- Filter repositories by name regexp, version constraints and by archs and types. -- @param repos table: Available repos obtained by the getRepositories() function to filter. -- @param name string: Lua regexp to filter package names by, when not set it will be ignored and packages will not be filtered by name. -- @param version string: Version constraints, when not set packages will not be filtered by version. -- @param archFilter table: List of architercures to keep, when not set the default filter for the current LuaDist achitecture is used. -- @param typeFilter table: List of types to to keep, when not set the default type for the current LuaDist architecture is used. -- @return table, err: Filtered repositories table containing repositories/packages/versions/architectures/types. On error nil and error message are returned. function filterRepositories(repos, name, ver, archFilter, typeFilter) local function myfilterRepositories(repos, name, ver, archFilter, typeFilter) -- By defauft leave only curent arch/type and Universal types assert(not name or type(name)=="string", "filterRepositories argument 'name' is not a string.") assert(not ver or type(ver)=="string", "filterRepositories argument 'ver' is not a string.") assert(not archFilter or type(archFilter)=="table", "filterRepositories argument 'archFilter' is not a table.") assert(not typeFilter or type(typeFilter)=="table", "filterRepositories argument 'typeFilter' is not a table.") assert(not repos or type(repos)=="table", "filterRepositories argument 'repos' is not a table.") -- By default support binaries and sources according to config if not archFilter then archFilter = { "Universal" } if config.binary then table.insert(archFilter, config.arch) end end if not typeFilter then typeFilter = { "all" } if config.binary then table.insert(typeFilter, config.type) end if config.source then table.insert(typeFilter, "source") end end -- Little help function local function isIn(table, value) for _,v in ipairs(table) do if v == value then return true end end return false end -- get default repos if none are provided local err if not repos then repos, err = getRepositories() end if not repos then return nil, err end -- Filter the repos structure local filter -- ok, this may look horrible but it works just fine ... took alot of typing too :P for repo, packages in pairs(repos) do for package, versions in pairs(packages) do if not name or package:match(name) then for version, archs in pairs(versions) do if not ver or dep.constrain(version,ver) then for arch, types in pairs(archs) do if isIn(archFilter,arch) then for type, dist in pairs(types) do if isIn(typeFilter,type) then filter = filter or {} filter[repo] = filter[repo] or {} filter[repo][package] = filter[repo][package] or {} filter[repo][package][version] = filter[repo][package][version] or {} filter[repo][package][version][arch] = filter[repo][package][version][arch] or {} filter[repo][package][version][arch][type] = dist end end end end end end end end end -- uff if not filter then return nil, "No suitable package available." end return filter end local ok, ret, err = pcall(myfilterRepositories, repos, name, ver, archFilter, typeFilter) if not ok then return nil, "Critical failure:\n" .. ret end return ret, err end --- Search for suitable package file. -- @param name string: Exact name of the package to search for (case sensitive). -- @param ver string: Version constraint or nil for newest version. -- @param repos table: Available repos obtained by the getRepositories() function. -- @param arch string: Optional architecture string to search for. Defaults to LuaDist configuration. -- @param typ string: Optional architecture type string to search for. Defaults to LuaDist configuration. -- @return url, err: dist file URL or unpacked package directory. Returns nil and error on failure. function findPackage(name, ver, repos, arch, typ) local function myFindPackage(name, ver, repos, arch, typ) assert(type(name)=="string", "findPackage argument 'name' is not a string.") assert(not ver or type(ver)=="string", "findPackage argument 'ver' is not a string.") assert(type(repos)=="table", "findPackage argument 'repos' is not a table.") assert(not arch or type(arch)=="string", "findPackage argument 'arch' is not a string.") assert(not typ or type(typ)=="string", "findPackage argument 'typ' is not a string.") -- Set default arch and typ arch = arch or config.arch typ = typ or config.type -- get repos and filer them for the package name, arch etc. local search, err = filterRepositories(repos,"^"..name.."$", ver) if not search then return nil, err end -- Sort to find most recent version local sorted for repo, packages in pairs(search) do for package, versions in pairs(packages) do for version, archs in pairs(versions) do local package -- Use binary package if possible if config.binary then package = (archs[arch] or {})[typ] or (archs[arch] or {})["all"] or (archs["Universal"] or {})["all"] end -- Use source package if not package and config.source then package = (archs[arch] or {})["source"] or (archs["Universal"] or {})["source"] end -- If found make it a candidate if package then sorted = sorted or {} table.insert(sorted, {version,repo .. "/" .. package}) end end end end if not sorted then return nil, "No suitable package found for " .. name .. "." end table.sort(sorted, function (a,b) return dep.compareVersions(a[1],b[1]) end) -- Return the top find local found = sorted[1][2] return found end local ok, ret, err = pcall(myFindPackage, name, ver, repos, arch, typ) if not ok then return nil, "CRITICAL: " .. ret end return ret, err end --- Find and install package. -- @param name string: Name of the package to install (case sensitive). Alternatively URL or path to dist packages and package directories can be used too. -- @param ver string: Version constraints or nil for newest version. Ignored when name points to a package file or directory. -- @param depl string: Deployment directory to install the package into. If nil LuaDist directory is used. -- @param repos table: Available repositories obtained by the getRepositories() function. Use nil for default repositories. -- @param variables table: Table of key value pairs that can be used to set additional CMake variables for source packages. -- @return ok, err: Returns true, warnings on success and nil, error message on failure. function installPackage(name, ver, depl, repos, variables) local function myInstallPackage(name, ver, depl, repos, variables) assert(type(name)=="string", "installPackage argument 'name' is not a string.") assert(not ver or type(ver)=="string", "installPackage argument 'type' is not a string.") assert(not depl or type(depl)=="string", "installPackage argument 'depl' is not a string.") assert(not repos or type(repos)=="table", "installPackage argument 'repos' is not a table.") assert(not variables or type(variables)=="table", "installPackage argument 'variables' is not a table.") -- make sure deplination is absolute and set if not depl then depl = config.root end if not depl:match("^[%a:]*/") then depl = sys.curDir() .. "/" .. depl end -- check if installing a directory or a dist directly local file = name:gsub("^file://","") if not file:match("^[%a:]*/") then file = sys.curDir() .. "/" .. file end -- Dir containing dist info is unpacked package if sys.isFile(file .. "/dist.info") then -- Copy to temp and make local dirname = file:match("([^/]+)$") local tmp = config.temp local ok, err = sys.copy(file, tmp) if not ok then return nil, "Install failed.\n" .. err end local ok, err = makePackage(tmp .. "/" .. dirname, depl, repos, variables) if not ok then return nil, err end -- Clean up if config.cleanup then sys.delete(tmp .. "/" .. dirname) end return ok, err end -- Remote or local file if name:match("%.dist$") or name:match("%.zip$") then -- Local path to file needs to be URL if sys.isFile(name) then name = "file://" .. name end local tmp, err = package.unpack(name) if not tmp then return nil, "Could not unpack package " .. name .. ".\n" .. err end -- Enter 1st dir in the unpacked folder local dir, err = sys.dir(tmp) if not dir then return nil, "Failed unpacking." end dir = tmp .. "/" .. dir[1]; local ok, err = makePackage(dir, depl, repos, variables) if not ok then return nil, err end -- Clean up if config.cleanup then sys.delete(tmp) end return ok, err end -- Installed local info = getPackage(name, depl) if info then local version = info.version if not ver or dep.constrain(version,ver) then return true, "Installed and satisfies constraints." else return nil, "Package "..name.."-"..version.." is installed but doesn't satisfy constraints, remove it first and try again. You might also try installing to a new deployment directory." end end -- Repo local err if not repos then repos, err = getRepositories() end if not repos then return nil, err end -- find and fetch package .. need exact match local url, err = findPackage(name, ver, repos) if not url then return nil, err end -- install the dist directly return installPackage(url, ver, depl, repos, variables) end local ok, ret, err = pcall(myInstallPackage, name, ver, depl, repos, variables) if not ok then return nil, "CRITICAL: " .. ret end return ret, err end --- Make and deploy a local package directory. -- @param src string: Path to the package directory to make or nil to use current directory. -- @param depl string: Deployment directory to install the package into. If nil LuaDist directory is used. -- @param repos table: Available repositories obtained by the getRepositories() function. Use nil for default repositories. -- @param variables table: Table of key value pairs that can be used to set additional CMake variables for source packages. -- @return ok, err: Returns true on success and nil, error message on failure. function makePackage(src, depl, repos, variables) local function myMakePackage(src, depl, repos, variables) assert(not src or type(src)=="string", "makePackage argument 'src' is not a string.") assert(not depl or type(depl)=="string", "makePackage argument 'depl' is not a string.") assert(not repos or type(repos)=="table", "makePackage argument 'repos' is not a table.") assert(not variables or type(variables)=="table", "makePackages argument 'variables' is not a table.") -- by default build in current directory if not src then src = sys.curDir() end if not src:match("^[%a:]*/") then src = sys.curDir() .. "/" .. src end -- make sure deplination is absolute and set if not depl then depl = config.root end if not depl:match("^[%a:]*/") then depl = sys.curDir() .. "/" .. depl end -- check type local info, err = persist.load(src .. "/dist.info") if not info then return nil, "Package does not contain valid dist.info.\n" .. err end if sys.exists(src .. "/CMakeLists.txt") then info.arch = info.arch or "Universal" info.type = info.type or "source" end if (info.arch ~= config.arch and info.arch ~= "Universal") then return nil, "Package "..info.name.."-"..info.version.." is for different achitecture: " .. (info.arch or "no arch") end if (info.type ~= config.type and info.type ~= "all" and info.type ~= "source") then return nil, "Package "..info.name.."-"..info.version.." is for different achitecture type: " .. (info.type or "no type") end -- resolve dependencies local warning = "Install successful" local deps = info.dependencies if deps then -- get repos if none are provided local err if not repos then repos,err = filterRepositories() end if not repos then return nil, err end -- Install required Deps for package, constraints in pairs(deps) do if type(constraints) == "string" then local ok, err = installPackage(package, constraints, depl, repos, variables) if not ok then return nil, "Failed dependency: " .. package .. " " .. constraints .. ".\n" .. err end end end -- Install external Deps deps = deps.external if deps then for package, constraints in pairs(deps) do if type(constraints) == "string" then local ok, err = installPackage(package, constraints, depl, repos, variables) if not ok then warning = warning .. "\nFailed external dependency: " .. package .. " " .. constraints .. ".\n" .. err end end end end end -- Just install if its not source if info.type ~= "source" then local ok, err = package.install(src, depl) if not ok then return nil, "Could not install package: " .. info.name .. "-" .. info.version .. "\n" .. err end return true, warning end --- We have to build the package -- Setup build env local vars = {} -- Merge in command variables for k,v in pairs(config.variables) do vars[k]=v end for k,v in pairs(variables or {}) do vars[k]=v end -- Set/Expand Include and Lib search paths if vars.CMAKE_INCLUDE_PATH then vars.CMAKE_INCLUDE_PATH = vars.CMAKE_INCLUDE_PATH .. ";" .. depl .. "/" .. config.inc else vars.CMAKE_INCLUDE_PATH = depl .. "/" .. config.inc end if vars.CMAKE_LIBRARY_PATH then vars.CMAKE_LIBRARY_PATH = vars.CMAKE_LIBRARY_PATH .. ";" .. depl .. "/" .. config.lib else vars.CMAKE_LIBRARY_PATH = depl .. "/" .. config.lib end -- Build local src, err = package.build(src, vars) if not src then return nil, "Could not build package: " .. info.name .. "-" .. info.version .. ".\n" .. err end local ok, err = package.install(src, depl) if not ok then return nil, "Could not install package: " .. info.name .. "-" .. info.version .. ".\n" .. err end sys.delete(src) return ok, warning end local ok, ret, err = pcall(myMakePackage, src, depl, repos, variables) if not ok then return nil, "CRITICAL: \n" .. ret end return ret, err end --- Make manifest for the given directory of packages. -- @param path string: Path to make dist.manifest in or nil for current directory. -- @return ok, err: True on success nil, error massage on failure. function makeManifest(path) local function myMakeManifest(path) assert(not path or type(path)=="string") path = path or sys.curDir() if not path:match("^[\a-zA-Z]?:?[\\/]") then path = sys.curDir() .. "/" .. path end return manifest.makeManifest(path) end local ok, ret, err = pcall(myMakeManifest, path) if not ok then return nil, "Critical failure:\n" .. ret end return ret, err end --- Get all installed packages. -- @param depl string: Deployment directory to get packages from. -- @return packages, err: Table of package names as keys and their dist.info tables as values. Returns nil, error massage on failure. function getPackages(depl) local function myGetPackages(depl) assert(not depl or type(depl)=="string", "getPackages argument 'depl' is not a sting.") depl = depl or config.root if not depl:match("^[\a-zA-Z]?:?[\\/]") then depl = sys.curDir() .. "/" .. depl end return manifest.collectPackages(depl) end local ok, ret, err = pcall(myGetPackages, depl) if not ok then return nil, "CRITICAL :\n" .. ret end return ret, err end --- Get info of a package -- @param name string: Module name to get info for. -- @param depl string: Deployment directory to get packages from. -- @return packages, err: Table of package names as keys and their dist.info tables as values. Returns nil, error massage on failure function getPackage(name, depl) local function myGetPackage(name, depl) assert(type(name)=="string", "getPackage argument 'name' is not a string.") assert(not depl or type(depl)=="string", "getPackages argument 'depl' is not a string.") depl = depl or config.root return manifest.getInfo(depl .. "/" .. config.share .. "/" .. name .. "/dist.info") end local ok, ret, err = pcall(myGetPackage, name, depl) if not ok then return nil, "CRITICAL: " .. ret end return ret, err end --- Uninstall package. -- @param name string: Package name to uninstall. -- @param depl string: Deployment directory to uninstall from, nil for default LuaDist directory. -- @return ok, err: Returns true on success, nil and error message on failure function deletePackage(name, depl) local function myDeletePackage(name, depl) assert(type(name)=="string", "deletePackage argument 'name' is not a string.") assert(not depl or type(depl)=="string", "deletePackage argument 'depl' is not a string.") depl = depl or config.root if not depl:match("^[\a-zA-Z]?:?[\\/]") then depl = sys.curDir() .. "/" .. depl end return package.delete(name, depl) end local ok, ret, err = pcall(myDeletePackage, name, depl) if not ok then return nil, "CRITICAL: " .. ret end return ret, err end --- Pack installed package. -- @param name string: Package name to pack. -- @param dest string: Destination path to save the result into. When not specified current directory will be used. -- @param depl string: Deployment directory to pack from, nil for default LuaDist directory. -- @return path, err: Returns path of the package, nil and error message on failure function packPackage(name, depl, dest) local function myPackPackage(name, depl, dest) assert(type(name)=="string", "packPackage argument 'name' is not a string.") assert(not depl or type(depl)=="string", "packPackage argument 'depl' is not a string.") assert(not dest or type(dest)=="string", "packPackage argument 'dest' is not a string.") depl = depl or config.root dest = dest or sys.curDir() if not depl:match("^[\a-zA-Z]?:?[\\/]") then depl = sys.curDir() .. "/" .. depl end return package.pack(name, depl, dest) end local ok, ret, err = pcall(myPackPackage, name, depl, dest) if not ok then return nil, "CRITICAL: " .. ret end return ret, err end --- Get root directory of LuaDist -- @return path: Full path to the LuaDist install directory function getRoot() return config.root end