ecosystems-bibliothecary 15.2.0 → 15.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1399499146bc9abb6b2053d1e842cff2a33e05166f4fca7fed6fba222146f5e
4
- data.tar.gz: 2b1460ba23303796a1b73bfa445fee11771857f5270effce6da04e6ed4ff920f
3
+ metadata.gz: c213dba45d6c84c67ecad66538e68fbd7ff843ec5fb6f6b1d80b757ae9ab5f15
4
+ data.tar.gz: a0b4a803b35d16820ed19b071ae8984efb9004ba712ca6457e6f9d9412d331e6
5
5
  SHA512:
6
- metadata.gz: 2ab9631fd2e5472d6a60784685252961f75fcae716561d1e7bd6c1bc7f87bba18f346988b26f787cc13bd666fc06a99db133926726880ee983e01fbe4f64a5d9
7
- data.tar.gz: 1dc6d4efca6e8b643ca7c76e495a6b40364169fc8b51d238f2708e5da1ec8d04515adafec8d2042951088729258b3552a547473c93a59ae3f12c7a89ff681065
6
+ metadata.gz: ade8a88ba728b3d95e333000cdcde1ad8ed9ae885ab775678a1b45c3d3521705f553c293be26ce5d63002f5ec86666e29108b7b456b672c647a58d56af8dc568
7
+ data.tar.gz: e5fdf2c94bde72360926307a0df9cc6d796d476aa983a5236fa5d13691c4bcf74a5e20bd3c0090725ddd37bfad8a4d0733cc6a2d99b6cf25a64a92ac6a7d507b
data/CHANGELOG.md CHANGED
@@ -13,6 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
13
 
14
14
  ### Removed
15
15
 
16
+ ## [15.3.0]
17
+
18
+ ### Added
19
+
20
+ - npm: pnpm-workspace.yaml support for catalog dependencies (pnpm 9+)
21
+ - alpm: Arch Linux PKGBUILD parser for depends, makedepends, and checkdepends
22
+ - apk: Alpine Linux APKBUILD parser for depends, makedepends, and checkdepends
23
+ - deb: Debian control file parser for Build-Depends, Depends, Pre-Depends, Recommends, Suggests
24
+ - rpm: RPM spec file parser for BuildRequires and Requires
25
+ - integrity: Added `integrity` field to Dependency class for lockfile hashes (npm package-lock.json, pnpm-lock.yaml, yarn.lock v4, bun.lock, go.sum, Gemfile.lock, deno.lock, composer.lock, stack.yaml.lock, Cargo.lock, Podfile.lock, mix.lock, rebar.lock, manifest.toml, poetry.lock, uv.lock)
26
+
16
27
  ## [15.2.0]
17
28
 
18
29
  ### Added
data/README.md CHANGED
@@ -42,6 +42,48 @@ Bibliothecary.analyse('./')
42
42
 
43
43
  All available config options are in: https://github.com/ecosyste-ms/bibliothecary/blob/master/lib/bibliothecary/configuration.rb
44
44
 
45
+ ## Dependency fields
46
+
47
+ Each parsed dependency is a `Bibliothecary::Dependency` with:
48
+
49
+ | Field | Type | Description |
50
+ |-------|------|-------------|
51
+ | `name` | String | Package name |
52
+ | `requirement` | String | Version requirement (defaults to `"*"`) |
53
+ | `platform` | String | Package manager platform (e.g. `"npm"`, `"maven"`) |
54
+ | `type` | String | Dependency scope: `"runtime"`, `"development"`, `"test"`, etc. |
55
+ | `direct` | Boolean | Direct dependency (vs transitive) |
56
+ | `deprecated` | Boolean | Deprecated dependency |
57
+ | `local` | Boolean | Local/file path dependency |
58
+ | `optional` | Boolean | Optional dependency |
59
+ | `original_name` | String | Original name before aliasing/normalization |
60
+ | `original_requirement` | String | Original requirement before resolution |
61
+ | `source` | String | Path to the manifest file |
62
+ | `integrity` | String | Lockfile integrity hash (see table below) |
63
+
64
+ ## Integrity hash support
65
+
66
+ The `integrity` field is populated for lockfiles that include per-dependency hashes:
67
+
68
+ | Lockfile | Platform | Hash format |
69
+ |----------|----------|-------------|
70
+ | package-lock.json | npm | `sha512-...` |
71
+ | pnpm-lock.yaml | npm | `sha512-...` |
72
+ | yarn.lock (v2+) | npm | `sha512-...` |
73
+ | bun.lock | npm | `sha512-...` |
74
+ | deno.lock | deno | `sha512-...` |
75
+ | go.sum | go | `h1:...` |
76
+ | Gemfile.lock | rubygems | `sha256=...` |
77
+ | poetry.lock | pypi | `sha256:...` |
78
+ | uv.lock | pypi | `sha256:...` |
79
+ | composer.lock | packagist | `sha1=...` |
80
+ | Cargo.lock | cargo | `sha256=...` |
81
+ | Podfile.lock | cocoapods | `sha1=...` |
82
+ | mix.lock | hex | `sha256=...` |
83
+ | rebar.lock | hex | `sha256=...` |
84
+ | manifest.toml (Gleam) | hex | `sha256=...` |
85
+ | stack.yaml.lock | hackage | `sha256=...` |
86
+
45
87
  ## Supported package manager file formats
46
88
 
47
89
  - Actions
@@ -49,9 +91,13 @@ All available config options are in: https://github.com/ecosyste-ms/bibliothecar
49
91
  - action.yaml
50
92
  - .github/workflows/\*.yml
51
93
  - .github/workflows/\*.yaml
94
+ - Alpm
95
+ - PKGBUILD
52
96
  - Anaconda
53
97
  - environment.yml
54
98
  - environment.yaml
99
+ - Apk
100
+ - APKBUILD
55
101
  - BentoML
56
102
  - bentofile.yaml
57
103
  - Bower
@@ -86,6 +132,9 @@ All available config options are in: https://github.com/ecosyste-ms/bibliothecar
86
132
  - CRAN
87
133
  - DESCRIPTION
88
134
  - renv.lock
135
+ - Deb
136
+ - debian/control
137
+ - control
89
138
  - Deno
90
139
  - deno.json
91
140
  - deno.jsonc
@@ -165,6 +214,7 @@ All available config options are in: https://github.com/ecosyste-ms/bibliothecar
165
214
  - npm-shrinkwrap.json
166
215
  - yarn.lock
167
216
  - pnpm-lock.yaml
217
+ - pnpm-workspace.yaml
168
218
  - bun.lock
169
219
  - npm-ls.json
170
220
  - Nuget
@@ -200,6 +250,8 @@ All available config options are in: https://github.com/ecosyste-ms/bibliothecar
200
250
  - pdm.lock
201
251
  - pip-resolved-dependencies.txt
202
252
  - pip-dependency-graph.json
253
+ - Rpm
254
+ - \*.spec
203
255
  - RubyGems
204
256
  - Gemfile
205
257
  - Gemfile.lock
@@ -18,6 +18,8 @@ module Bibliothecary
18
18
  # for cases where it did not match the resolved name. This can be used for features like aliasing.
19
19
  # @source [String] source An optional string to store the location of the manifest that contained this
20
20
  # dependency, e.g. "src/package.json".
21
+ # @attr_reader [String] integrity An optional integrity hash from the lockfile, stored as-is
22
+ # (e.g. "sha512-abc123..." for npm, "h1:xyz..." for go.sum).
21
23
  class Dependency
22
24
  FIELDS = %i[
23
25
  name
@@ -31,6 +33,7 @@ module Bibliothecary
31
33
  optional
32
34
  original_name
33
35
  source
36
+ integrity
34
37
  ].freeze
35
38
 
36
39
  attr_reader(*FIELDS)
@@ -46,7 +49,8 @@ module Bibliothecary
46
49
  local: nil,
47
50
  optional: nil,
48
51
  original_name: nil,
49
- source: nil
52
+ source: nil,
53
+ integrity: nil
50
54
  )
51
55
  @name = name
52
56
  @platform = platform
@@ -59,6 +63,7 @@ module Bibliothecary
59
63
  @optional = optional
60
64
  @original_name = original_name
61
65
  @source = source
66
+ @integrity = integrity
62
67
  end
63
68
 
64
69
  def eql?(other)
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibliothecary
4
+ module Parsers
5
+ class Alpm
6
+ include Bibliothecary::Analyser
7
+
8
+ def self.file_patterns
9
+ ["PKGBUILD"]
10
+ end
11
+
12
+ def self.mapping
13
+ {
14
+ match_filename("PKGBUILD") => {
15
+ kind: "manifest",
16
+ parser: :parse_pkgbuild,
17
+ can_have_lockfile: false,
18
+ },
19
+ }
20
+ end
21
+
22
+ def self.parse_pkgbuild(file_contents, options: {})
23
+ source = options.fetch(:filename, "PKGBUILD")
24
+ dependencies = []
25
+
26
+ # Parse depends (runtime)
27
+ extract_variable(file_contents, "depends").each do |dep|
28
+ name, requirement = parse_dependency(dep)
29
+ dependencies << Dependency.new(
30
+ name: name,
31
+ requirement: requirement || "*",
32
+ type: "runtime",
33
+ source: source,
34
+ platform: platform_name
35
+ )
36
+ end
37
+
38
+ # Parse makedepends (build)
39
+ extract_variable(file_contents, "makedepends").each do |dep|
40
+ name, requirement = parse_dependency(dep)
41
+ dependencies << Dependency.new(
42
+ name: name,
43
+ requirement: requirement || "*",
44
+ type: "build",
45
+ source: source,
46
+ platform: platform_name
47
+ )
48
+ end
49
+
50
+ # Parse checkdepends (test)
51
+ extract_variable(file_contents, "checkdepends").each do |dep|
52
+ name, requirement = parse_dependency(dep)
53
+ dependencies << Dependency.new(
54
+ name: name,
55
+ requirement: requirement || "*",
56
+ type: "test",
57
+ source: source,
58
+ platform: platform_name
59
+ )
60
+ end
61
+
62
+ ParserResult.new(dependencies: dependencies)
63
+ end
64
+
65
+ def self.extract_variable(contents, var_name)
66
+ # PKGBUILD uses bash array syntax: depends=('pkg1' 'pkg2') or depends=(pkg1 pkg2)
67
+ # Can also span multiple lines
68
+ pattern = /^#{var_name}=\(([^)]*)\)/m
69
+ match = contents.match(pattern)
70
+ return [] unless match
71
+
72
+ # Extract items, handling both quoted and unquoted formats
73
+ # 'pkg1' 'pkg2' or "pkg1" "pkg2" or pkg1 pkg2
74
+ items = match[1].scan(/['"]([^'"]+)['"]|(\S+)/).flatten.compact
75
+ items.reject { |d| d.empty? || d.start_with?("$") }
76
+ end
77
+
78
+ def self.parse_dependency(dep_string)
79
+ # Parse version constraints like "glibc>=2.17" or "openssl>1.1"
80
+ # Operators: >=, <=, >, <, =
81
+ if dep_string =~ /^(.+?)([><=]+)(.+)$/
82
+ [$1, "#{$2}#{$3}"]
83
+ else
84
+ [dep_string, nil]
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibliothecary
4
+ module Parsers
5
+ class Apk
6
+ include Bibliothecary::Analyser
7
+
8
+ def self.file_patterns
9
+ ["APKBUILD"]
10
+ end
11
+
12
+ def self.mapping
13
+ {
14
+ match_filename("APKBUILD") => {
15
+ kind: "manifest",
16
+ parser: :parse_apkbuild,
17
+ can_have_lockfile: false,
18
+ },
19
+ }
20
+ end
21
+
22
+ def self.parse_apkbuild(file_contents, options: {})
23
+ source = options.fetch(:filename, "APKBUILD")
24
+ dependencies = []
25
+
26
+ # Parse depends (runtime)
27
+ extract_variable(file_contents, "depends").each do |dep|
28
+ name, requirement = parse_dependency(dep)
29
+ dependencies << Dependency.new(
30
+ name: name,
31
+ requirement: requirement || "*",
32
+ type: "runtime",
33
+ source: source,
34
+ platform: platform_name
35
+ )
36
+ end
37
+
38
+ # Parse makedepends (build)
39
+ extract_variable(file_contents, "makedepends").each do |dep|
40
+ name, requirement = parse_dependency(dep)
41
+ dependencies << Dependency.new(
42
+ name: name,
43
+ requirement: requirement || "*",
44
+ type: "build",
45
+ source: source,
46
+ platform: platform_name
47
+ )
48
+ end
49
+
50
+ # Parse checkdepends (test)
51
+ extract_variable(file_contents, "checkdepends").each do |dep|
52
+ name, requirement = parse_dependency(dep)
53
+ dependencies << Dependency.new(
54
+ name: name,
55
+ requirement: requirement || "*",
56
+ type: "test",
57
+ source: source,
58
+ platform: platform_name
59
+ )
60
+ end
61
+
62
+ ParserResult.new(dependencies: dependencies)
63
+ end
64
+
65
+ def self.extract_variable(contents, var_name)
66
+ # Match variable assignment with double or single quotes, handling multi-line with backslash
67
+ # Examples:
68
+ # depends="foo bar"
69
+ # makedepends="foo
70
+ # bar"
71
+ # checkdepends='foo bar'
72
+ pattern = /^#{var_name}=["']([^"']*?)["']/m
73
+ match = contents.match(pattern)
74
+ return [] unless match
75
+
76
+ # Split on whitespace and filter out empty strings, negated packages (!), and variable references ($)
77
+ match[1].split(/\s+/).reject { |d| d.empty? || d.start_with?("!") || d.start_with?("$") }
78
+ end
79
+
80
+ def self.parse_dependency(dep_string)
81
+ # Parse version constraints like "openssl-dev>3" or "zlib-dev>=1.2.3"
82
+ # Operators: >=, <=, >, <, =, ~
83
+ if dep_string =~ /^(.+?)([><=~]+)(.+)$/
84
+ [$1, "#{$2}#{$3}"]
85
+ else
86
+ [dep_string, nil]
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -14,7 +14,7 @@ module Bibliothecary
14
14
  match_filename("bentofile.yaml") => {
15
15
  kind: 'manifest',
16
16
  parser: :parse_bentofile,
17
- related_to: [ 'manifest' ]
17
+ can_have_lockfile: false,
18
18
  }
19
19
  }
20
20
  end
@@ -16,6 +16,7 @@ module Bibliothecary
16
16
  match_filename("bower.json") => {
17
17
  kind: "manifest",
18
18
  parser: :parse_manifest,
19
+ can_have_lockfile: false,
19
20
  },
20
21
  }
21
22
  end
@@ -55,6 +55,7 @@ module Bibliothecary
55
55
  name = block[/name\s*=\s*"([^"]+)"/, 1]
56
56
  version = block[/version\s*=\s*"([^"]+)"/, 1]
57
57
  source = block[/source\s*=\s*"([^"]+)"/, 1]
58
+ checksum = block[/checksum\s*=\s*"([^"]+)"/, 1]
58
59
 
59
60
  # Skip packages without a registry source (local/workspace packages)
60
61
  next unless source&.start_with?("registry+")
@@ -64,7 +65,8 @@ module Bibliothecary
64
65
  requirement: version,
65
66
  type: "runtime",
66
67
  source: options.fetch(:filename, nil),
67
- platform: platform_name
68
+ platform: platform_name,
69
+ integrity: checksum ? "sha256=#{checksum}" : nil
68
70
  )
69
71
  end
70
72
  ParserResult.new(dependencies: dependencies)
@@ -16,6 +16,7 @@ module Bibliothecary
16
16
  match_filename("project.clj") => {
17
17
  kind: "manifest",
18
18
  parser: :parse_manifest,
19
+ can_have_lockfile: false,
19
20
  },
20
21
  }
21
22
  end
@@ -49,6 +49,9 @@ module Bibliothecary
49
49
  source = options.fetch(:filename, nil)
50
50
  dependencies = []
51
51
 
52
+ # Parse SPEC CHECKSUMS section to build lookup table
53
+ checksums = parse_spec_checksums(file_contents)
54
+
52
55
  # Match pod entries: " - Name (version)" or " - Name/Subspec (version)"
53
56
  # Only process lines in PODS section (before DEPENDENCIES section)
54
57
  pods_section = file_contents.split(/^DEPENDENCIES:/)[0]
@@ -60,13 +63,38 @@ module Bibliothecary
60
63
  name: base_name,
61
64
  requirement: version,
62
65
  type: "runtime",
63
- source: source
66
+ source: source,
67
+ integrity: checksums[base_name]
64
68
  )
65
69
  end
66
70
 
67
71
  ParserResult.new(dependencies: dependencies)
68
72
  end
69
73
 
74
+ def self.parse_spec_checksums(file_contents)
75
+ checksums = {}
76
+ in_checksums = false
77
+
78
+ file_contents.each_line do |line|
79
+ if line.start_with?("SPEC CHECKSUMS:")
80
+ in_checksums = true
81
+ next
82
+ end
83
+
84
+ next unless in_checksums
85
+
86
+ # End of section (blank line or new section)
87
+ break if line.strip.empty? || (line !~ /^\s/ && line.strip != "")
88
+
89
+ # Match " Name: sha1hash"
90
+ if (match = line.match(/^\s+([^:]+):\s*([a-f0-9]+)\s*$/))
91
+ checksums[match[1]] = "sha1=#{match[2]}"
92
+ end
93
+ end
94
+
95
+ checksums
96
+ end
97
+
70
98
  def self.parse_podspec(file_contents, options: {})
71
99
  source = options.fetch(:filename, nil)
72
100
  deps = []
@@ -14,7 +14,7 @@ module Bibliothecary
14
14
  match_filename("cog.yaml") => {
15
15
  kind: 'manifest',
16
16
  parser: :parse_cog_yaml,
17
- related_to: [ 'manifest' ]
17
+ can_have_lockfile: false,
18
18
  }
19
19
  }
20
20
  end
@@ -16,10 +16,12 @@ module Bibliothecary
16
16
  match_filename("environment.yml") => {
17
17
  parser: :parse_conda,
18
18
  kind: "manifest",
19
+ can_have_lockfile: false,
19
20
  },
20
21
  match_filename("environment.yaml") => {
21
22
  parser: :parse_conda,
22
23
  kind: "manifest",
24
+ can_have_lockfile: false,
23
25
  },
24
26
  }
25
27
  end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibliothecary
4
+ module Parsers
5
+ class Deb
6
+ include Bibliothecary::Analyser
7
+
8
+ def self.file_patterns
9
+ ["debian/control", "control"]
10
+ end
11
+
12
+ def self.mapping
13
+ {
14
+ match_filename("debian/control") => {
15
+ kind: "manifest",
16
+ parser: :parse_control,
17
+ can_have_lockfile: false,
18
+ },
19
+ match_filename("control") => {
20
+ kind: "manifest",
21
+ parser: :parse_control,
22
+ can_have_lockfile: false,
23
+ },
24
+ }
25
+ end
26
+
27
+ BUILD_DEP_FIELDS = %w[Build-Depends Build-Depends-Indep Build-Depends-Arch].freeze
28
+ RUNTIME_DEP_FIELDS = %w[Depends Pre-Depends Recommends Suggests].freeze
29
+
30
+ def self.parse_control(file_contents, options: {})
31
+ source = options.fetch(:filename, nil)
32
+ dependencies = []
33
+
34
+ # Parse the control file - it's in RFC 822 format with continuation lines
35
+ # Fields can span multiple lines if continuation lines start with whitespace
36
+ fields = parse_fields(file_contents)
37
+
38
+ # Build dependencies
39
+ BUILD_DEP_FIELDS.each do |field_name|
40
+ next unless fields[field_name.downcase]
41
+
42
+ parse_dependency_list(fields[field_name.downcase]).each do |dep|
43
+ dependencies << Dependency.new(
44
+ name: dep[:name],
45
+ requirement: dep[:requirement] || "*",
46
+ type: "build",
47
+ source: source,
48
+ platform: platform_name
49
+ )
50
+ end
51
+ end
52
+
53
+ # Runtime dependencies
54
+ RUNTIME_DEP_FIELDS.each do |field_name|
55
+ next unless fields[field_name.downcase]
56
+
57
+ parse_dependency_list(fields[field_name.downcase]).each do |dep|
58
+ dependencies << Dependency.new(
59
+ name: dep[:name],
60
+ requirement: dep[:requirement] || "*",
61
+ type: "runtime",
62
+ source: source,
63
+ platform: platform_name
64
+ )
65
+ end
66
+ end
67
+
68
+ ParserResult.new(dependencies: dependencies)
69
+ end
70
+
71
+ def self.parse_fields(contents)
72
+ fields = {}
73
+ current_field = nil
74
+ current_value = []
75
+
76
+ contents.each_line do |line|
77
+ if line =~ /^(\S+):\s*(.*)$/
78
+ # Save previous field
79
+ if current_field
80
+ fields[current_field] = current_value.join(" ").strip
81
+ end
82
+ current_field = $1.downcase
83
+ current_value = [$2]
84
+ elsif line =~ /^\s+(.*)$/ && current_field
85
+ # Continuation line
86
+ current_value << $1
87
+ elsif line.strip.empty?
88
+ # Empty line - save current field and reset
89
+ if current_field
90
+ fields[current_field] = current_value.join(" ").strip
91
+ end
92
+ current_field = nil
93
+ current_value = []
94
+ end
95
+ end
96
+
97
+ # Save last field
98
+ if current_field
99
+ fields[current_field] = current_value.join(" ").strip
100
+ end
101
+
102
+ fields
103
+ end
104
+
105
+ def self.parse_dependency_list(dep_string)
106
+ deps = []
107
+
108
+ # Dependencies are comma-separated
109
+ dep_string.split(/,/).each do |dep|
110
+ dep = dep.strip
111
+ next if dep.empty?
112
+ next if dep.start_with?("$") # Skip substitution variables like ${shlibs:Depends}
113
+
114
+ # Handle alternatives (pkg1 | pkg2) - just take the first one
115
+ dep = dep.split("|").first.strip
116
+
117
+ # Parse package name and optional version constraint
118
+ # Format: package (>= 1.0) or just package
119
+ if dep =~ /^(\S+)\s*\(([^)]+)\)$/
120
+ name = $1
121
+ constraint = $2.strip
122
+ deps << { name: name, requirement: constraint }
123
+ elsif dep =~ /^(\S+)$/
124
+ deps << { name: $1, requirement: nil }
125
+ end
126
+ end
127
+
128
+ deps
129
+ end
130
+ end
131
+ end
132
+ end
@@ -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