ecosystems-bibliothecary 15.2.0 → 15.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -1
  3. data/README.md +57 -1
  4. data/lib/bibliothecary/dependency.rb +6 -1
  5. data/lib/bibliothecary/parsers/alpm.rb +89 -0
  6. data/lib/bibliothecary/parsers/apk.rb +91 -0
  7. data/lib/bibliothecary/parsers/bazel.rb +65 -0
  8. data/lib/bibliothecary/parsers/bentoml.rb +1 -1
  9. data/lib/bibliothecary/parsers/bower.rb +1 -0
  10. data/lib/bibliothecary/parsers/cargo.rb +3 -1
  11. data/lib/bibliothecary/parsers/clojars.rb +1 -0
  12. data/lib/bibliothecary/parsers/cocoapods.rb +29 -1
  13. data/lib/bibliothecary/parsers/cog.rb +1 -1
  14. data/lib/bibliothecary/parsers/conda.rb +2 -0
  15. data/lib/bibliothecary/parsers/deb.rb +132 -0
  16. data/lib/bibliothecary/parsers/deno.rb +15 -1
  17. data/lib/bibliothecary/parsers/dub.rb +2 -0
  18. data/lib/bibliothecary/parsers/dvc.rb +1 -1
  19. data/lib/bibliothecary/parsers/go.rb +4 -2
  20. data/lib/bibliothecary/parsers/hackage.rb +4 -3
  21. data/lib/bibliothecary/parsers/haxelib.rb +1 -0
  22. data/lib/bibliothecary/parsers/hex.rb +22 -7
  23. data/lib/bibliothecary/parsers/luarocks.rb +1 -0
  24. data/lib/bibliothecary/parsers/meteor.rb +1 -0
  25. data/lib/bibliothecary/parsers/mlflow.rb +1 -1
  26. data/lib/bibliothecary/parsers/nimble.rb +1 -0
  27. data/lib/bibliothecary/parsers/npm.rb +81 -12
  28. data/lib/bibliothecary/parsers/ollama.rb +1 -1
  29. data/lib/bibliothecary/parsers/packagist.rb +28 -31
  30. data/lib/bibliothecary/parsers/pypi.rb +16 -2
  31. data/lib/bibliothecary/parsers/rpm.rb +80 -0
  32. data/lib/bibliothecary/parsers/rubygems.rb +34 -4
  33. data/lib/bibliothecary/version.rb +1 -1
  34. metadata +6 -1
@@ -52,16 +52,30 @@ module Bibliothecary
52
52
  manifest = JSON.parse(file_contents)
53
53
  source = options.fetch(:filename, nil)
54
54
 
55
+ # Build integrity lookup from jsr and npm sections
56
+ integrity_map = {}
57
+ manifest.fetch("jsr", {}).each do |key, value|
58
+ integrity_map["jsr:#{key}"] = value["integrity"] if value.is_a?(Hash) && value["integrity"]
59
+ end
60
+ manifest.fetch("npm", {}).each do |key, value|
61
+ integrity_map["npm:#{key}"] = value["integrity"] if value.is_a?(Hash) && value["integrity"]
62
+ end
63
+
55
64
  dependencies = manifest.fetch("specifiers", {}).map do |specifier, resolved_version|
56
65
  name, _requirement = parse_specifier(specifier)
57
66
  next unless name
58
67
 
68
+ # Determine protocol (npm: or jsr:) and build lookup key
69
+ protocol = specifier.start_with?("jsr:") ? "jsr" : "npm"
70
+ integrity_key = "#{protocol}:#{name}@#{resolved_version}"
71
+
59
72
  Dependency.new(
60
73
  name: name,
61
74
  requirement: resolved_version,
62
75
  type: "runtime",
63
76
  source: source,
64
- platform: platform_name
77
+ platform: platform_name,
78
+ integrity: integrity_map[integrity_key]
65
79
  )
66
80
  end.compact
67
81
 
@@ -18,10 +18,12 @@ module Bibliothecary
18
18
  match_filename("dub.json") => {
19
19
  kind: "manifest",
20
20
  parser: :parse_json_manifest,
21
+ can_have_lockfile: false,
21
22
  },
22
23
  match_filename("dub.sdl") => {
23
24
  kind: "manifest",
24
25
  parser: :parse_sdl_manifest,
26
+ can_have_lockfile: false,
25
27
  },
26
28
  }
27
29
  end
@@ -14,7 +14,7 @@ module Bibliothecary
14
14
  match_filename("dvc.yaml") => {
15
15
  kind: 'manifest',
16
16
  parser: :parse_dvc_yaml,
17
- related_to: [ 'manifest' ]
17
+ can_have_lockfile: false,
18
18
  }
19
19
  }
20
20
  end
@@ -209,10 +209,12 @@ module Bibliothecary
209
209
  requirement: match[2].strip.split("/").first,
210
210
  type: "runtime",
211
211
  source: options.fetch(:filename, nil),
212
- platform: platform_name
212
+ platform: platform_name,
213
+ integrity: match[3].strip
213
214
  )
214
215
  end
215
- dependencies = deps.uniq
216
+ # Dedupe by name+requirement, keeping the first occurrence (h1 hash, not go.mod hash)
217
+ dependencies = deps.uniq { |d| [d.name, d.requirement] }
216
218
  ParserResult.new(dependencies: dependencies)
217
219
  end
218
220
 
@@ -12,8 +12,8 @@ module Bibliothecary
12
12
  # Matches build-tool-depends format: package:tool == version
13
13
  BUILD_TOOL_REGEXP = /^\s*([a-zA-Z][a-zA-Z0-9-]*):[a-zA-Z][a-zA-Z0-9-]*\s*((?:[<>=!]+\s*[\d.*]+(?:\s*&&\s*[<>=!]+\s*[\d.*]+)*)?)/
14
14
 
15
- # Matches stack.yaml.lock hackage entries like: hackage: fuzzyset-0.2.4@sha256:...
16
- STACK_LOCK_REGEXP = /hackage:\s*([a-zA-Z0-9-]+)-([0-9.]+)@/
15
+ # Matches stack.yaml.lock hackage entries like: hackage: fuzzyset-0.2.4@sha256:hash,size
16
+ STACK_LOCK_REGEXP = /hackage:\s*([a-zA-Z0-9-]+)-([0-9.]+)@sha256:([a-f0-9]+)/
17
17
 
18
18
  def self.file_patterns
19
19
  ["*.cabal", "*cabal.config", "stack.yaml.lock", "cabal.project.freeze"]
@@ -198,7 +198,8 @@ module Bibliothecary
198
198
  name: match[1],
199
199
  requirement: match[2],
200
200
  type: "runtime",
201
- source: source
201
+ source: source,
202
+ integrity: "sha256=#{match[3]}"
202
203
  )
203
204
  end
204
205
 
@@ -16,6 +16,7 @@ module Bibliothecary
16
16
  match_filename("haxelib.json") => {
17
17
  kind: "manifest",
18
18
  parser: :parse_manifest,
19
+ can_have_lockfile: false,
19
20
  },
20
21
  }
21
22
  end
@@ -5,13 +5,15 @@ module Bibliothecary
5
5
  class Hex
6
6
  include Bibliothecary::Analyser
7
7
 
8
- # Matches mix.lock entries: "name": {:hex, :name, "version", ...
8
+ # Matches mix.lock entries: "name": {:hex, :name, "version", "hash", ...
9
9
  # or "name": {:git, "url", "ref", ...
10
- HEX_LOCK_REGEXP = /"([^"]+)":\s*\{:hex,\s*:[^,]+,\s*"([^"]+)"/
10
+ HEX_LOCK_REGEXP = /"([^"]+)":\s*\{:hex,\s*:[^,]+,\s*"([^"]+)",\s*"([^"]+)"/
11
11
  GIT_LOCK_REGEXP = /"([^"]+)":\s*\{:git,\s*"([^"]+)",\s*"([^"]+)"/
12
12
 
13
13
  # Matches rebar.lock entries: {<<"name">>,{pkg,<<"name">>,<<"version">>},N}
14
14
  REBAR_LOCK_REGEXP = /\{<<"([^"]+)">>,\{pkg,<<"[^"]+">>,<<"([^"]+)">>},\d+\}/
15
+ # Matches rebar.lock pkg_hash entries: {<<"name">>, <<"HASH">>}
16
+ REBAR_PKG_HASH_REGEXP = /\{<<"([^"]+)">>,\s*<<"([A-F0-9]+)">>}/
15
17
 
16
18
  def self.file_patterns
17
19
  ["mix.exs", "mix.lock", "gleam.toml", "manifest.toml", "rebar.lock"]
@@ -74,14 +76,15 @@ module Bibliothecary
74
76
  source = options.fetch(:filename, "mix.lock")
75
77
  deps = []
76
78
 
77
- # Match hex packages: "name": {:hex, :name, "version", ...
78
- file_contents.scan(HEX_LOCK_REGEXP) do |name, version|
79
+ # Match hex packages: "name": {:hex, :name, "version", "hash", ...
80
+ file_contents.scan(HEX_LOCK_REGEXP) do |name, version, hash|
79
81
  deps << Dependency.new(
80
82
  platform: platform_name,
81
83
  name: name,
82
84
  requirement: version,
83
85
  type: "runtime",
84
- source: source
86
+ source: source,
87
+ integrity: "sha256=#{hash}"
85
88
  )
86
89
  end
87
90
 
@@ -135,12 +138,14 @@ module Bibliothecary
135
138
  manifest.fetch("packages", []).each do |pkg|
136
139
  next unless pkg["source"] == "hex"
137
140
 
141
+ checksum = pkg["outer_checksum"]
138
142
  deps << Dependency.new(
139
143
  platform: platform_name,
140
144
  name: pkg["name"],
141
145
  requirement: pkg["version"],
142
146
  type: "runtime",
143
- source: source
147
+ source: source,
148
+ integrity: checksum ? "sha256=#{checksum.downcase}" : nil
144
149
  )
145
150
  end
146
151
 
@@ -151,13 +156,23 @@ module Bibliothecary
151
156
  source = options.fetch(:filename, "rebar.lock")
152
157
  deps = []
153
158
 
159
+ # Parse pkg_hash section to build lookup table
160
+ pkg_hashes = {}
161
+ pkg_hash_section = file_contents[/\{pkg_hash,\s*\[(.*?)\]\}/m, 1]
162
+ if pkg_hash_section
163
+ pkg_hash_section.scan(REBAR_PKG_HASH_REGEXP) do |name, hash|
164
+ pkg_hashes[name] = "sha256=#{hash.downcase}"
165
+ end
166
+ end
167
+
154
168
  file_contents.scan(REBAR_LOCK_REGEXP) do |name, version|
155
169
  deps << Dependency.new(
156
170
  platform: platform_name,
157
171
  name: name,
158
172
  requirement: version,
159
173
  type: "runtime",
160
- source: source
174
+ source: source,
175
+ integrity: pkg_hashes[name]
161
176
  )
162
177
  end
163
178
 
@@ -14,6 +14,7 @@ module Bibliothecary
14
14
  match_extension(".rockspec") => {
15
15
  kind: "manifest",
16
16
  parser: :parse_rockspec,
17
+ can_have_lockfile: false,
17
18
  },
18
19
  }
19
20
  end
@@ -16,6 +16,7 @@ module Bibliothecary
16
16
  match_filename("versions.json") => {
17
17
  kind: "manifest",
18
18
  parser: :parse_manifest,
19
+ can_have_lockfile: false,
19
20
  },
20
21
  }
21
22
  end
@@ -14,7 +14,7 @@ module Bibliothecary
14
14
  match_filename("MLmodel") => {
15
15
  kind: 'manifest',
16
16
  parser: :parse_mlmodel,
17
- related_to: [ 'manifest' ]
17
+ can_have_lockfile: false,
18
18
  }
19
19
  }
20
20
  end
@@ -18,6 +18,7 @@ module Bibliothecary
18
18
  match_extension(".nimble") => {
19
19
  kind: "manifest",
20
20
  parser: :parse_nimble,
21
+ can_have_lockfile: false,
21
22
  },
22
23
  }
23
24
  end
@@ -11,7 +11,7 @@ module Bibliothecary
11
11
  PACKAGE_LOCK_JSON_MAX_DEPTH = 10
12
12
 
13
13
  def self.file_patterns
14
- ["package.json", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml", "bun.lock", "npm-ls.json"]
14
+ ["package.json", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml", "pnpm-workspace.yaml", "bun.lock", "npm-ls.json"]
15
15
  end
16
16
 
17
17
  def self.mapping
@@ -32,6 +32,11 @@ module Bibliothecary
32
32
  kind: "lockfile",
33
33
  parser: :parse_pnpm_lock,
34
34
  },
35
+ match_filename("pnpm-workspace.yaml") => {
36
+ kind: "manifest",
37
+ parser: :parse_pnpm_workspace,
38
+ related_to: ["lockfile"],
39
+ },
35
40
  match_filename("npm-ls.json") => {
36
41
  kind: "lockfile",
37
42
  parser: :parse_ls,
@@ -101,7 +106,8 @@ module Bibliothecary
101
106
  type: dep.fetch("dev", false) || dep.fetch("devOptional", false) ? "development" : "runtime",
102
107
  local: dep.fetch("link", false),
103
108
  source: source,
104
- platform: platform_name
109
+ platform: platform_name,
110
+ integrity: dep["integrity"]
105
111
  )
106
112
  end
107
113
  end
@@ -122,7 +128,8 @@ module Bibliothecary
122
128
  requirement: version,
123
129
  type: type,
124
130
  source: source,
125
- platform: platform_name
131
+ platform: platform_name,
132
+ integrity: requirement["integrity"]
126
133
  )] + child_dependencies
127
134
  end
128
135
  end
@@ -191,7 +198,8 @@ module Bibliothecary
191
198
  type: nil, # yarn.lock doesn't report on the type of dependency
192
199
  local: dep[:requirements]&.first&.start_with?("file:"),
193
200
  source: options.fetch(:filename, nil),
194
- platform: platform_name
201
+ platform: platform_name,
202
+ integrity: dep[:integrity]
195
203
  )
196
204
  end
197
205
  ParserResult.new(dependencies: dependencies)
@@ -232,11 +240,23 @@ module Bibliothecary
232
240
 
233
241
  def self.parse_v2_yarn_lock(contents, source = nil)
234
242
  deps = []
235
- # Match package blocks: "package@npm:req": followed by version: x.y.z
236
- # Examples: "js-tokens@npm:^3.0.0 || ^4.0.0": or "pkg1@npm:req1, pkg2@npm:req2":
237
- contents.scan(/^"([^"]+)":\s*\n\s+version:\s*([^\n]+)/m) do |packages_str, version|
243
+ # Split into blocks by double newlines or by unquoted key lines
244
+ # Each block starts with "package@npm:req": and continues until the next package
245
+ blocks = contents.split(/\n\n+/)
246
+
247
+ blocks.each do |block|
248
+ # Match the package header: "package@npm:...":\n version: ...
249
+ match = block.match(/^"([^"]+)":\s*\n(.+)/m)
250
+ next unless match
251
+
252
+ packages_str = match[1]
253
+ body = match[2]
254
+
255
+ version = body[/version:\s*([^\n]+)/, 1]
256
+ checksum = body[/checksum:\s*([^\n]+)/, 1]
257
+
238
258
  # Skip workspace/local packages and patches
239
- next if version.include?("use.local") && packages_str.include?("workspace")
259
+ next if version&.include?("use.local") && packages_str.include?("workspace")
240
260
  next if packages_str.include?("@patch:")
241
261
 
242
262
  packages = packages_str.split(", ")
@@ -251,6 +271,7 @@ module Bibliothecary
251
271
  original_requirement: alias_name.nil? ? nil : version.to_s,
252
272
  version: version.to_s,
253
273
  source: source,
274
+ integrity: checksum,
254
275
  }
255
276
  end
256
277
  deps
@@ -285,7 +306,8 @@ module Bibliothecary
285
306
  original_requirement: original_requirement,
286
307
  type: is_dev ? "development" : "runtime",
287
308
  source: source,
288
- platform: platform_name
309
+ platform: platform_name,
310
+ integrity: details.dig("resolution", "integrity")
289
311
  )
290
312
  end
291
313
  end
@@ -322,7 +344,8 @@ module Bibliothecary
322
344
  original_requirement: original_requirement,
323
345
  type: is_dev ? "development" : "runtime",
324
346
  source: source,
325
- platform: platform_name
347
+ platform: platform_name,
348
+ integrity: details.dig("resolution", "integrity")
326
349
  )
327
350
  end
328
351
  end
@@ -331,6 +354,7 @@ module Bibliothecary
331
354
  dependencies = parsed_contents.fetch("importers", {}).fetch(".", {}).fetch("dependencies", {})
332
355
  dev_dependencies = parsed_contents.fetch("importers", {}).fetch(".", {}).fetch("devDependencies", {})
333
356
  dependency_mapping = dependencies.merge(dev_dependencies)
357
+ packages = parsed_contents.fetch("packages", {})
334
358
 
335
359
  # "dependencies" is in "packages" for < v9 and in "snapshots" for >= v9
336
360
  # as of https://github.com/pnpm/pnpm/pull/7700.
@@ -364,6 +388,10 @@ module Bibliothecary
364
388
  dev_name == name && dev_details["version"] == version
365
389
  end
366
390
 
391
+ # In v9, integrity is stored in packages section, not snapshots
392
+ package_key = "#{name}@#{version}"
393
+ integrity = packages.dig(package_key, "resolution", "integrity")
394
+
367
395
  Dependency.new(
368
396
  name: name,
369
397
  requirement: version,
@@ -371,7 +399,8 @@ module Bibliothecary
371
399
  original_requirement: original_requirement,
372
400
  type: is_dev ? "development" : "runtime",
373
401
  source: source,
374
- platform: platform_name
402
+ platform: platform_name,
403
+ integrity: integrity
375
404
  )
376
405
  end
377
406
  end
@@ -395,6 +424,45 @@ module Bibliothecary
395
424
  ParserResult.new(dependencies: dependencies)
396
425
  end
397
426
 
427
+ def self.parse_pnpm_workspace(contents, options: {})
428
+ parsed = YAML.load(contents)
429
+ source = options.fetch(:filename, nil)
430
+
431
+ dependencies = []
432
+
433
+ # Parse the default catalog (pnpm 9+)
434
+ if parsed["catalog"].is_a?(Hash)
435
+ parsed["catalog"].each do |name, requirement|
436
+ dependencies << Dependency.new(
437
+ name: name,
438
+ requirement: requirement,
439
+ type: "runtime",
440
+ source: source,
441
+ platform: platform_name
442
+ )
443
+ end
444
+ end
445
+
446
+ # Parse named catalogs (pnpm 9+)
447
+ if parsed["catalogs"].is_a?(Hash)
448
+ parsed["catalogs"].each do |_catalog_name, catalog_deps|
449
+ next unless catalog_deps.is_a?(Hash)
450
+
451
+ catalog_deps.each do |name, requirement|
452
+ dependencies << Dependency.new(
453
+ name: name,
454
+ requirement: requirement,
455
+ type: "runtime",
456
+ source: source,
457
+ platform: platform_name
458
+ )
459
+ end
460
+ end
461
+ end
462
+
463
+ ParserResult.new(dependencies: dependencies)
464
+ end
465
+
398
466
  def self.parse_ls(file_contents, options: {})
399
467
  manifest = JSON.parse(file_contents)
400
468
 
@@ -425,7 +493,8 @@ module Bibliothecary
425
493
  type: dev_deps&.include?(name) ? "development" : "runtime",
426
494
  local: is_local,
427
495
  source: source,
428
- platform: platform_name
496
+ platform: platform_name,
497
+ integrity: info[3]
429
498
  )
430
499
  end
431
500
  ParserResult.new(dependencies: dependencies)
@@ -14,7 +14,7 @@ module Bibliothecary
14
14
  match_filename("Modelfile") => {
15
15
  kind: 'manifest',
16
16
  parser: :parse_modelfile,
17
- related_to: [ 'manifest' ]
17
+ can_have_lockfile: false,
18
18
  }
19
19
  }
20
20
  end
@@ -27,42 +27,39 @@ module Bibliothecary
27
27
 
28
28
  def self.parse_lockfile(file_contents, options: {})
29
29
  manifest = JSON.parse file_contents
30
- dependencies = manifest.fetch("packages", []).map do |dependency|
31
- requirement = dependency["version"]
32
-
33
- # Store Drupal version if Drupal, but include the original manifest version for reference
34
- if drupal_module?(dependency)
35
- original_requirement = requirement
36
- requirement = dependency.dig("source", "reference")
37
- end
30
+ source = options.fetch(:filename, nil)
38
31
 
39
- Dependency.new(
40
- name: dependency["name"],
41
- requirement: requirement,
42
- type: "runtime",
43
- original_requirement: original_requirement,
44
- source: options.fetch(:filename, nil),
45
- platform: platform_name
46
- )
32
+ dependencies = manifest.fetch("packages", []).map do |dependency|
33
+ parse_composer_dependency(dependency, "runtime", source)
47
34
  end + manifest.fetch("packages-dev", []).map do |dependency|
48
- requirement = dependency["version"]
35
+ parse_composer_dependency(dependency, "development", source)
36
+ end
37
+ ParserResult.new(dependencies: dependencies)
38
+ end
49
39
 
50
- # Store Drupal version if Drupal, but include the original manifest version for reference
51
- if drupal_module?(dependency)
52
- original_requirement = requirement
53
- requirement = dependency.dig("source", "reference")
54
- end
40
+ def self.parse_composer_dependency(dependency, type, source)
41
+ requirement = dependency["version"]
42
+ original_requirement = nil
55
43
 
56
- Dependency.new(
57
- name: dependency["name"],
58
- requirement: requirement,
59
- type: "development",
60
- original_requirement: original_requirement,
61
- source: options.fetch(:filename, nil),
62
- platform: platform_name
63
- )
44
+ # Store Drupal version if Drupal, but include the original manifest version for reference
45
+ if drupal_module?(dependency)
46
+ original_requirement = requirement
47
+ requirement = dependency.dig("source", "reference")
64
48
  end
65
- ParserResult.new(dependencies: dependencies)
49
+
50
+ # Extract shasum from dist if present and non-empty
51
+ shasum = dependency.dig("dist", "shasum")
52
+ integrity = shasum && !shasum.empty? ? "sha1=#{shasum}" : nil
53
+
54
+ Dependency.new(
55
+ name: dependency["name"],
56
+ requirement: requirement,
57
+ type: type,
58
+ original_requirement: original_requirement,
59
+ source: source,
60
+ platform: platform_name,
61
+ integrity: integrity
62
+ )
66
63
  end
67
64
 
68
65
  def self.parse_manifest(file_contents, options: {})
@@ -131,12 +131,16 @@ module Bibliothecary
131
131
  name = block[/name\s*=\s*"([^"]+)"/, 1]
132
132
  version = block[/version\s*=\s*"([^"]+)"/, 1]
133
133
 
134
+ # Extract sdist hash: sdist = { url = "...", hash = "sha256:...", size = ... }
135
+ integrity = block[/^sdist\s*=\s*\{[^}]*hash\s*=\s*"([^"]+)"/m, 1]
136
+
134
137
  dependencies << Dependency.new(
135
138
  platform: platform_name,
136
139
  name: name,
137
140
  requirement: version,
138
141
  type: "runtime", # All dependencies are considered runtime
139
- source: source
142
+ source: source,
143
+ integrity: integrity
140
144
  )
141
145
  end
142
146
  ParserResult.new(dependencies: dependencies)
@@ -306,6 +310,15 @@ module Bibliothecary
306
310
 
307
311
  groups = ["runtime"] if groups.empty?
308
312
 
313
+ # Extract sdist hash from files array (look for .tar.gz entry)
314
+ integrity = nil
315
+ if (files_match = block[/^files\s*=\s*\[(.*?)\]/m, 1])
316
+ # Match .tar.gz file entry and extract hash
317
+ if (sdist_match = files_match.match(/\{file\s*=\s*"[^"]+\.tar\.gz",\s*hash\s*=\s*"([^"]+)"\}/))
318
+ integrity = sdist_match[1]
319
+ end
320
+ end
321
+
309
322
  groups.each do |group|
310
323
  # Poetry lockfiles should already contain normalized names, but we'll
311
324
  # apply it here as well just to be consistent with pyproject.toml parsing.
@@ -316,7 +329,8 @@ module Bibliothecary
316
329
  requirement: version,
317
330
  type: group,
318
331
  source: options.fetch(:filename, nil),
319
- platform: platform_name
332
+ platform: platform_name,
333
+ integrity: integrity
320
334
  )
321
335
  end
322
336
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibliothecary
4
+ module Parsers
5
+ class Rpm
6
+ include Bibliothecary::Analyser
7
+
8
+ def self.file_patterns
9
+ ["*.spec"]
10
+ end
11
+
12
+ def self.mapping
13
+ {
14
+ match_extension(".spec") => {
15
+ kind: "manifest",
16
+ parser: :parse_spec,
17
+ can_have_lockfile: false,
18
+ },
19
+ }
20
+ end
21
+
22
+ def self.parse_spec(file_contents, options: {})
23
+ source = options.fetch(:filename, nil)
24
+ dependencies = []
25
+
26
+ # Parse BuildRequires (build dependencies)
27
+ file_contents.scan(/^BuildRequires:\s*(.+)$/i) do |match|
28
+ parse_dependency_line(match[0]).each do |dep|
29
+ dependencies << Dependency.new(
30
+ name: dep[:name],
31
+ requirement: dep[:requirement] || "*",
32
+ type: "build",
33
+ source: source,
34
+ platform: platform_name
35
+ )
36
+ end
37
+ end
38
+
39
+ # Parse Requires (runtime dependencies), including Requires(pre), Requires(post), etc.
40
+ file_contents.scan(/^Requires(?:\([^)]+\))?:\s*(.+)$/i) do |match|
41
+ parse_dependency_line(match[0]).each do |dep|
42
+ dependencies << Dependency.new(
43
+ name: dep[:name],
44
+ requirement: dep[:requirement] || "*",
45
+ type: "runtime",
46
+ source: source,
47
+ platform: platform_name
48
+ )
49
+ end
50
+ end
51
+
52
+ ParserResult.new(dependencies: dependencies)
53
+ end
54
+
55
+ def self.parse_dependency_line(line)
56
+ # Dependencies can be comma or whitespace separated
57
+ # Each dependency can have version constraints like: pkg >= 1.0
58
+ # Also filter out RPM macros like %{name}
59
+ deps = []
60
+
61
+ # Split on commas first, then handle each part
62
+ line.split(/,/).each do |part|
63
+ part = part.strip
64
+ next if part.empty?
65
+ next if part.start_with?("%") # Skip RPM macros
66
+ next if part.start_with?("/") # Skip file paths like /bin/sh
67
+
68
+ # Check for version constraint (pkg >= 1.0, pkg < 2.0, etc.)
69
+ if part =~ /^(\S+)\s+([<>=]+)\s*(\S+)$/
70
+ deps << { name: $1, requirement: "#{$2} #{$3}" }
71
+ elsif part =~ /^(\S+)$/
72
+ deps << { name: $1, requirement: nil }
73
+ end
74
+ end
75
+
76
+ deps
77
+ end
78
+ end
79
+ end
80
+ end