pray-cli 1.16.0 → 1.18.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/lib/pray/apply_report.rb +49 -0
  4. data/lib/pray/cli/commands/init.rb +1 -20
  5. data/lib/pray/cli/commands/init_prayer.rb +76 -0
  6. data/lib/pray/cli/commands/init_prayer_manifest.rb +102 -0
  7. data/lib/pray/cli/commands/packages.rb +13 -2
  8. data/lib/pray/cli/commands/trust.rb +3 -3
  9. data/lib/pray/cli/commands/update_latest.rb +14 -16
  10. data/lib/pray/cli/commands/workflow.rb +5 -0
  11. data/lib/pray/cli/help.rb +52 -15
  12. data/lib/pray/cli/helpers.rb +13 -5
  13. data/lib/pray/cli/parse.rb +21 -1
  14. data/lib/pray/cli.rb +3 -1
  15. data/lib/pray/destination.rb +32 -2
  16. data/lib/pray/distribution.rb +94 -0
  17. data/lib/pray/format_serialize.rb +1 -4
  18. data/lib/pray/git_cache.rb +192 -0
  19. data/lib/pray/git_clone.rb +32 -0
  20. data/lib/pray/git_materialize.rb +76 -0
  21. data/lib/pray/git_run.rb +41 -0
  22. data/lib/pray/git_sources.rb +60 -145
  23. data/lib/pray/local_prayer.rb +38 -0
  24. data/lib/pray/lockfile.rb +8 -2
  25. data/lib/pray/manifest.rb +6 -3
  26. data/lib/pray/manifest_json.rb +1 -0
  27. data/lib/pray/manifest_parser_blocks.rb +15 -15
  28. data/lib/pray/materialize.rb +7 -1
  29. data/lib/pray/package_spec.rb +25 -13
  30. data/lib/pray/package_spec_render.rb +4 -3
  31. data/lib/pray/package_spec_version.rb +33 -0
  32. data/lib/pray/plan.rb +2 -1
  33. data/lib/pray/publish.rb +27 -2
  34. data/lib/pray/registry.rb +4 -15
  35. data/lib/pray/registry_http_cache.rb +17 -0
  36. data/lib/pray/render.rb +6 -36
  37. data/lib/pray/render_managed.rb +64 -0
  38. data/lib/pray/render_patch.rb +63 -12
  39. data/lib/pray/resolve.rb +6 -37
  40. data/lib/pray/resolve_local.rb +39 -0
  41. data/lib/pray/resolve_source.rb +5 -0
  42. data/lib/pray/serve.rb +15 -4
  43. data/lib/pray/serve_range.rb +36 -0
  44. data/lib/pray/torrent_manifest.rb +78 -0
  45. data/lib/pray/upstream_drift.rb +42 -0
  46. data/lib/pray/upstream_merge.rb +25 -0
  47. data/lib/pray/upstream_refresh.rb +28 -2
  48. data/lib/pray/verify.rb +2 -2
  49. data/lib/pray/version.rb +2 -2
  50. data/lib/pray.rb +10 -0
  51. metadata +17 -1
@@ -1,48 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require "fileutils"
5
3
  require "pathname"
4
+ require_relative "git_cache"
6
5
 
7
6
  module Pray
8
7
  GitSourceCheckout = Struct.new(:cache_directory, :revision, :subdir)
9
8
 
9
+ class GitSourceSet
10
+ def initialize(project_root, sources, lockfile, refresh:)
11
+ @project_root = project_root
12
+ @lockfile = lockfile
13
+ @refresh = refresh
14
+ @sources = sources.select { |source| source.kind == "git" }.to_h { |source| [source.name, source] }
15
+ @checkouts = {}
16
+ end
17
+
18
+ def [](name)
19
+ return @checkouts[name] if @checkouts.key?(name)
20
+
21
+ source = @sources[name]
22
+ return nil unless source
23
+
24
+ checkout = GitSources.prepare_one_git_source(@project_root, source, @lockfile, refresh: @refresh)
25
+ @checkouts[name] = checkout if checkout
26
+ checkout
27
+ end
28
+
29
+ def revisions
30
+ @checkouts.each_with_object({}) do |(name, checkout), collected|
31
+ next if checkout.revision.to_s.empty?
32
+
33
+ collected[name] = checkout.revision
34
+ end
35
+ end
36
+ end
37
+
10
38
  module GitSources
11
39
  module_function
12
40
 
13
41
  def prepare_git_sources(project_root, sources, lockfile, refresh: false)
14
- checkouts = {}
15
- sources.each do |source|
16
- next unless source.kind == "git"
17
-
18
- clone_url = source.url.delete_prefix("git+")
19
- if local_filesystem_source?(clone_url) && !local_git_repo_path(project_root, clone_url)
20
- source_root = local_git_source_root(project_root, clone_url)
21
- if source_root
22
- checkouts[source.name] = GitSourceCheckout.new(
23
- cache_directory: source_root,
24
- revision: "",
25
- subdir: source.subdir
26
- )
27
- end
28
- next
29
- end
30
-
31
- pinned_revision = refresh ? nil : pinned_revision_for_source(lockfile, source)
32
- cache_directory, revision = ensure_git_repository(
33
- project_root,
34
- clone_url,
35
- refresh: refresh,
36
- pinned_revision: pinned_revision,
37
- sparse_subdir: source.subdir
38
- )
39
- checkouts[source.name] = GitSourceCheckout.new(
40
- cache_directory: cache_directory,
41
- revision: revision,
42
+ GitSourceSet.new(project_root, sources, lockfile, refresh: refresh)
43
+ end
44
+
45
+ def prepare_one_git_source(project_root, source, lockfile, refresh:)
46
+ clone_url = source.url.delete_prefix("git+")
47
+ if local_filesystem_source?(clone_url) && !local_git_repo_path(project_root, clone_url)
48
+ source_root = local_git_source_root(project_root, clone_url)
49
+ return unless source_root
50
+
51
+ return GitSourceCheckout.new(
52
+ cache_directory: source_root,
53
+ revision: "",
42
54
  subdir: source.subdir
43
55
  )
44
56
  end
45
- checkouts
57
+
58
+ pinned_revision = refresh ? nil : pinned_revision_for_source(lockfile, source)
59
+ cache_directory, revision = GitCache.ensure_git_repository(
60
+ project_root,
61
+ clone_url,
62
+ refresh: refresh,
63
+ pinned_revision: pinned_revision,
64
+ sparse_subdir: source.subdir
65
+ )
66
+ GitSourceCheckout.new(
67
+ cache_directory: cache_directory,
68
+ revision: revision,
69
+ subdir: source.subdir
70
+ )
46
71
  end
47
72
 
48
73
  def resolve_distribution_root(repo_root, subdir)
@@ -83,39 +108,12 @@ module Pray
83
108
  discover_distribution_root(path)
84
109
  end
85
110
 
86
- def ensure_git_repository(project_root, clone_url, refresh:, pinned_revision:, sparse_subdir:)
87
- cache_directory = git_source_cache_directory(project_root, clone_url)
88
- if File.directory?(File.join(cache_directory, ".git"))
89
- refresh_global_git_cache(clone_url) if refresh
90
- if pinned_revision
91
- checkout_git_revision(cache_directory, clone_url, pinned_revision, refresh)
92
- elsif refresh
93
- refresh_git_worktree(cache_directory, clone_url)
94
- end
95
- apply_sparse_checkout(cache_directory, sparse_subdir) if sparse_subdir
96
- revision = git_head_revision(cache_directory)
97
- return [cache_directory, revision]
98
- end
99
-
100
- FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
101
- FileUtils.mkdir_p(File.dirname(cache_directory))
102
- if seed_git_cache_from_global(clone_url, cache_directory, project_root)
103
- run_git_in_repo(cache_directory, "remote", "set-url", "origin", clone_url)
104
- else
105
- run_git(project_root, "clone", "--depth", "1", clone_url, cache_directory)
106
- mirror_git_cache_to_global(clone_url, cache_directory)
107
- end
108
- checkout_git_revision(cache_directory, clone_url, pinned_revision, true) if pinned_revision
109
- apply_sparse_checkout(cache_directory, sparse_subdir) if sparse_subdir
110
- [cache_directory, git_head_revision(cache_directory)]
111
- end
112
-
113
- def git_source_cache_directory(project_root, clone_url)
114
- File.join(project_root, ".pray", "cache", "git", cache_key(clone_url))
111
+ def git_source_cache_directory(project_root, clone_url, subdir = nil)
112
+ GitCache.git_source_cache_directory(project_root, clone_url, subdir)
115
113
  end
116
114
 
117
- def cache_key(text)
118
- Hashing.sha256_prefixed(text)[7, 16]
115
+ def git_source_cached_repository(project_root, clone_url)
116
+ GitCache.git_source_cached_repository(project_root, clone_url)
119
117
  end
120
118
 
121
119
  def pinned_revision_for_source(lockfile, source)
@@ -142,88 +140,5 @@ module Pray
142
140
  path = clone_url.delete_prefix("file://")
143
141
  Pathname.new(path).absolute? ? path : File.expand_path(path, project_root)
144
142
  end
145
-
146
- def global_cache_root
147
- return ENV["PRAY_CACHE"] if ENV["PRAY_CACHE"]
148
- return File.join(ENV["PRAY_HOME"], "cache") if ENV["PRAY_HOME"]
149
-
150
- home = ENV["HOME"]
151
- home ? File.join(home, ".cache", "pray") : nil
152
- end
153
-
154
- def global_git_cache_directory(clone_url)
155
- root = global_cache_root
156
- root ? File.join(root, "git", cache_key(clone_url)) : nil
157
- end
158
-
159
- def global_git_cache_ready?(global_cache)
160
- File.directory?(File.join(global_cache, ".git")) || File.file?(File.join(global_cache, "HEAD"))
161
- end
162
-
163
- def seed_git_cache_from_global(clone_url, destination, working_directory)
164
- global_cache = global_git_cache_directory(clone_url)
165
- return false unless global_cache && global_git_cache_ready?(global_cache)
166
-
167
- run_git(working_directory, "clone", "--depth", "1", "--quiet", global_cache, destination)
168
- true
169
- end
170
-
171
- def mirror_git_cache_to_global(clone_url, project_cache)
172
- global_cache = global_git_cache_directory(clone_url)
173
- return unless global_cache
174
- return if global_git_cache_ready?(global_cache)
175
-
176
- FileUtils.mkdir_p(File.dirname(global_cache))
177
- FileUtils.rm_rf(global_cache) if File.exist?(global_cache)
178
- run_git(File.dirname(project_cache), "clone", "--bare", "--quiet", File.basename(project_cache), global_cache)
179
- end
180
-
181
- def apply_sparse_checkout(repository, subdir)
182
- run_git_in_repo(repository, "sparse-checkout", "init", "--cone")
183
- run_git_in_repo(repository, "sparse-checkout", "set", subdir)
184
- end
185
-
186
- def checkout_git_revision(repository, _clone_url, revision, refresh)
187
- run_git_in_repo(repository, "fetch", "--depth", "1", "origin", revision) if refresh
188
- run_git_in_repo(repository, "checkout", "--force", revision)
189
- end
190
-
191
- def refresh_git_worktree(repository, _clone_url)
192
- run_git_in_repo(repository, "fetch", "--depth", "1", "origin")
193
- run_git_in_repo(repository, "reset", "--hard", "origin/HEAD")
194
- end
195
-
196
- def refresh_global_git_cache(clone_url)
197
- global_cache = global_git_cache_directory(clone_url)
198
- return unless global_cache && global_git_cache_ready?(global_cache)
199
-
200
- run_git_in_repo(global_cache, "fetch", "--depth", "1", "origin")
201
- end
202
-
203
- def git_head_revision(repository)
204
- output, status = Open3.capture2e("git", "-C", repository, "rev-parse", "HEAD")
205
- raise Error.resolution(command_error("git rev-parse HEAD", output)) unless status.success?
206
-
207
- revision = output.strip
208
- raise Error.resolution("git repository has no HEAD revision") if revision.empty?
209
-
210
- revision
211
- end
212
-
213
- def run_git(cwd, *arguments)
214
- output, status = Open3.capture2e("git", "-C", cwd, *arguments)
215
- return if status.success?
216
-
217
- raise Error.resolution(command_error("git #{arguments.join(" ")}", output))
218
- end
219
-
220
- def run_git_in_repo(repository, *arguments)
221
- run_git(repository, *arguments)
222
- end
223
-
224
- def command_error(program, output)
225
- message = output.strip
226
- message.empty? ? "#{program} failed" : "#{program} failed: #{message}"
227
- end
228
143
  end
229
144
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ DEFAULT_LOCAL_PRAYER_NAME = "project"
5
+ DEFAULT_PATH_SOURCE_NAME = "local"
6
+ DEFAULT_PATH_SOURCE_DIRECTORY = "prayers"
7
+
8
+ module LocalPrayer
9
+ module_function
10
+
11
+ def validate_name!(name)
12
+ trimmed = name.to_s.strip
13
+ raise Error.usage("prayer name is missing") if trimmed.empty?
14
+ if trimmed.include?("/") || trimmed.include?("\\") || trimmed == "." || trimmed == ".."
15
+ raise Error.usage("prayer name must be a single folder name")
16
+ end
17
+ if reserved_distribution_layout_name?(trimmed)
18
+ raise Error.usage("#{trimmed} is reserved for the distribution layout")
19
+ end
20
+
21
+ trimmed
22
+ end
23
+
24
+ def path_source_package_directory(source_name, package_name)
25
+ prefix = "#{source_name}/"
26
+ rest = package_name.delete_prefix(prefix)
27
+ if rest != package_name && !rest.empty? && !rest.include?("/") && !rest.include?("\\")
28
+ return rest
29
+ end
30
+
31
+ package_name.to_s.tr("/", "-").tr("\\", "-")
32
+ end
33
+
34
+ def reserved_distribution_layout_name?(name)
35
+ name.match?(/\Av[0-9]+\z/)
36
+ end
37
+ end
38
+ end
data/lib/pray/lockfile.rb CHANGED
@@ -5,6 +5,8 @@ require "perfect_toml"
5
5
  require_relative "lockfile_serialize"
6
6
 
7
7
  module Pray
8
+ LOCAL_EMBED_PACKAGE = "local"
9
+
8
10
  LockSource = Struct.new(:name, :kind, :url, :revision, :host_key_fingerprint)
9
11
  LockedPackage = Struct.new(
10
12
  :name, :version, :source, :path, :tree_hash, :artifact_hash, :artifact,
@@ -20,7 +22,11 @@ module Pray
20
22
  ManagedSpanRecord = Struct.new(
21
23
  :id, :target, :open_line, :close_line, :ideal_checksum, :package, :export,
22
24
  :source_checksum, :silenced
23
- )
25
+ ) do
26
+ def local_embed?
27
+ package == LOCAL_EMBED_PACKAGE
28
+ end
29
+ end
24
30
  ProvisionedFileRecord = Struct.new(:path, :content_hash, :package, :export)
25
31
 
26
32
  Lockfile = Struct.new(
@@ -179,7 +185,7 @@ module Pray
179
185
  package: packages.map do |package|
180
186
  LockedPackage.new(
181
187
  name: package.declaration.name,
182
- version: package.spec.version,
188
+ version: package.spec.recorded_version,
183
189
  source: package.declaration.source,
184
190
  path: relative_lockfile_path(project_root, package.root),
185
191
  tree_hash: package.tree_hash,
data/lib/pray/manifest.rb CHANGED
@@ -46,8 +46,8 @@ module Pray
46
46
  end
47
47
  end
48
48
 
49
- ManifestLocal = Struct.new(:path, :position, :optional, :bound) do
50
- def initialize(path:, position: "after", optional: false, bound: false)
49
+ ManifestLocal = Struct.new(:path, :position, :optional, :bound, :file) do
50
+ def initialize(path:, position: "after", optional: false, bound: false, file: nil)
51
51
  super
52
52
  end
53
53
  end
@@ -135,7 +135,10 @@ module Pray
135
135
  PathSafety.validate_project_relative_path!(package.path) if package.path
136
136
  PathSafety.validate_destination_path!(package.file) if package.file
137
137
  end
138
- manifest.local.each { |local| PathSafety.validate_project_relative_path!(local.path) }
138
+ manifest.local.each do |local|
139
+ PathSafety.validate_project_relative_path!(local.path)
140
+ PathSafety.validate_destination_path!(local.file) if local.file
141
+ end
139
142
  end
140
143
  end
141
144
 
@@ -87,6 +87,7 @@ module Pray
87
87
  "optional" => entry.optional
88
88
  }
89
89
  fields["bound"] = true if entry.bound
90
+ fields["file"] = entry.file if entry.file
90
91
  fields
91
92
  end
92
93
 
@@ -116,7 +116,7 @@ module Pray
116
116
  while (statement = next_statement)
117
117
  if statement == "end"
118
118
  unless saw_package
119
- raise Error.parse("manifest", "file block requires a pray package declaration")
119
+ raise Error.parse("manifest", "file block requires a pray package")
120
120
  end
121
121
  return
122
122
  end
@@ -132,7 +132,15 @@ module Pray
132
132
  return true
133
133
  end
134
134
  if (match = statement.match(/\A(?:pray|use|include|package) (.+)\z/))
135
- bind_file_package(manifest, match[1], file_path)
135
+ rest = match[1]
136
+ values, keywords = parse_call(rest)
137
+ if values.length == 1 && keywords.empty?
138
+ first = string_from_value(values.first)
139
+ if Destination.local_path_form?(first)
140
+ raise Error.parse("manifest", "file: requires a package")
141
+ end
142
+ end
143
+ bind_file_package(manifest, rest, file_path)
136
144
  return true
137
145
  end
138
146
  raise Error.parse("manifest", "unsupported statement inside file block: #{statement}")
@@ -161,19 +169,11 @@ module Pray
161
169
  end
162
170
 
163
171
  def apply_local_pray_path(manifest, first, keywords, values, destination_index)
164
- return false if package_signal?(values, keywords)
165
- return false unless Destination.local_path_form?(first)
166
-
167
- in_compose = destination_index &&
168
- manifest.targets[destination_index]&.mode == "compose"
169
- unless in_compose
170
- raise Error.parse("manifest", "local pray paths are only valid inside compose blocks")
171
- end
172
-
173
- local = ManifestLocal.new(path: first, position: "after", optional: false, bound: true)
174
- Destination.bind_local_entry(manifest.targets[destination_index], local.path)
175
- Destination.upsert_local(manifest, local)
176
- true
172
+ file_dest = keywords["file"]&.as_string
173
+ other_package_signal = values.length > 1 ||
174
+ %w[source export exports optional path git tag rev tarball oci targets features]
175
+ .any? { |key| keywords.key?(key) }
176
+ Destination.try_apply_local_pray(manifest, first, file_dest, other_package_signal, destination_index)
177
177
  end
178
178
 
179
179
  def package_signal?(values, keywords)
@@ -54,9 +54,15 @@ module Pray
54
54
  raise
55
55
  end
56
56
  end
57
+ previous_lockfile = project.previous_lockfile
58
+ unless locked || frozen
59
+ if Upstream.apply_path_upstream_refreshes(project, previous_lockfile, nil, options)
60
+ project = Resolve.resolve_project_in_context(manifest_path, project_root, options)
61
+ previous_lockfile = project.previous_lockfile
62
+ end
63
+ end
57
64
  rendered = Render.render_project(project)
58
65
  lockfile_path = default_lockfile_path(project.project_root)
59
- previous_lockfile = project.previous_lockfile
60
66
  next_lockfile = LockfileIO.build_lockfile(
61
67
  project.manifest_hash,
62
68
  project.environment,
@@ -12,12 +12,12 @@ module Pray
12
12
  PackageUpstream = Struct.new(:name, :constraint, keyword_init: true)
13
13
 
14
14
  PackageSpec = Struct.new(
15
- :name, :version, :summary, :description, :authors, :license, :homepage,
15
+ :name, :version, :summary, :description, :authors, :maintainers, :license, :homepage,
16
16
  :source_code_uri, :changelog_uri, :prayfile_version, :files, :exports,
17
17
  :skills, :templates, :adapters, :targets, :dependencies, :metadata, :upstream
18
18
  ) do
19
19
  def initialize(
20
- name: "", version: "", summary: nil, description: nil, authors: [], license: nil,
20
+ name: "", version: "", summary: nil, description: nil, authors: [], maintainers: [], license: nil,
21
21
  homepage: nil, source_code_uri: nil, changelog_uri: nil, prayfile_version: nil,
22
22
  files: [], exports: {}, skills: {}, templates: {}, adapters: {}, targets: [],
23
23
  dependencies: [], metadata: {}, upstream: nil
@@ -47,8 +47,9 @@ module Pray
47
47
  def canonicalized
48
48
  dup.tap do |copy|
49
49
  copy.files = files.sort
50
- copy.authors = authors.sort
51
- copy.targets = targets.sort
50
+ copy.authors = Array(authors).sort
51
+ copy.maintainers = Array(maintainers).sort
52
+ copy.targets = Array(targets).sort
52
53
  copy.dependencies = dependencies.sort_by { |dependency| [dependency.name, dependency.constraint, dependency.optional] }
53
54
  end
54
55
  end
@@ -227,18 +228,29 @@ module Pray
227
228
  end
228
229
  end
229
230
 
231
+ STRING_ASSIGNMENTS = {
232
+ "name" => :name=,
233
+ "version" => :version=,
234
+ "summary" => :summary=,
235
+ "description" => :description=,
236
+ "license" => :license=,
237
+ "homepage" => :homepage=,
238
+ "source_code_uri" => :source_code_uri=,
239
+ "changelog_uri" => :changelog_uri=,
240
+ "prayfile_version" => :prayfile_version=,
241
+ "pray_version" => :prayfile_version=
242
+ }.freeze
243
+
230
244
  def apply_assignment(spec, field, value)
245
+ setter = STRING_ASSIGNMENTS[field]
246
+ if setter
247
+ spec.public_send(setter, string_from_literal(value))
248
+ return
249
+ end
250
+
231
251
  case field
232
- when "name" then spec.name = string_from_literal(value)
233
- when "version" then spec.version = string_from_literal(value)
234
- when "summary" then spec.summary = string_from_literal(value)
235
- when "description" then spec.description = string_from_literal(value)
236
252
  when "authors" then spec.authors = array_of_strings(value)
237
- when "license" then spec.license = string_from_literal(value)
238
- when "homepage" then spec.homepage = string_from_literal(value)
239
- when "source_code_uri" then spec.source_code_uri = string_from_literal(value)
240
- when "changelog_uri" then spec.changelog_uri = string_from_literal(value)
241
- when "prayfile_version" then spec.prayfile_version = string_from_literal(value)
253
+ when "maintainers" then spec.maintainers = array_of_strings(value)
242
254
  when "files" then spec.files = array_of_strings(value)
243
255
  when "targets" then spec.targets = array_of_strings(value)
244
256
  when "exports" then spec.exports = parse_exports(value)
@@ -4,9 +4,9 @@ module Pray
4
4
  module PackageSpecRender
5
5
  module_function
6
6
 
7
- def fork_spec_after_refresh(local, new_upstream, local_prayspec_file, clean_replica, merged_content_paths)
7
+ def fork_spec_after_refresh(local, new_upstream, clean_replica, merged_content_paths)
8
8
  spec = local.dup
9
- spec.files = [local_prayspec_file, *merged_content_paths]
9
+ spec.files = merged_content_paths.dup
10
10
  if clean_replica
11
11
  spec.exports = new_upstream.exports.dup
12
12
  spec.templates = new_upstream.templates.dup
@@ -32,10 +32,11 @@ module Pray
32
32
 
33
33
  def push_identity_fields(lines, spec)
34
34
  push_assignment(lines, "name", spec.name)
35
- push_assignment(lines, "version", spec.version)
35
+ push_assignment(lines, "version", spec.version) unless spec.version.to_s.empty?
36
36
  push_optional(lines, "summary", spec.summary)
37
37
  push_optional(lines, "description", spec.description)
38
38
  push_array(lines, "authors", spec.authors) unless spec.authors.nil? || spec.authors.empty?
39
+ push_array(lines, "maintainers", spec.maintainers) unless spec.maintainers.nil? || spec.maintainers.empty?
39
40
  push_optional(lines, "license", spec.license)
40
41
  push_optional(lines, "homepage", spec.homepage)
41
42
  push_optional(lines, "source_code_uri", spec.source_code_uri)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ class PackageSpec
5
+ LOCAL_VERSION = "local"
6
+
7
+ def has_release_version?
8
+ !version.to_s.empty? && version != LOCAL_VERSION
9
+ end
10
+
11
+ def recorded_version
12
+ has_release_version? ? version : LOCAL_VERSION
13
+ end
14
+
15
+ def require_release_version!
16
+ return if has_release_version?
17
+
18
+ raise Error.manifest("package #{name} needs a version before it can be packaged")
19
+ end
20
+
21
+ def satisfy_constraint!(constraint)
22
+ unless has_release_version?
23
+ normalized = Constraint.normalize_version_constraint(constraint)
24
+ return if normalized.empty? || normalized == "*"
25
+
26
+ raise Error.resolution("package #{name} has no version; add spec.version or omit the constraint")
27
+ end
28
+ return if Constraint.version_satisfies(version, constraint)
29
+
30
+ raise Error.resolution("package #{name} version #{version} does not satisfy constraint #{constraint}")
31
+ end
32
+ end
33
+ end
data/lib/pray/plan.rb CHANGED
@@ -13,7 +13,8 @@ module Pray
13
13
  def build_materialization_preview(project, rendered, lockfile, _lockfile_path, previous_lockfile)
14
14
  destinations = RenderDest.validate_destinations!(project, previous_lockfile)
15
15
  MaterializationPreview.new(
16
- package_lines: package_summary_lines(previous_lockfile, lockfile, project),
16
+ package_lines: ApplyReport.local_summary_lines(previous_lockfile, project) +
17
+ package_summary_lines(previous_lockfile, lockfile, project),
17
18
  lockfile: lockfile_change_status(previous_lockfile, lockfile),
18
19
  targets: rendered.map { |target| target_change(project, target) },
19
20
  provisioned: destinations.map { |file, status| [file.path, status.to_s] },
data/lib/pray/publish.rb CHANGED
@@ -17,14 +17,16 @@ module Pray
17
17
  def publish_to_root(project, root, signer: "local", signer_fingerprint: nil)
18
18
  root = File.expand_path(root)
19
19
  index = load_registry_index(root)
20
+ distribution = Distribution.read_settings(root)
20
21
  package_names = index.packages.to_set
21
22
 
22
23
  project.packages.each do |package|
24
+ package.spec.require_release_version!
23
25
  artifact_path = registry_artifact_path(package.declaration.name, package.spec.version)
24
26
  metadata_path = registry_metadata_path(root, package.declaration.name)
25
27
  metadata = load_registry_package_metadata(metadata_path, package.declaration.name)
26
28
  existing = metadata.versions.find { |entry| entry.version == package.spec.version }
27
- stored_artifact = stored_package_artifact(root, artifact_path, package, existing)
29
+ stored_artifact = stored_package_artifact(root, artifact_path, package, existing, distribution)
28
30
  if stored_artifact && stored_publish_matches?(
29
31
  stored_artifact, package, signer, signer_fingerprint, existing
30
32
  )
@@ -35,6 +37,14 @@ module Pray
35
37
 
36
38
  archive_bytes = Archive.build_package_archive_bytes(package)
37
39
  write_output_bytes(File.join(root, artifact_path), archive_bytes)
40
+ TorrentManifest.write(
41
+ root,
42
+ name: package.declaration.name,
43
+ version: package.spec.version,
44
+ artifact_path: artifact_path,
45
+ archive_bytes: archive_bytes,
46
+ settings: distribution
47
+ )
38
48
  version_entry = published_registry_package_version(
39
49
  package,
40
50
  signer,
@@ -54,10 +64,24 @@ module Pray
54
64
  end
55
65
 
56
66
  def publish_to_server(project, server_url, signer: "local", signer_fingerprint: nil)
67
+ distribution = Distribution.fetch_settings(server_url)
57
68
  project.packages.each do |package|
58
69
  archive_bytes = Archive.build_package_archive_bytes(package)
59
70
  artifact_path = registry_artifact_path(package.declaration.name, package.spec.version)
60
71
  Registry.http_put(join_url(server_url, artifact_path), "application/octet-stream", archive_bytes)
72
+ if distribution.allows_torrent?
73
+ Registry.http_put(
74
+ join_url(server_url, TorrentManifest.path_for(artifact_path)),
75
+ "application/json",
76
+ TorrentManifest.bytes(
77
+ name: package.declaration.name,
78
+ version: package.spec.version,
79
+ artifact_path: artifact_path,
80
+ archive_bytes: archive_bytes,
81
+ trackers: distribution.bootstrap_trackers
82
+ )
83
+ )
84
+ end
61
85
 
62
86
  metadata = RegistryPackageMetadata.new(
63
87
  name: package.declaration.name,
@@ -95,7 +119,7 @@ module Pray
95
119
  )
96
120
  end
97
121
 
98
- def stored_package_artifact(root, artifact_path, package, existing)
122
+ def stored_package_artifact(root, artifact_path, package, existing, distribution)
99
123
  return unless existing&.artifact == artifact_path && existing.tree_hash == package.tree_hash
100
124
 
101
125
  stored_path = File.join(root, artifact_path)
@@ -104,6 +128,7 @@ module Pray
104
128
  artifact_bytes = File.binread(stored_path)
105
129
  return unless existing.artifact_hash == Hashing.sha256_prefixed(artifact_bytes)
106
130
  return unless stored_prayspec_matches?(artifact_bytes, package.root)
131
+ return unless TorrentManifest.descriptor_present?(root, artifact_path, distribution)
107
132
 
108
133
  artifact_bytes
109
134
  end
data/lib/pray/registry.rb CHANGED
@@ -9,6 +9,8 @@ require "time"
9
9
  require_relative "path_safety"
10
10
  require_relative "http_body"
11
11
  require_relative "registry_paths"
12
+ require_relative "registry_http_cache"
13
+ require_relative "git_materialize"
12
14
 
13
15
  module Pray
14
16
  RegistryPackageVersion = Struct.new(
@@ -134,9 +136,7 @@ module Pray
134
136
  return parse_metadata(File.read(metadata_path, encoding: "UTF-8"))
135
137
  end
136
138
 
137
- PathSafety.reject_unsafe_package_name!(package_name)
138
- response = http_get(join_url(source_url, "v1/packages/#{package_name}.json"))
139
- parse_metadata(response)
139
+ RegistryHttpCache.fetch_package_metadata(source_url, package_name)
140
140
  end
141
141
 
142
142
  def parse_metadata(text)
@@ -207,18 +207,7 @@ module Pray
207
207
  end
208
208
 
209
209
  def read_local_artifact_bytes(source_root, artifact)
210
- if artifact.start_with?("file://")
211
- path = PathSafety.join_under_root(source_root, artifact.delete_prefix("file://"))
212
- raise Error.resolution("package artifact path escapes distribution root") unless path
213
- return File.binread(path)
214
- end
215
- reject_absolute_artifact!(artifact)
216
-
217
- path = PathSafety.join_under_root(source_root, artifact)
218
- raise Error.resolution("package artifact path escapes distribution root") unless path
219
- raise Error.resolution("package artifact missing at #{path}") unless File.exist?(path)
220
-
221
- File.binread(path)
210
+ GitMaterialize.read_local_artifact_bytes(source_root, artifact)
222
211
  end
223
212
 
224
213
  def read_artifact_bytes(source_url, artifact)