• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nightconcept / almandine / 14846535458

05 May 2025 09:22PM UTC coverage: 32.421% (-35.5%) from 67.965%
14846535458

push

github

web-flow
fix: Change init module to be e2e testable (#17)

91 of 420 new or added lines in 11 files covered. (21.67%)

462 of 1425 relevant lines covered (32.42%)

1.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.9
/src/modules/install.lua
1
--[[
2
  Install Command Module
3

4
  Provides functionality to install dependencies based on the lockfile (`almd-lock.lua`).
5
  If the lockfile doesn't exist or is outdated compared to `project.lua`, it will be generated/updated first.
6
]]
7
--
8

9
---TODO: remove this once we have a pass over this file
10
-- luacheck: ignore
11
---@class InstallDeps
12
---@field load_manifest fun(): table, string?
13
---@field ensure_lib_dir fun(): nil
14
---@field downloader table
15
---@field lockfile table
16
---@field hash_utils table
17
---@field filesystem table
18
---@field url_utils table
19
---@field printer table Printer utility with stdout/stderr methods.
20

21
---
22
-- Installs dependencies based primarily on the lockfile.
23
-- Generates or updates the lockfile if necessary based on the manifest.
24
-- If dep_name is provided, only installs that specific dependency after ensuring the lockfile is up-to-date.
25
--
26
-- @param dep_name string|nil Dependency name to install (or all if nil).
27
-- @param deps InstallDeps A table containing required dependencies:
28
--   - load_manifest function: Function to load the project manifest.
29
--   - ensure_lib_dir function: Function to ensure the library directory exists.
30
--   - downloader table: The downloader module.
31
--   - lockfile table: The lockfile utility module.
32
--   - hash_utils table: The hash utility module (optional, primarily for future verification).
33
--   - filesystem table: The filesystem utility module.
34
--   - url_utils table: The URL utility module.
35
-- @return boolean success True if successful, false otherwise.
36
-- @return string|nil output_message Message for stdout.
37
-- @return string|nil error_message Message for stderr.
38
local function install_dependencies(dep_name, deps)
39
  local load_manifest = deps.load_manifest
×
40
  local ensure_lib_dir = deps.ensure_lib_dir
×
41
  local effective_downloader = deps.downloader
×
42
  local lockfile_utils = deps.lockfile -- Use the passed-in lockfile utility
×
43
  local fs_utils = deps.filesystem -- Use the passed-in filesystem utility
×
44
  local url_utils = deps.url_utils -- Use the passed-in url utility
×
45
  local hash_utils = deps.hash_utils -- Use the passed-in hash utility
×
46

NEW
47
  local output_messages = {}
×
NEW
48
  local error_messages = {}
×
49

50
  ensure_lib_dir() -- Ensure base lib directory exists
×
51

52
  -- 1. Load Manifest
53
  local manifest, err_load = load_manifest()
×
54
  if not manifest then
×
NEW
55
    table.insert(error_messages, "Failed to load manifest (project.lua): " .. tostring(err_load))
×
NEW
56
    return false, nil, table.concat(error_messages, "\n")
×
57
  end
58
  local manifest_deps = manifest.dependencies or {}
×
59

60
  -- 2. Load Lockfile (if exists)
61
  local existing_lock_data, err_lock_load = lockfile_utils.load_lockfile()
×
62
  if err_lock_load then
×
NEW
63
    table.insert(error_messages, "Warning: Could not load existing lockfile: " .. tostring(err_lock_load))
×
64
    existing_lock_data = { package = {} } -- Treat as empty, ensuring .package exists
×
65
  end
66
  local existing_lock_deps = existing_lock_data.package or {} -- Ensure it's a table
×
67

68
  -- 3. Reconcile Manifest with Lockfile & Build Target Lockfile Data
69
  local target_lock_deps = {}
×
70
  local lockfile_updated = false
×
71
  local needs_content_hash = {} -- Keep track of deps needing post-download hash verification/calculation
×
72

NEW
73
  table.insert(output_messages, "Checking project.lua dependencies against lockfile...")
×
74
  for name, dep_info in pairs(manifest_deps) do
×
75
    -- Validate manifest entry format (must be table with url and path)
76
    if type(dep_info) == "table" and dep_info.url and dep_info.path then
×
77
      -- Try to extract commit hash from URL for github: prefix
78
      local _, extracted_commit_hash = url_utils.normalize_github_url(dep_info.url, nil)
×
79

80
      -- Normalize path with forward slashes
NEW
81
      local normalized_path = fs_utils.normalize_path(dep_info.path)
×
82

83
      local manifest_entry = {
×
84
        source = dep_info.url, -- Store the original source URL
85
        path = normalized_path, -- Use normalized path
86
        -- hash = nil -- Set below
87
      }
88

89
      -- Determine the hash type and value for the lockfile
90
      if extracted_commit_hash then
×
91
        manifest_entry.hash = "github:" .. extracted_commit_hash
×
92
      else
93
        -- No extractable GitHub hash, mark for SHA512 content hash
94
        needs_content_hash[name] = true
×
95
        -- manifest_entry.hash will be set after download if needed
96
      end
97

98
      local existing_entry = existing_lock_deps[name]
×
99

100
      -- Determine if update is needed
101
      local needs_update = false
×
102
      if not existing_entry then
×
103
        needs_update = true
×
NEW
104
        table.insert(output_messages, string.format("Adding new lockfile entry for %s.", name))
×
105
      elseif existing_entry.source ~= manifest_entry.source or existing_entry.path ~= manifest_entry.path then
×
106
        needs_update = true
×
NEW
107
        table.insert(output_messages, string.format("Updating lockfile entry for %s (source/path changed).", name))
×
108
      elseif manifest_entry.hash and existing_entry.hash ~= manifest_entry.hash then
×
109
        -- If we determined a github: hash and it differs from existing
110
        needs_update = true
×
NEW
111
        table.insert(output_messages,
×
112
          string.format(
×
113
            "Updating lockfile entry for %s (hash changed: %s -> %s).",
114
            name,
115
            existing_entry.hash or "N/A",
×
116
            manifest_entry.hash
117
          )
118
        )
119
      elseif needs_content_hash[name] and (not existing_entry.hash or not existing_entry.hash:find("^sha512:")) then
×
120
        -- If we need a content hash, but existing doesn't have one (or it's not sha512)
121
        needs_update = true
×
NEW
122
        table.insert(output_messages, string.format("Marking lockfile entry for %s for SHA512 hash update.", name))
×
123
      end
124

125
      if needs_update then
×
126
        target_lock_deps[name] = manifest_entry -- Store the entry derived from manifest
×
127
        lockfile_updated = true -- Mark lockfile as needing save
×
128
      else
129
        -- Entry exists and matches (or SHA512 will be verified later), copy it
130
        target_lock_deps[name] = existing_entry
×
131
        -- If existing entry needs content hash check, mark it
132
        if existing_entry.hash and existing_entry.hash:find("^sha512:") then
×
133
          needs_content_hash[name] = true -- Mark for verification after download
×
134
        end
135
      end
136
    else
137
      -- Invalid entry format, print message and skip
NEW
138
      table.insert(error_messages, string.format("Warning: Skipping %s: Invalid entry in project.lua. Requires at least url and path.", name))
×
139
    end
140
  end
141

142
  -- Stale entry removal logic remains implicitly handled
143

144
  -- 4. Pre-Save Lockfile
145
  local lock_data_to_save = {
×
146
    api_version = existing_lock_data.api_version or "1",
147
    package = target_lock_deps,
148
  }
149

150
  -- Remove commit field before saving (if it accidentally exists)
151
  for _, entry in pairs(lock_data_to_save.package) do
×
152
    entry.commit = nil
×
153
  end
154

155
  if lockfile_updated or fs_utils.get_path_type("almd-lock.lua") ~= "file" then
×
NEW
156
    table.insert(output_messages, "Updating almd-lock.lua...")
×
157
    local ok_save, err_save = lockfile_utils.write_lockfile(lock_data_to_save)
×
158
    if not ok_save then
×
NEW
159
      table.insert(error_messages, "Warning: Failed to save lockfile: " .. tostring(err_save))
×
NEW
160
      table.insert(error_messages, "Warning: Proceeding with installation despite lockfile save failure.")
×
161
    else
NEW
162
      table.insert(output_messages, "Lockfile updated.")
×
163
    end
164
  else
NEW
165
    table.insert(output_messages, "Lockfile is up-to-date.")
×
166
  end
167

168
  -- 5. Install Dependencies from the Target Lockfile Data
NEW
169
  table.insert(output_messages, "Installing dependencies from lockfile...")
×
170
  local install_count = 0
×
171
  local fail_count = 0
×
172
  local content_hashes_verified_or_calculated = false -- Flag if SHA512 logic ran
×
173

174
  for name, lock_entry in pairs(target_lock_deps) do
×
175
    -- Check if we should process this dependency (either all deps or the specific one)
176
    local should_process = (not dep_name) or (dep_name == name)
×
177

178
    if should_process then
×
179
      -- Validate the lock entry format
180
      if type(lock_entry) == "table" and lock_entry.source and lock_entry.path and lock_entry.hash then
×
181
        -- ### Start of main processing logic for valid entry ###
182
        local source_url = lock_entry.source
×
183
        local target_path = lock_entry.path
×
184
        local expected_hash = lock_entry.hash
×
185

186
        -- Ensure path is normalized for operations but keeps forward slashes in lockfile
NEW
187
        local normalized_path = fs_utils.normalize_path(target_path)
×
188
        -- For file operations, we may need to convert back to OS-specific format
NEW
189
        local system_path = target_path -- This is fine as is since it came from lockfile        
×
190
        -- Get the raw download URL, preferring normalized version
191
        local _, _, normalized_download_url, norm_err = url_utils.normalize_github_url(source_url, nil)
×
192
        local download_url = normalized_download_url or source_url -- Fallback to original source if normalization fails
×
193
        if norm_err and normalized_download_url then
×
NEW
194
          table.insert(error_messages, string.format("Warning: Using potentially non-raw URL for %s. Normalization error: %s", name, norm_err))
×
195
        end
196

NEW
197
        local target_dir = system_path:match("(.+)[\\/]")
×
198
        if target_dir then
×
199
          fs_utils.ensure_dir_exists(target_dir)
×
200
        end
201

202
        local display_hash = expected_hash:match("^github:(.+)$")
×
203
        local display_name = name .. (display_hash and ("@" .. display_hash:sub(1, 7)) or "")
×
NEW
204
        table.insert(output_messages, string.format("Downloading %s from %s to %s...", display_name, download_url, system_path))
×
NEW
205
        local ok, err_download = effective_downloader.download(download_url, system_path)
×
206

207
        if ok then
×
NEW
208
          table.insert(output_messages, string.format("Downloaded %s to %s", name, system_path))
×
209
          install_count = install_count + 1
×
210

211
          -- Verify or Calculate SHA512 hash if needed
212
          if needs_content_hash[name] then
×
213
            content_hashes_verified_or_calculated = true -- Mark that we ran this logic
×
NEW
214
            table.insert(output_messages, string.format("Calculating/Verifying SHA512 hash for %s...", name))
×
NEW
215
            local calculated_hash, hash_err = hash_utils.calculate_sha512(system_path)
×
216

217
            if calculated_hash then
×
218
              local calculated_hash_str = "sha512:" .. calculated_hash
×
219
              if expected_hash:find("^sha512:") then -- Verify existing SHA512 hash
×
220
                if expected_hash == calculated_hash_str then
×
NEW
221
                  table.insert(output_messages, string.format("  -> SHA512 Verified: %s", calculated_hash_str))
×
222
                else
NEW
223
                  table.insert(error_messages,
×
224
                    string.format(
×
225
                      "Error: SHA512 Mismatch for %s! Expected %s, got %s",
226
                      name,
227
                      expected_hash,
228
                      calculated_hash_str
229
                    )
230
                  )
NEW
231
                  table.insert(error_messages, "  -> This may indicate corrupted download or lockfile tampering.")
×
232
                  fail_count = fail_count + 1 -- Count as failure due to mismatch
×
233
                end
234
              else -- Calculate and store new SHA512 hash
235
                lock_entry.hash = calculated_hash_str
×
236
                -- Ensure path is normalized in the lockfile
NEW
237
                lock_entry.path = normalized_path
×
NEW
238
                table.insert(output_messages, string.format("  -> SHA512 Calculated: %s", lock_entry.hash))
×
239
                lockfile_updated = true -- Mark that we need to re-save the lockfile
×
240
              end
241
            else
NEW
242
              table.insert(error_messages, string.format("Warning: Failed to calculate hash for %s: %s", name, hash_err or "Unknown error"))
×
243
            end
244
          end
245
        else
NEW
246
          table.insert(error_messages, string.format("Error: Failed to download %s: %s", name, err_download or "Unknown error"))
×
247
          fail_count = fail_count + 1
×
248
        end
249
        -- ### End of main processing logic for valid entry ###
250
      else
251
        -- Invalid lock entry format
NEW
252
        table.insert(error_messages, string.format("Warning: Skipping %s: Invalid lock entry format (missing source, path, or hash).", name))
×
253
        fail_count = fail_count + 1
×
254
      end
255
    end -- end if should_process
256
  end
257

258
  -- 6. Re-save Lockfile if hashes were calculated/updated
259
  if lockfile_updated and content_hashes_verified_or_calculated then
×
NEW
260
    table.insert(output_messages, "Saving updated lockfile with content hashes...")
×
261
    -- Ensure commit field is definitely gone before final save
262
    for _, entry in pairs(target_lock_deps) do
×
263
      entry.commit = nil
×
264
    end
265
    local final_lock_data = { api_version = lock_data_to_save.api_version, package = target_lock_deps }
×
266
    local ok_resave, err_resave = lockfile_utils.write_lockfile(final_lock_data)
×
267
    if not ok_resave then
×
NEW
268
      table.insert(error_messages, "Warning: Failed to re-save lockfile with calculated/verified content hashes: " .. tostring(err_resave))
×
269
    else
NEW
270
      table.insert(output_messages, "Lockfile updated with content hashes.")
×
271
    end
272
  end
273

NEW
274
  table.insert(output_messages, string.format("Installation finished. %d dependencies installed, %d failed.", install_count, fail_count))
×
275
  if fail_count > 0 then
×
NEW
276
    table.insert(error_messages, "Some dependencies failed to install or verify.")
×
NEW
277
    return false, table.concat(output_messages, "\n"), table.concat(error_messages, "\n")
×
278
  end
279

280
  -- Success
NEW
281
  local final_output = table.concat(output_messages, "\n")
×
282
  local final_error = nil
NEW
283
  if #error_messages > 0 then
×
NEW
284
    final_error = table.concat(error_messages, "\n")
×
285
  end
NEW
286
  return true, final_output, final_error
×
287
end
288

289
-- Keep lockfile require accessible if needed by other parts (though unlikely now)
290
-- local lockfile = require("utils.lockfile") -- Already required at the top
291

292
local function help_info()
293
  print([[
×
294
Usage: almd install [<dep_name>]
295

296
Installs dependencies based on almd-lock.lua.
297
If almd-lock.lua is missing or outdated compared to project.lua, it will be
298
generated or updated before installation.
299

300
If <dep_name> is specified, only that dependency will be installed after
301
ensuring the lockfile is up-to-date.
302

303
Examples:
304
  almd install          # Install all dependencies from lockfile (update if needed)
305
  almd install my_lib   # Install only 'my_lib' from lockfile (update if needed)
306
]])
×
307
end
308

309
return {
2✔
310
  install_dependencies = install_dependencies,
2✔
311
  -- lockfile = lockfile, -- No longer need to export lockfile module itself
312
  help_info = help_info,
2✔
313
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc