pray-cli 1.15.0 → 1.17.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 +17 -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/workflow.rb +5 -0
  9. data/lib/pray/cli/help.rb +52 -15
  10. data/lib/pray/cli/helpers.rb +13 -5
  11. data/lib/pray/cli/parse.rb +21 -1
  12. data/lib/pray/cli.rb +3 -1
  13. data/lib/pray/constraint.rb +17 -4
  14. data/lib/pray/dependency_graph.rb +43 -0
  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_sources.rb +11 -10
  19. data/lib/pray/local_prayer.rb +38 -0
  20. data/lib/pray/lockfile.rb +8 -2
  21. data/lib/pray/manifest.rb +6 -3
  22. data/lib/pray/manifest_json.rb +1 -0
  23. data/lib/pray/manifest_parser_blocks.rb +15 -15
  24. data/lib/pray/materialize.rb +8 -1
  25. data/lib/pray/package_spec.rb +25 -13
  26. data/lib/pray/package_spec_render.rb +4 -3
  27. data/lib/pray/package_spec_version.rb +33 -0
  28. data/lib/pray/plan.rb +2 -1
  29. data/lib/pray/publish.rb +27 -2
  30. data/lib/pray/registry.rb +12 -0
  31. data/lib/pray/registry_paths.rb +14 -0
  32. data/lib/pray/render.rb +6 -36
  33. data/lib/pray/render_managed.rb +64 -0
  34. data/lib/pray/render_patch.rb +63 -12
  35. data/lib/pray/resolve.rb +17 -50
  36. data/lib/pray/resolve_context.rb +7 -0
  37. data/lib/pray/resolve_deps.rb +18 -0
  38. data/lib/pray/resolve_local.rb +39 -0
  39. data/lib/pray/resolve_source.rb +5 -0
  40. data/lib/pray/resolve_tarball.rb +66 -0
  41. data/lib/pray/serve.rb +15 -4
  42. data/lib/pray/serve_range.rb +36 -0
  43. data/lib/pray/torrent_manifest.rb +78 -0
  44. data/lib/pray/upstream_drift.rb +42 -0
  45. data/lib/pray/upstream_merge.rb +25 -0
  46. data/lib/pray/upstream_refresh.rb +28 -2
  47. data/lib/pray/verify.rb +2 -2
  48. data/lib/pray/verify_locked_dest.rb +47 -0
  49. data/lib/pray/version.rb +2 -2
  50. data/lib/pray.rb +15 -1
  51. metadata +17 -1
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module DependencyGraph
5
+ module_function
6
+
7
+ def find_dependency_cycle(edges)
8
+ visiting = {}
9
+ visited = {}
10
+ stack = []
11
+ edges.keys.sort.each do |name|
12
+ next if visited[name]
13
+
14
+ cycle = depth_first_search(name, edges, visiting, visited, stack)
15
+ return cycle if cycle
16
+ end
17
+ nil
18
+ end
19
+
20
+ def depth_first_search(name, edges, visiting, visited, stack)
21
+ return nil if visited[name]
22
+ if visiting[name]
23
+ start = stack.index(name)
24
+ return nil unless start
25
+
26
+ return stack[start..] + [name]
27
+ end
28
+
29
+ visiting[name] = true
30
+ stack << name
31
+ Array(edges[name]).each do |dependency|
32
+ next unless edges.key?(dependency)
33
+
34
+ cycle = depth_first_search(dependency, edges, visiting, visited, stack)
35
+ return cycle if cycle
36
+ end
37
+ stack.pop
38
+ visiting.delete(name)
39
+ visited[name] = true
40
+ nil
41
+ end
42
+ end
43
+ end
@@ -16,8 +16,7 @@ module Pray
16
16
 
17
17
  def local_path_form?(value)
18
18
  value.start_with?(".", "/") ||
19
- value.end_with?(".md", ".txt", ".markdown") ||
20
- !value.include?("/")
19
+ value.end_with?(".md", ".txt", ".markdown")
21
20
  end
22
21
 
23
22
  def destination_target_name(mode, path)
@@ -106,6 +105,7 @@ module Pray
106
105
  if existing.position == "after" && local.position != "after"
107
106
  existing.position = local.position
108
107
  end
108
+ existing.file ||= local.file
109
109
  end
110
110
 
111
111
  def bind_package_entry(target, package_name)
@@ -154,5 +154,35 @@ module Pray
154
154
  else false
155
155
  end
156
156
  end
157
+
158
+ def local_compose_embed?(manifest, local)
159
+ return false if local.file
160
+
161
+ manifest.targets.none? do |target|
162
+ target.mode == "tree" &&
163
+ target.entries.any? { |entry| entry.kind == "local" && entry.path == local.path }
164
+ end
165
+ end
166
+
167
+ def try_apply_local_pray(manifest, path, file_dest, other_package_signal, destination_index)
168
+ return false unless local_path_form?(path)
169
+
170
+ if file_dest
171
+ return false if other_package_signal
172
+ raise Error.parse("manifest", "a local file cannot use file:")
173
+ end
174
+ return false if other_package_signal
175
+ unless destination_index
176
+ raise Error.parse("manifest", "local pray paths are only valid inside compose")
177
+ end
178
+ mode = manifest.targets[destination_index].mode
179
+ unless mode == "compose"
180
+ raise Error.parse("manifest", "local pray paths are only valid inside compose")
181
+ end
182
+ local = ManifestLocal.new(path: path, bound: true)
183
+ bind_local_entry(manifest.targets[destination_index], local.path)
184
+ upsert_local(manifest, local)
185
+ true
186
+ end
157
187
  end
158
188
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module Pray
7
+ DISTRIBUTION_CONFIG_SPEC = "pray-distribution-config-1"
8
+ TORRENT_PROTOCOL = "torrent"
9
+ DISTRIBUTION_CONFIG_FIELDS = %w[spec protocols bootstrap_trackers enable_dht].freeze
10
+
11
+ RegistryDistributionSettings = Struct.new(
12
+ :spec, :protocols, :bootstrap_trackers, :enable_dht,
13
+ keyword_init: true
14
+ ) do
15
+ def initialize(spec: DISTRIBUTION_CONFIG_SPEC, protocols: [], bootstrap_trackers: [], enable_dht: false)
16
+ super
17
+ end
18
+
19
+ def allows_torrent?
20
+ Array(protocols).include?(TORRENT_PROTOCOL)
21
+ end
22
+ end
23
+
24
+ module Distribution
25
+ module_function
26
+
27
+ def default_settings
28
+ RegistryDistributionSettings.new
29
+ end
30
+
31
+ def parse_settings(text)
32
+ data = JSON.parse(text)
33
+ unless data.is_a?(Hash)
34
+ raise Error.parse("distribution config", "expected an object")
35
+ end
36
+
37
+ data.each_key do |key|
38
+ next if DISTRIBUTION_CONFIG_FIELDS.include?(key)
39
+
40
+ raise Error.parse("distribution config", "unsupported field: #{key}")
41
+ end
42
+ settings = RegistryDistributionSettings.new(
43
+ spec: data.fetch("spec", DISTRIBUTION_CONFIG_SPEC),
44
+ protocols: Array(data["protocols"]),
45
+ bootstrap_trackers: Array(data["bootstrap_trackers"]),
46
+ enable_dht: data["enable_dht"] == true
47
+ )
48
+ validate_settings!(settings)
49
+ settings
50
+ rescue JSON::ParserError => error
51
+ raise Error.parse("distribution config", error.message)
52
+ end
53
+
54
+ def read_settings(root)
55
+ path = File.join(root, "v1", "distribution.json")
56
+ return default_settings unless File.file?(path)
57
+
58
+ parse_settings(File.read(path))
59
+ end
60
+
61
+ def write_settings(root, settings = default_settings)
62
+ validate_settings!(settings)
63
+ path = File.join(root, "v1", "distribution.json")
64
+ FileUtils.mkdir_p(File.dirname(path))
65
+ payload = {"spec" => settings.spec, "protocols" => Array(settings.protocols)}
66
+ File.write(path, JSON.pretty_generate(payload))
67
+ end
68
+
69
+ def validate_settings!(settings)
70
+ unless settings.spec == DISTRIBUTION_CONFIG_SPEC
71
+ raise Error.parse("distribution config", "unsupported distribution config spec: #{settings.spec}")
72
+ end
73
+ Array(settings.protocols).each do |name|
74
+ next if name == TORRENT_PROTOCOL
75
+
76
+ raise Error.parse("distribution config", "unsupported protocol: #{name}")
77
+ end
78
+ if settings.enable_dht
79
+ raise Error.parse("distribution config", "DHT announce is not implemented")
80
+ end
81
+ return if Array(settings.bootstrap_trackers).empty? || settings.allows_torrent?
82
+
83
+ raise Error.parse("distribution config", "bootstrap_trackers requires protocols to include torrent")
84
+ end
85
+
86
+ def fetch_settings(source_url)
87
+ parse_settings(Registry.http_get(Registry.join_url(source_url, "v1/distribution.json")))
88
+ rescue Error => error
89
+ return default_settings if error.message.match?(/:\s*404\b/)
90
+
91
+ raise
92
+ end
93
+ end
94
+ end
@@ -42,10 +42,7 @@ module Pray
42
42
  path = target.skills.first || ""
43
43
  lines << %(tree "#{path}" do)
44
44
  target.entries.each do |entry|
45
- next unless entry.kind == "package"
46
-
47
- package = find_package(manifest, entry.name)
48
- lines << " #{ManifestMethods.format_package_declaration(package)}" if package
45
+ lines << " #{format_destination_entry(entry, manifest)}"
49
46
  end
50
47
  lines << "end"
51
48
  end
@@ -16,8 +16,8 @@ module Pray
16
16
  next unless source.kind == "git"
17
17
 
18
18
  clone_url = source.url.delete_prefix("git+")
19
- if local_filesystem_source?(clone_url) && !local_git_repo_path(clone_url)
20
- source_root = local_git_source_root(clone_url)
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
21
  if source_root
22
22
  checkouts[source.name] = GitSourceCheckout.new(
23
23
  cache_directory: source_root,
@@ -76,12 +76,8 @@ module Pray
76
76
  File.directory?(File.join(path, "v1", "packages"))
77
77
  end
78
78
 
79
- def local_git_source_root(clone_url)
80
- path = if clone_url.start_with?("file://")
81
- clone_url.delete_prefix("file://")
82
- else
83
- clone_url
84
- end
79
+ def local_git_source_root(project_root, clone_url)
80
+ path = clone_url_filesystem_path(project_root, clone_url)
85
81
  return nil unless File.exist?(path)
86
82
 
87
83
  discover_distribution_root(path)
@@ -136,12 +132,17 @@ module Pray
136
132
  clone_url.start_with?("file://") || Pathname.new(clone_url).absolute?
137
133
  end
138
134
 
139
- def local_git_repo_path(clone_url)
140
- path = clone_url.delete_prefix("file://")
135
+ def local_git_repo_path(project_root, clone_url)
136
+ path = clone_url_filesystem_path(project_root, clone_url)
141
137
  git_directory = File.join(path, ".git")
142
138
  File.directory?(git_directory) ? path : nil
143
139
  end
144
140
 
141
+ def clone_url_filesystem_path(project_root, clone_url)
142
+ path = clone_url.delete_prefix("file://")
143
+ Pathname.new(path).absolute? ? path : File.expand_path(path, project_root)
144
+ end
145
+
145
146
  def global_cache_root
146
147
  return ENV["PRAY_CACHE"] if ENV["PRAY_CACHE"]
147
148
  return File.join(ENV["PRAY_HOME"], "cache") if ENV["PRAY_HOME"]
@@ -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,
@@ -133,6 +139,7 @@ module Pray
133
139
  def resolve_project(...) = Resolve.resolve_project(...)
134
140
  def render_project(...) = Render.render_project(...)
135
141
  def inspect_project(...) = Verify.inspect_project(...)
142
+ def inspect_locked_destinations(...) = Verify.inspect_locked_destinations(...)
136
143
  def verify_project(...) = Verify.verify_project(...)
137
144
  def drift_project(...) = Verify.drift_project(...)
138
145
  end
@@ -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
@@ -8,6 +8,7 @@ require "pathname"
8
8
  require "time"
9
9
  require_relative "path_safety"
10
10
  require_relative "http_body"
11
+ require_relative "registry_paths"
11
12
 
12
13
  module Pray
13
14
  RegistryPackageVersion = Struct.new(
@@ -40,6 +41,17 @@ module Pray
40
41
  module_function
41
42
 
42
43
  def resolve_registry_package_root(project_root, source_url, declaration, preferred_version: nil, offline: false)
44
+ if local_source?(source_url)
45
+ return resolve_local_registry_package_root(
46
+ project_root,
47
+ source_url,
48
+ RegistryPaths.local_registry_root(project_root, source_url),
49
+ declaration,
50
+ preferred_version: preferred_version,
51
+ offline: offline
52
+ )
53
+ end
54
+
43
55
  metadata = fetch_package_metadata(source_url, declaration.name)
44
56
  registry_latest_version = registry_latest_version_label(metadata)
45
57
  selected = select_package_version(metadata, declaration.constraint, preferred_version)