fy36
2025-05-14 a37aca60ff9914b0abb710f04118b22420f4f398
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
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