pray-cli 1.16.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -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/destination.rb +32 -2
  14. data/lib/pray/distribution.rb +94 -0
  15. data/lib/pray/format_serialize.rb +1 -4
  16. data/lib/pray/local_prayer.rb +38 -0
  17. data/lib/pray/lockfile.rb +8 -2
  18. data/lib/pray/manifest.rb +6 -3
  19. data/lib/pray/manifest_json.rb +1 -0
  20. data/lib/pray/manifest_parser_blocks.rb +15 -15
  21. data/lib/pray/materialize.rb +7 -1
  22. data/lib/pray/package_spec.rb +25 -13
  23. data/lib/pray/package_spec_render.rb +4 -3
  24. data/lib/pray/package_spec_version.rb +33 -0
  25. data/lib/pray/plan.rb +2 -1
  26. data/lib/pray/publish.rb +27 -2
  27. data/lib/pray/render.rb +6 -36
  28. data/lib/pray/render_managed.rb +64 -0
  29. data/lib/pray/render_patch.rb +63 -12
  30. data/lib/pray/resolve.rb +5 -36
  31. data/lib/pray/resolve_local.rb +39 -0
  32. data/lib/pray/resolve_source.rb +5 -0
  33. data/lib/pray/serve.rb +15 -4
  34. data/lib/pray/serve_range.rb +36 -0
  35. data/lib/pray/torrent_manifest.rb +78 -0
  36. data/lib/pray/upstream_drift.rb +42 -0
  37. data/lib/pray/upstream_merge.rb +25 -0
  38. data/lib/pray/upstream_refresh.rb +28 -2
  39. data/lib/pray/verify.rb +2 -2
  40. data/lib/pray/version.rb +2 -2
  41. data/lib/pray.rb +10 -0
  42. metadata +12 -1
@@ -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
@@ -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/render.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "fileutils"
4
4
  require "pathname"
5
5
  require_relative "render_content"
6
+ require_relative "render_managed"
6
7
 
7
8
  module Pray
8
9
  RenderedTarget = Struct.new(:path, :content, :managed_spans) do
@@ -76,7 +77,7 @@ module Pray
76
77
 
77
78
  (target.entries || []).each do |entry|
78
79
  if entry.kind == "local"
79
- append_scoped_local(builder, project, entry.path, symbols)
80
+ append_scoped_local(builder, managed_spans, project, target, output, entry.path, symbols)
80
81
  else
81
82
  append_scoped_package(builder, managed_spans, project, target, output, entry.name, symbols)
82
83
  end
@@ -99,18 +100,17 @@ module Pray
99
100
  builder.append_empty_line
100
101
  end
101
102
  symbols = project.manifest.symbols || {}
103
+ managed_spans = []
102
104
  unbound_locals.each do |local|
103
105
  next if local.content.empty? && local.optional
104
106
 
105
107
  builder.append_line("### #{local.manifest_path}")
106
- builder.append_body(Substitute.substitute_pray_symbols(local.content, symbols))
107
- builder.append_empty_line
108
+ append_managed_local(builder, managed_spans, local, target, output, symbols)
108
109
  end
109
110
 
110
111
  builder.append_line("## Shared instructions")
111
112
  builder.append_empty_line
112
113
 
113
- managed_spans = []
114
114
  project.packages.each do |package|
115
115
  next unless Environment.package_matches_environment?(package.declaration.groups, project.environment)
116
116
  next unless Destination.package_bound_to_compose?(package.declaration, target)
@@ -133,13 +133,11 @@ module Pray
133
133
  builder.append_empty_line
134
134
  end
135
135
 
136
- def append_scoped_local(builder, project, path, symbols)
136
+ def append_scoped_local(builder, managed_spans, project, target, output, path, symbols)
137
137
  local = project.local_files.find { |candidate| candidate.manifest_path == path }
138
138
  return unless local
139
- return if local.content.empty? && local.optional
140
139
 
141
- builder.append_body(Substitute.substitute_pray_symbols(local.content, symbols))
142
- builder.append_empty_line
140
+ append_managed_local(builder, managed_spans, local, target, output, symbols)
143
141
  end
144
142
 
145
143
  def append_scoped_package(builder, managed_spans, project, target, output, package_name, symbols)
@@ -154,34 +152,6 @@ module Pray
154
152
  end
155
153
  end
156
154
 
157
- def append_managed_export(builder, managed_spans, package, export, target, output, symbols)
158
- body = package.export_bodies[export]
159
- unless body
160
- raise Error.integrity("compose cannot write binary export #{export}; use file: for unmarked bytes") if package.spec.exports[export]&.kind == "file"
161
- raise Error.render("package #{package.declaration.name} is missing cached export #{export}")
162
- end
163
-
164
- body = Substitute.substitute_pray_symbols(body, symbols)
165
- identifier = Hashing.marker_id("#{package.declaration.name}:#{export}:#{target.name}")
166
- open_line = builder.next_line_number
167
- builder.append_line("<!-- pray:#{identifier} -->")
168
- builder.append_body(body)
169
- close_line = builder.next_line_number
170
- builder.append_line("<!-- pray:#{identifier} -->")
171
- managed_spans << ManagedSpanRecord.new(
172
- id: identifier,
173
- target: output,
174
- open_line: open_line,
175
- close_line: close_line,
176
- ideal_checksum: Hashing.checksum_managed_span_content(body),
177
- package: package.declaration.name,
178
- export: export,
179
- source_checksum: package.source_checksum,
180
- silenced: false
181
- )
182
- builder.append_empty_line
183
- end
184
-
185
155
  def should_inline_export?(package, export_name)
186
156
  export = package.spec.exports[export_name]
187
157
  export.nil? || %w[fragment file].include?(export.kind)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Render
5
+ module_function
6
+
7
+ def append_managed_local(builder, managed_spans, local, target, output, symbols)
8
+ return if local.content.empty? && local.optional
9
+
10
+ body = Substitute.substitute_pray_symbols(local.content, symbols)
11
+ append_managed_span(
12
+ builder,
13
+ managed_spans,
14
+ "local:#{local.manifest_path}:#{target.name}",
15
+ body,
16
+ output,
17
+ LOCAL_EMBED_PACKAGE,
18
+ local.manifest_path,
19
+ local.source_checksum
20
+ )
21
+ end
22
+
23
+ def append_managed_export(builder, managed_spans, package, export, target, output, symbols)
24
+ body = package.export_bodies[export]
25
+ unless body
26
+ raise Error.integrity("compose cannot write binary export #{export}; use file: for unmarked bytes") if package.spec.exports[export]&.kind == "file"
27
+ raise Error.render("package #{package.declaration.name} is missing cached export #{export}")
28
+ end
29
+
30
+ body = Substitute.substitute_pray_symbols(body, symbols)
31
+ append_managed_span(
32
+ builder,
33
+ managed_spans,
34
+ "#{package.declaration.name}:#{export}:#{target.name}",
35
+ body,
36
+ output,
37
+ package.declaration.name,
38
+ export,
39
+ package.source_checksum
40
+ )
41
+ end
42
+
43
+ def append_managed_span(builder, managed_spans, seed, body, output, package, export, source_checksum)
44
+ identifier = Hashing.marker_id(seed)
45
+ open_line = builder.next_line_number
46
+ builder.append_line("<!-- pray:#{identifier} -->")
47
+ builder.append_body(body)
48
+ close_line = builder.next_line_number
49
+ builder.append_line("<!-- pray:#{identifier} -->")
50
+ managed_spans << ManagedSpanRecord.new(
51
+ id: identifier,
52
+ target: output,
53
+ open_line: open_line,
54
+ close_line: close_line,
55
+ ideal_checksum: Hashing.checksum_managed_span_content(body),
56
+ package: package,
57
+ export: export,
58
+ source_checksum: source_checksum,
59
+ silenced: false
60
+ )
61
+ builder.append_empty_line
62
+ end
63
+ end
64
+ end