pray-cli 1.6.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 +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE.md +21 -0
- data/README.md +125 -0
- data/SECURITY.md +28 -0
- data/bin/polyrun +108 -0
- data/bin/pray +26 -0
- data/lib/pray/archive.rb +68 -0
- data/lib/pray/auth_client.rb +97 -0
- data/lib/pray/cli/commands/auth.rb +42 -0
- data/lib/pray/cli/commands/distribution.rb +17 -0
- data/lib/pray/cli/commands/init.rb +62 -0
- data/lib/pray/cli/commands/meta.rb +15 -0
- data/lib/pray/cli/commands/packages.rb +107 -0
- data/lib/pray/cli/commands/trust.rb +73 -0
- data/lib/pray/cli/commands/workflow.rb +126 -0
- data/lib/pray/cli/help.rb +168 -0
- data/lib/pray/cli/helpers.rb +166 -0
- data/lib/pray/cli/parse.rb +136 -0
- data/lib/pray/cli/parse_auth.rb +91 -0
- data/lib/pray/cli/parse_trust.rb +152 -0
- data/lib/pray/cli/suggest.rb +57 -0
- data/lib/pray/cli.rb +117 -0
- data/lib/pray/confess.rb +113 -0
- data/lib/pray/config.rb +52 -0
- data/lib/pray/constraint.rb +80 -0
- data/lib/pray/destination.rb +158 -0
- data/lib/pray/dotenv.rb +46 -0
- data/lib/pray/environment.rb +41 -0
- data/lib/pray/error.rb +55 -0
- data/lib/pray/format_manifest.rb +255 -0
- data/lib/pray/format_serialize.rb +135 -0
- data/lib/pray/git_sources.rb +228 -0
- data/lib/pray/hashing.rb +59 -0
- data/lib/pray/invocation.rb +110 -0
- data/lib/pray/literal.rb +382 -0
- data/lib/pray/lockfile.rb +259 -0
- data/lib/pray/lockfile_serialize.rb +125 -0
- data/lib/pray/manifest.rb +126 -0
- data/lib/pray/manifest_formatter.rb +56 -0
- data/lib/pray/manifest_json.rb +128 -0
- data/lib/pray/manifest_parser.rb +152 -0
- data/lib/pray/manifest_parser_blocks.rb +216 -0
- data/lib/pray/manifest_parser_helpers.rb +208 -0
- data/lib/pray/materialize.rb +120 -0
- data/lib/pray/package_spec.rb +245 -0
- data/lib/pray/path_safety.rb +41 -0
- data/lib/pray/plan.rb +83 -0
- data/lib/pray/project_context.rb +67 -0
- data/lib/pray/publish.rb +162 -0
- data/lib/pray/registry.rb +355 -0
- data/lib/pray/render.rb +360 -0
- data/lib/pray/resolve.rb +396 -0
- data/lib/pray/resolve_context.rb +19 -0
- data/lib/pray/serve.rb +130 -0
- data/lib/pray/serve_federation.rb +109 -0
- data/lib/pray/session.rb +90 -0
- data/lib/pray/ssh_agent.rb +115 -0
- data/lib/pray/statement_surface.rb +262 -0
- data/lib/pray/substitute.rb +50 -0
- data/lib/pray/sync.rb +219 -0
- data/lib/pray/terminal.rb +30 -0
- data/lib/pray/trust.rb +201 -0
- data/lib/pray/trust_feed.rb +110 -0
- data/lib/pray/trust_ops.rb +202 -0
- data/lib/pray/verify.rb +257 -0
- data/lib/pray/version.rb +6 -0
- data/lib/pray-cli.rb +4 -0
- data/lib/pray.rb +46 -0
- data/pray-cli.gemspec +48 -0
- metadata +187 -0
data/lib/pray/config.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toml-rb"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
module Config
|
|
7
|
+
PrayConfig = Struct.new(:local) do
|
|
8
|
+
def initialize(local: PrayLocalConfig.new)
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
PrayLocalConfig = Struct.new(:package, :source) do
|
|
14
|
+
def initialize(package: {}, source: {})
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def load_user_config
|
|
22
|
+
path = user_config_path
|
|
23
|
+
return PrayConfig.new unless path && File.file?(path)
|
|
24
|
+
|
|
25
|
+
data = TomlRB.load_file(path)
|
|
26
|
+
PrayConfig.new(
|
|
27
|
+
local: PrayLocalConfig.new(
|
|
28
|
+
package: data.dig("local", "package") || {},
|
|
29
|
+
source: data.dig("local", "source") || {}
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
rescue TomlRB::ParseError => error
|
|
33
|
+
raise Error.parse("config", "#{path}: #{error.message}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def user_config_path
|
|
37
|
+
if (path = ENV["PRAY_CONFIG"])
|
|
38
|
+
return path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if (home = ENV["PRAY_HOME"])
|
|
42
|
+
candidate = File.join(home, "config.toml")
|
|
43
|
+
return candidate if File.file?(candidate)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
home = ENV["HOME"]
|
|
47
|
+
return nil unless home
|
|
48
|
+
|
|
49
|
+
File.join(home, ".config", "pray", "config.toml")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module Constraint
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def normalize_version_constraint(constraint)
|
|
8
|
+
trimmed = constraint.strip
|
|
9
|
+
return trimmed if trimmed.empty? || trimmed == "*"
|
|
10
|
+
return trimmed if operator_prefixed?(trimmed)
|
|
11
|
+
|
|
12
|
+
if bare_semver?(trimmed)
|
|
13
|
+
"=#{trimmed}"
|
|
14
|
+
else
|
|
15
|
+
trimmed
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def version_satisfies(version, constraint)
|
|
20
|
+
normalized = normalize_version_constraint(constraint)
|
|
21
|
+
return true if normalized.empty? || normalized == "*"
|
|
22
|
+
|
|
23
|
+
requirement = Gem::Requirement.new(normalized.strip)
|
|
24
|
+
requirement.satisfied_by?(Gem::Version.new(version))
|
|
25
|
+
rescue ArgumentError => error
|
|
26
|
+
raise Error.resolution(error.message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pessimistic_constraint_for_version(version)
|
|
30
|
+
parsed = Gem::Version.new(version)
|
|
31
|
+
if parsed.segments[1].to_i.zero? && parsed.segments[2].to_i.zero?
|
|
32
|
+
"~> #{parsed.segments[0]}.0"
|
|
33
|
+
else
|
|
34
|
+
"~> #{parsed.segments[0]}.#{parsed.segments[1]}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def latest_constraint_for_package(current_constraint, latest_version)
|
|
39
|
+
normalized = normalize_version_constraint(current_constraint)
|
|
40
|
+
return "*" if normalized == "*"
|
|
41
|
+
return pessimistic_constraint_for_version(latest_version) if normalized.start_with?("~")
|
|
42
|
+
|
|
43
|
+
if normalized.start_with?("^")
|
|
44
|
+
parsed = Gem::Version.new(latest_version)
|
|
45
|
+
return "^#{parsed.segments[0]}.#{parsed.segments[1]}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if normalized.start_with?("=") || bare_semver?(current_constraint.strip)
|
|
49
|
+
return "=#{latest_version}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
pessimistic_constraint_for_version(latest_version)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ruby_pessimistic_to_semver(constraint)
|
|
56
|
+
text = constraint.strip.sub(/\A~>\s*/, "")
|
|
57
|
+
parts = text.split(".")
|
|
58
|
+
raise Error.resolution("unsupported Ruby pessimistic constraint: #{constraint}") if parts.empty? || parts.length > 3
|
|
59
|
+
|
|
60
|
+
numbers = [parts[0].to_i, (parts[1] || 0).to_i, (parts[2] || 0).to_i]
|
|
61
|
+
lower = numbers.join(".")
|
|
62
|
+
upper = case parts.length
|
|
63
|
+
when 1 then "#{numbers[0] + 1}.0.0"
|
|
64
|
+
when 2 then "#{numbers[0]}.#{numbers[1] + 1}.0"
|
|
65
|
+
else "#{numbers[0]}.#{numbers[1] + 1}.0"
|
|
66
|
+
end
|
|
67
|
+
">= #{lower}, < #{upper}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def operator_prefixed?(text)
|
|
71
|
+
text.start_with?("~>", "~", "^", "=", ">", "<") || text.include?("*")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def bare_semver?(text)
|
|
75
|
+
Gem::Version.correct?(text)
|
|
76
|
+
rescue
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
DestinationEntry = Struct.new(:kind, :name, :path) do
|
|
5
|
+
def self.package(name)
|
|
6
|
+
new(kind: "package", name: name, path: nil)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.local(path)
|
|
10
|
+
new(kind: "local", name: nil, path: path)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Destination
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def local_path_form?(value)
|
|
18
|
+
value.start_with?(".", "/") ||
|
|
19
|
+
value.end_with?(".md", ".txt", ".markdown") ||
|
|
20
|
+
!value.include?("/")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def destination_target_name(mode, path)
|
|
24
|
+
prefix = case mode
|
|
25
|
+
when "compose" then "compose"
|
|
26
|
+
when "tree" then "tree"
|
|
27
|
+
else "legacy"
|
|
28
|
+
end
|
|
29
|
+
"#{prefix}:#{path}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def new_destination_target(mode, path)
|
|
33
|
+
target = ManifestTarget.new(
|
|
34
|
+
name: destination_target_name(mode, path),
|
|
35
|
+
mode: mode,
|
|
36
|
+
scoped: true
|
|
37
|
+
)
|
|
38
|
+
case mode
|
|
39
|
+
when "compose" then target.outputs << path
|
|
40
|
+
when "tree" then target.skills << path
|
|
41
|
+
end
|
|
42
|
+
target
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def upsert_package(manifest, package)
|
|
46
|
+
existing = manifest.packages.find { |candidate| candidate.name == package.name }
|
|
47
|
+
unless existing
|
|
48
|
+
manifest.packages << package
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if existing.constraint != package.constraint &&
|
|
53
|
+
existing.constraint != "*" &&
|
|
54
|
+
package.constraint != "*"
|
|
55
|
+
raise Error.manifest(
|
|
56
|
+
"package #{package.name} declared with conflicting constraints " \
|
|
57
|
+
"(#{existing.constraint} vs #{package.constraint})"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
existing.constraint = package.constraint if existing.constraint == "*" && package.constraint != "*"
|
|
61
|
+
|
|
62
|
+
if existing.source.nil?
|
|
63
|
+
existing.source = package.source
|
|
64
|
+
elsif package.source && existing.source != package.source
|
|
65
|
+
raise Error.manifest("package #{package.name} declared with conflicting sources")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
package.exports.each do |export|
|
|
69
|
+
existing.exports << export unless existing.exports.include?(export)
|
|
70
|
+
end
|
|
71
|
+
package.roles.each do |role|
|
|
72
|
+
existing.roles << role unless existing.roles.include?(role)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if package.file
|
|
76
|
+
if existing.file && existing.file != package.file
|
|
77
|
+
raise Error.manifest(
|
|
78
|
+
"package #{package.name} declared with conflicting file: destinations"
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
existing.file = package.file
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
existing.bound = existing.bound || package.bound
|
|
85
|
+
existing.optional = existing.optional || package.optional
|
|
86
|
+
existing.path ||= package.path
|
|
87
|
+
existing.git ||= package.git
|
|
88
|
+
existing.tag ||= package.tag
|
|
89
|
+
existing.rev ||= package.rev
|
|
90
|
+
existing.tarball ||= package.tarball
|
|
91
|
+
existing.oci ||= package.oci
|
|
92
|
+
package.groups.each do |group|
|
|
93
|
+
existing.groups << group unless existing.groups.include?(group)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def upsert_local(manifest, local)
|
|
98
|
+
existing = manifest.local.find { |candidate| candidate.path == local.path }
|
|
99
|
+
unless existing
|
|
100
|
+
manifest.local << local
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
existing.bound = existing.bound || local.bound
|
|
105
|
+
existing.optional = existing.optional || local.optional
|
|
106
|
+
if existing.position == "after" && local.position != "after"
|
|
107
|
+
existing.position = local.position
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def bind_package_entry(target, package_name)
|
|
112
|
+
entry = DestinationEntry.package(package_name)
|
|
113
|
+
return if target.entries.any? { |candidate| candidate.kind == "package" && candidate.name == package_name }
|
|
114
|
+
|
|
115
|
+
target.entries << entry
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def bind_local_entry(target, path)
|
|
119
|
+
return if target.entries.any? { |candidate| candidate.kind == "local" && candidate.path == path }
|
|
120
|
+
|
|
121
|
+
target.entries << DestinationEntry.local(path)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def role_for_destination(mode)
|
|
125
|
+
case mode
|
|
126
|
+
when "compose" then "fragment"
|
|
127
|
+
when "tree" then "folder"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def package_bound_to_compose?(package, target)
|
|
132
|
+
if target.scoped && target.mode == "compose"
|
|
133
|
+
return target.entries.any? { |entry| entry.kind == "package" && entry.name == package.name }
|
|
134
|
+
end
|
|
135
|
+
return false if package.bound || package.file
|
|
136
|
+
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def package_bound_to_tree?(package, target)
|
|
141
|
+
if target.scoped && target.mode == "tree"
|
|
142
|
+
return target.entries.any? { |entry| entry.kind == "package" && entry.name == package.name }
|
|
143
|
+
end
|
|
144
|
+
return false if package.bound || package.file
|
|
145
|
+
|
|
146
|
+
true
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def export_kind_matches_role?(kind, role)
|
|
150
|
+
case role
|
|
151
|
+
when "fragment" then kind == "fragment"
|
|
152
|
+
when "folder" then %w[folder skill].include?(kind)
|
|
153
|
+
when "file" then kind == "file"
|
|
154
|
+
else false
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/pray/dotenv.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module Dotenv
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def load_dotenv_variables(project_root_hint)
|
|
8
|
+
path = File.join(project_root_hint, ".env")
|
|
9
|
+
return {} unless File.file?(path)
|
|
10
|
+
|
|
11
|
+
text = File.read(path)
|
|
12
|
+
parse_dotenv_text(text)
|
|
13
|
+
rescue
|
|
14
|
+
{}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse_dotenv_text(text)
|
|
18
|
+
variables = {}
|
|
19
|
+
text.each_line do |line|
|
|
20
|
+
trimmed = line.strip
|
|
21
|
+
next if trimmed.empty? || trimmed.start_with?("#")
|
|
22
|
+
|
|
23
|
+
assignment = trimmed.delete_prefix("export ").strip
|
|
24
|
+
key, value = assignment.split("=", 2)
|
|
25
|
+
next unless value
|
|
26
|
+
|
|
27
|
+
key = key.strip
|
|
28
|
+
next if key.empty?
|
|
29
|
+
next unless key.start_with?("PRAY_")
|
|
30
|
+
|
|
31
|
+
variables[key] = parse_dotenv_value(value.strip)
|
|
32
|
+
end
|
|
33
|
+
variables
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def parse_dotenv_value(value)
|
|
37
|
+
if value.length >= 2
|
|
38
|
+
quote = value[0]
|
|
39
|
+
if (quote == '"' || quote == "'") && value[-1] == quote
|
|
40
|
+
return value[1...-1]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module Environment
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def package_matches_environment?(groups, environment)
|
|
8
|
+
return true if groups.empty?
|
|
9
|
+
|
|
10
|
+
return false if environment.nil?
|
|
11
|
+
|
|
12
|
+
groups.any? { |group| group == environment }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def collect_group_names(manifest)
|
|
16
|
+
manifest.packages.flat_map(&:groups).uniq.sort
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate_environment(manifest, environment)
|
|
20
|
+
return if environment.nil?
|
|
21
|
+
|
|
22
|
+
if environment.empty?
|
|
23
|
+
raise Error.resolution("environment name cannot be empty")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
known_groups = collect_group_names(manifest)
|
|
27
|
+
if known_groups.empty?
|
|
28
|
+
raise Error.resolution("unknown environment #{environment}; Prayfile defines no groups")
|
|
29
|
+
end
|
|
30
|
+
return if known_groups.include?(environment)
|
|
31
|
+
|
|
32
|
+
raise Error.resolution(
|
|
33
|
+
"unknown environment #{environment}; available groups are #{known_groups.join(", ")}"
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def should_render_package?(declaration, environment)
|
|
38
|
+
package_matches_environment?(declaration.groups, environment)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/pray/error.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
def initialize(category, message, parse_kind: nil)
|
|
6
|
+
@category = category
|
|
7
|
+
@parse_kind = parse_kind
|
|
8
|
+
super(message)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :category
|
|
12
|
+
|
|
13
|
+
def exit_code
|
|
14
|
+
case category
|
|
15
|
+
when :parse, :usage then 2
|
|
16
|
+
when :manifest, :io then 1
|
|
17
|
+
when :resolution then 3
|
|
18
|
+
when :integrity then 4
|
|
19
|
+
when :render then 5
|
|
20
|
+
when :verify then 6
|
|
21
|
+
when :unsupported then 8
|
|
22
|
+
else 1
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_s
|
|
27
|
+
prefix = case category
|
|
28
|
+
when :parse then "#{@parse_kind} parse error"
|
|
29
|
+
when :usage then "usage error"
|
|
30
|
+
when :manifest then "manifest error"
|
|
31
|
+
when :resolution then "resolution error"
|
|
32
|
+
when :integrity then "integrity error"
|
|
33
|
+
when :render then "render error"
|
|
34
|
+
when :verify then "verify error"
|
|
35
|
+
when :unsupported then "unsupported feature"
|
|
36
|
+
when :io then "I/O error"
|
|
37
|
+
else category.to_s
|
|
38
|
+
end
|
|
39
|
+
"#{prefix}: #{super}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.parse(kind, message)
|
|
43
|
+
new(:parse, message, parse_kind: kind)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.manifest(message) = new(:manifest, message)
|
|
47
|
+
def self.resolution(message) = new(:resolution, message)
|
|
48
|
+
def self.integrity(message) = new(:integrity, message)
|
|
49
|
+
def self.render(message) = new(:render, message)
|
|
50
|
+
def self.verify(message) = new(:verify, message)
|
|
51
|
+
def self.unsupported(message) = new(:unsupported, message)
|
|
52
|
+
def self.usage(message) = new(:usage, message)
|
|
53
|
+
def self.io(error) = new(:io, error.message)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
PackageFormatHint = Struct.new(:roles, :file_path, :exports) do
|
|
5
|
+
def initialize(roles: [], file_path: nil, exports: [])
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module FormatManifest
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def uses_destination_dsl?(manifest)
|
|
14
|
+
manifest.targets.any? { |target| target.scoped || target.mode != "legacy" } ||
|
|
15
|
+
manifest.packages.any? { |package| package.bound || package.file } ||
|
|
16
|
+
manifest.local.any?(&:bound)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def classify_format_hints(project)
|
|
20
|
+
hints = {}
|
|
21
|
+
project.packages.each do |package|
|
|
22
|
+
roles = []
|
|
23
|
+
file_path = package.declaration.file
|
|
24
|
+
package.selected_exports.each do |export_name|
|
|
25
|
+
export = package.spec.exports[export_name]
|
|
26
|
+
next unless export
|
|
27
|
+
|
|
28
|
+
%w[fragment folder file].each do |role|
|
|
29
|
+
if Destination.export_kind_matches_role?(export.kind, role) && !roles.include?(role)
|
|
30
|
+
roles << role
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
if file_path.nil? && export.kind == "file"
|
|
34
|
+
file_path = export.default_path || export_name
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
hints[package.declaration.name] = PackageFormatHint.new(
|
|
38
|
+
roles: roles,
|
|
39
|
+
file_path: file_path,
|
|
40
|
+
exports: ambiguous_exports_for_roles(package.selected_exports, package.spec.exports, roles)
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
hints
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def recommend_manifest(manifest, hints)
|
|
47
|
+
recommended = if has_migratable_legacy_targets?(manifest)
|
|
48
|
+
migrate_legacy_manifest(manifest, hints)
|
|
49
|
+
else
|
|
50
|
+
deep_clone_manifest(manifest)
|
|
51
|
+
end
|
|
52
|
+
omit_context_resolved_exports(recommended)
|
|
53
|
+
omit_default_sources(recommended)
|
|
54
|
+
recommended
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def format_recommended(manifest, hints)
|
|
58
|
+
recommended = recommend_manifest(manifest, hints)
|
|
59
|
+
text = FormatSerialize.serialize_recommended(recommended)
|
|
60
|
+
reparsed = Pray.parse_manifest(text)
|
|
61
|
+
if reparsed.manifest_hash != recommended.manifest_hash
|
|
62
|
+
raise Error.manifest("formatted Prayfile did not round-trip to an equivalent manifest")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
text
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def has_migratable_legacy_targets?(manifest)
|
|
69
|
+
manifest.targets.any? do |target|
|
|
70
|
+
!target.scoped && target.mode == "legacy" &&
|
|
71
|
+
(!target.outputs.empty? || !target.skills.empty?)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def migrate_legacy_manifest(manifest, hints)
|
|
76
|
+
next_manifest = Manifest.new(
|
|
77
|
+
prayfile_version: manifest.prayfile_version,
|
|
78
|
+
sources: manifest.sources.map { |source| source.dup },
|
|
79
|
+
targets: [],
|
|
80
|
+
packages: manifest.packages.map { |package| clone_package(package) },
|
|
81
|
+
local: manifest.local.map { |local| local.dup },
|
|
82
|
+
symbols: manifest.symbols.dup,
|
|
83
|
+
render: manifest.render.dup
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
apply_format_hints(next_manifest.packages, hints)
|
|
87
|
+
|
|
88
|
+
compose_paths = unique_paths(
|
|
89
|
+
manifest.targets.flat_map { |target| target.outputs.map { |path| [path, target.name] } }
|
|
90
|
+
)
|
|
91
|
+
tree_paths = unique_paths(
|
|
92
|
+
manifest.targets.flat_map { |target| target.skills.map { |path| [path, target.name] } }
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
compose_paths.each do |path, target_names|
|
|
96
|
+
target = Destination.new_destination_target("compose", path)
|
|
97
|
+
locals_for_compose(next_manifest.local).each do |local|
|
|
98
|
+
Destination.bind_local_entry(target, local.path)
|
|
99
|
+
entry = next_manifest.local.find { |candidate| candidate.path == local.path }
|
|
100
|
+
entry.bound = true if entry
|
|
101
|
+
end
|
|
102
|
+
packages_for_role(next_manifest.packages, "fragment", target_names).each do |package|
|
|
103
|
+
Destination.bind_package_entry(target, package.name)
|
|
104
|
+
mark_package_bound(next_manifest.packages, package.name, "fragment")
|
|
105
|
+
end
|
|
106
|
+
next_manifest.targets << target
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
tree_paths.each do |path, target_names|
|
|
110
|
+
target = Destination.new_destination_target("tree", path)
|
|
111
|
+
packages_for_role(next_manifest.packages, "folder", target_names).each do |package|
|
|
112
|
+
Destination.bind_package_entry(target, package.name)
|
|
113
|
+
mark_package_bound(next_manifest.packages, package.name, "folder")
|
|
114
|
+
end
|
|
115
|
+
next_manifest.targets << target
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
next_manifest.packages.each do |package|
|
|
119
|
+
next unless package.file
|
|
120
|
+
|
|
121
|
+
package.bound = true
|
|
122
|
+
package.roles << "file" unless package.roles.include?("file")
|
|
123
|
+
end
|
|
124
|
+
next_manifest.local.each do |local|
|
|
125
|
+
local.position = "after" if local.bound
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
manifest.targets.each do |target|
|
|
129
|
+
next unless FormatSerialize.target_has_extras?(target)
|
|
130
|
+
|
|
131
|
+
next_manifest.targets << ManifestTarget.new(
|
|
132
|
+
name: target.name,
|
|
133
|
+
commands: target.commands.dup,
|
|
134
|
+
rules: target.rules.dup,
|
|
135
|
+
max_bytes: target.max_bytes,
|
|
136
|
+
mode: "legacy",
|
|
137
|
+
scoped: false
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
next_manifest
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def apply_format_hints(packages, hints)
|
|
145
|
+
packages.each do |package|
|
|
146
|
+
hint = hints[package.name]
|
|
147
|
+
if hint
|
|
148
|
+
hint.roles.each do |role|
|
|
149
|
+
package.roles << role unless package.roles.include?(role)
|
|
150
|
+
end
|
|
151
|
+
package.file ||= hint.file_path
|
|
152
|
+
if package.exports.empty? && !hint.exports.empty?
|
|
153
|
+
package.exports = hint.exports.dup
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
if package.file && !package.roles.include?("file")
|
|
157
|
+
package.roles << "file"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def omit_context_resolved_exports(manifest)
|
|
163
|
+
manifest.packages.each do |package|
|
|
164
|
+
package.exports.clear if package.bound && package.exports.length <= 1
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def omit_default_sources(manifest)
|
|
169
|
+
sole_source = (manifest.sources.length == 1) ? manifest.sources.first.name : nil
|
|
170
|
+
source_names = manifest.sources.map(&:name).to_set
|
|
171
|
+
manifest.packages.each do |package|
|
|
172
|
+
source = package.source
|
|
173
|
+
next unless source
|
|
174
|
+
|
|
175
|
+
matches_sole = sole_source == source
|
|
176
|
+
namespace = package.name.split("/", 2).first
|
|
177
|
+
matches_namespace = namespace == source && source_names.include?(source)
|
|
178
|
+
package.source = nil if matches_sole || matches_namespace
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def unique_paths(items)
|
|
183
|
+
map = {}
|
|
184
|
+
items.each do |path, target_name|
|
|
185
|
+
map[path] ||= Set.new
|
|
186
|
+
map[path] << target_name
|
|
187
|
+
end
|
|
188
|
+
map.sort.map { |path, names| [path, names] }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def locals_for_compose(locals)
|
|
192
|
+
before = []
|
|
193
|
+
after = []
|
|
194
|
+
locals.each do |local|
|
|
195
|
+
next if local.bound
|
|
196
|
+
|
|
197
|
+
if %w[start before].include?(local.position)
|
|
198
|
+
before << local
|
|
199
|
+
else
|
|
200
|
+
after << local
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
before + after
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def packages_for_role(packages, role, target_names)
|
|
207
|
+
packages.select do |package|
|
|
208
|
+
next false if package.file
|
|
209
|
+
if !package.targets.empty? && package.targets.none? { |name| target_names.include?(name) }
|
|
210
|
+
next false
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
package.roles.include?(role)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def mark_package_bound(packages, name, role)
|
|
218
|
+
package = packages.find { |entry| entry.name == name }
|
|
219
|
+
return unless package
|
|
220
|
+
|
|
221
|
+
package.bound = true
|
|
222
|
+
package.roles << role unless package.roles.include?(role)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def ambiguous_exports_for_roles(selected_exports, exports, roles)
|
|
226
|
+
ambiguous = []
|
|
227
|
+
roles.each do |role|
|
|
228
|
+
matching = selected_exports.select do |export_name|
|
|
229
|
+
export = exports[export_name]
|
|
230
|
+
export && Destination.export_kind_matches_role?(export.kind, role)
|
|
231
|
+
end
|
|
232
|
+
next unless matching.length > 1
|
|
233
|
+
|
|
234
|
+
matching.each do |export_name|
|
|
235
|
+
ambiguous << export_name unless ambiguous.include?(export_name)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
ambiguous
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def deep_clone_manifest(manifest)
|
|
242
|
+
Marshal.load(Marshal.dump(manifest))
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def clone_package(package)
|
|
246
|
+
package.dup.tap do |copy|
|
|
247
|
+
copy.exports = package.exports.dup
|
|
248
|
+
copy.targets = package.targets.dup
|
|
249
|
+
copy.features = package.features.dup
|
|
250
|
+
copy.groups = package.groups.dup
|
|
251
|
+
copy.roles = package.roles.dup
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|