mt-lang 0.2.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/.ruby-version +1 -0
- data/AUTHORS +3 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +82 -0
- data/LICENSE +21 -0
- data/README.md +1208 -0
- data/Rakefile +332 -0
- data/bin/mtc +8 -0
- data/bin/profile-mtc-checks +133 -0
- data/bin/tracy-profiler +0 -0
- data/docs/build-guide.md +536 -0
- data/docs/index.html +2778 -0
- data/docs/language-design.md +1519 -0
- data/docs/language-manual.md +1534 -0
- data/lib/milk_tea/base.rb +49 -0
- data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
- data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
- data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
- data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
- data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
- data/lib/milk_tea/bindings/bindgen.rb +207 -0
- data/lib/milk_tea/bindings/cli.rb +121 -0
- data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
- data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
- data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
- data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
- data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
- data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
- data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
- data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
- data/lib/milk_tea/bindings/steamworks.rb +629 -0
- data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
- data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
- data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
- data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
- data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
- data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
- data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
- data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
- data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
- data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
- data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
- data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
- data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
- data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
- data/lib/milk_tea/bindings.rb +23 -0
- data/lib/milk_tea/core/ast.rb +311 -0
- data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
- data/lib/milk_tea/core/binding_types.rb +18 -0
- data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
- data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
- data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
- data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
- data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
- data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
- data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
- data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
- data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
- data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
- data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
- data/lib/milk_tea/core/c_backend/statements.rb +563 -0
- data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
- data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
- data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
- data/lib/milk_tea/core/c_backend.rb +287 -0
- data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
- data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
- data/lib/milk_tea/core/compile_time.rb +329 -0
- data/lib/milk_tea/core/control_flow/builder.rb +582 -0
- data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
- data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
- data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
- data/lib/milk_tea/core/control_flow/graph.rb +90 -0
- data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
- data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
- data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
- data/lib/milk_tea/core/control_flow/termination.rb +31 -0
- data/lib/milk_tea/core/control_flow.rb +34 -0
- data/lib/milk_tea/core/cst.rb +48 -0
- data/lib/milk_tea/core/cst_builder.rb +19 -0
- data/lib/milk_tea/core/ir.rb +85 -0
- data/lib/milk_tea/core/keywords.rb +97 -0
- data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
- data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
- data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
- data/lib/milk_tea/core/lexer/indentation.rb +62 -0
- data/lib/milk_tea/core/lexer/numbers.rb +96 -0
- data/lib/milk_tea/core/lexer/recovery.rb +32 -0
- data/lib/milk_tea/core/lexer/strings.rb +167 -0
- data/lib/milk_tea/core/lexer/symbols.rb +71 -0
- data/lib/milk_tea/core/lexer/trivia.rb +53 -0
- data/lib/milk_tea/core/lexer.rb +430 -0
- data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
- data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
- data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
- data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
- data/lib/milk_tea/core/lowering/async.rb +714 -0
- data/lib/milk_tea/core/lowering/block.rb +1052 -0
- data/lib/milk_tea/core/lowering/calls.rb +1565 -0
- data/lib/milk_tea/core/lowering/declarations.rb +214 -0
- data/lib/milk_tea/core/lowering/dyn.rb +206 -0
- data/lib/milk_tea/core/lowering/events.rb +1054 -0
- data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
- data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
- data/lib/milk_tea/core/lowering/functions.rb +242 -0
- data/lib/milk_tea/core/lowering/loops.rb +1087 -0
- data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
- data/lib/milk_tea/core/lowering/proc.rb +419 -0
- data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
- data/lib/milk_tea/core/lowering/scans.rb +220 -0
- data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
- data/lib/milk_tea/core/lowering/utils.rb +1453 -0
- data/lib/milk_tea/core/lowering.rb +378 -0
- data/lib/milk_tea/core/module_binder.rb +181 -0
- data/lib/milk_tea/core/module_loader/errors.rb +21 -0
- data/lib/milk_tea/core/module_loader.rb +479 -0
- data/lib/milk_tea/core/module_path_resolver.rb +153 -0
- data/lib/milk_tea/core/module_roots.rb +88 -0
- data/lib/milk_tea/core/parser/attributes.rb +71 -0
- data/lib/milk_tea/core/parser/blocks.rb +119 -0
- data/lib/milk_tea/core/parser/declarations.rb +749 -0
- data/lib/milk_tea/core/parser/expressions.rb +624 -0
- data/lib/milk_tea/core/parser/recovery.rb +131 -0
- data/lib/milk_tea/core/parser/statements.rb +756 -0
- data/lib/milk_tea/core/parser/types.rb +271 -0
- data/lib/milk_tea/core/parser.rb +400 -0
- data/lib/milk_tea/core/prelude_installer.rb +29 -0
- data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
- data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
- data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
- data/lib/milk_tea/core/pretty_printer.rb +17 -0
- data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
- data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
- data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
- data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
- data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
- data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
- data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
- data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
- data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
- data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
- data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
- data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
- data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
- data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
- data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
- data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
- data/lib/milk_tea/core/token.rb +26 -0
- data/lib/milk_tea/core/token_stream.rb +30 -0
- data/lib/milk_tea/core/types/layout.rb +243 -0
- data/lib/milk_tea/core/types/predicates.rb +609 -0
- data/lib/milk_tea/core/types/registry.rb +83 -0
- data/lib/milk_tea/core/types/types.rb +1696 -0
- data/lib/milk_tea/core/types/visitor.rb +442 -0
- data/lib/milk_tea/core.rb +25 -0
- data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
- data/lib/milk_tea/dap/protocol.rb +58 -0
- data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
- data/lib/milk_tea/dap/server/debug_map.rb +291 -0
- data/lib/milk_tea/dap/server/handlers.rb +374 -0
- data/lib/milk_tea/dap/server/launch.rb +261 -0
- data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
- data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
- data/lib/milk_tea/dap/server/utilities.rb +160 -0
- data/lib/milk_tea/dap/server/wire.rb +55 -0
- data/lib/milk_tea/dap/server.rb +131 -0
- data/lib/milk_tea/dap/session.rb +152 -0
- data/lib/milk_tea/dap.rb +6 -0
- data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
- data/lib/milk_tea/lsp/diagnostics.rb +611 -0
- data/lib/milk_tea/lsp/protocol.rb +104 -0
- data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
- data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
- data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
- data/lib/milk_tea/lsp/server/completion.rb +1099 -0
- data/lib/milk_tea/lsp/server/configuration.rb +167 -0
- data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
- data/lib/milk_tea/lsp/server/definition.rb +779 -0
- data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
- data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
- data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
- data/lib/milk_tea/lsp/server/formatting.rb +575 -0
- data/lib/milk_tea/lsp/server/hover.rb +1465 -0
- data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
- data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
- data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
- data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
- data/lib/milk_tea/lsp/server/progress.rb +47 -0
- data/lib/milk_tea/lsp/server/references.rb +433 -0
- data/lib/milk_tea/lsp/server/rename.rb +598 -0
- data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
- data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
- data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
- data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
- data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
- data/lib/milk_tea/lsp/server/utilities.rb +520 -0
- data/lib/milk_tea/lsp/server.rb +415 -0
- data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
- data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
- data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
- data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
- data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
- data/lib/milk_tea/lsp/workspace/store.rb +154 -0
- data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
- data/lib/milk_tea/lsp/workspace.rb +127 -0
- data/lib/milk_tea/lsp.rb +13 -0
- data/lib/milk_tea/packages/atomic_write.rb +63 -0
- data/lib/milk_tea/packages/dependency_solver.rb +194 -0
- data/lib/milk_tea/packages/graph.rb +139 -0
- data/lib/milk_tea/packages/lock.rb +421 -0
- data/lib/milk_tea/packages/manager_cli.rb +485 -0
- data/lib/milk_tea/packages/manifest.rb +356 -0
- data/lib/milk_tea/packages/manifest_editor.rb +134 -0
- data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
- data/lib/milk_tea/packages/registry_store.rb +420 -0
- data/lib/milk_tea/packages/services.rb +41 -0
- data/lib/milk_tea/packages/source_cache.rb +71 -0
- data/lib/milk_tea/packages/source_fetcher.rb +124 -0
- data/lib/milk_tea/packages/source_resolver.rb +389 -0
- data/lib/milk_tea/packages/version.rb +164 -0
- data/lib/milk_tea/packages.rb +16 -0
- data/lib/milk_tea/tooling/asset_pack.rb +141 -0
- data/lib/milk_tea/tooling/build.rb +1124 -0
- data/lib/milk_tea/tooling/build_cache.rb +240 -0
- data/lib/milk_tea/tooling/cli.rb +2688 -0
- data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
- data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
- data/lib/milk_tea/tooling/debug_map.rb +248 -0
- data/lib/milk_tea/tooling/docs_app.rb +369 -0
- data/lib/milk_tea/tooling/error_formatter.rb +86 -0
- data/lib/milk_tea/tooling/formatter.rb +787 -0
- data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
- data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
- data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
- data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
- data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
- data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
- data/lib/milk_tea/tooling/linter/rules.rb +593 -0
- data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
- data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
- data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
- data/lib/milk_tea/tooling/linter.rb +798 -0
- data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
- data/lib/milk_tea/tooling/public/css/docs.css +166 -0
- data/lib/milk_tea/tooling/public/js/docs.js +94 -0
- data/lib/milk_tea/tooling/run.rb +408 -0
- data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
- data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
- data/lib/milk_tea/tooling/views/404.erb +7 -0
- data/lib/milk_tea/tooling/views/index.erb +87 -0
- data/lib/milk_tea/tooling/views/layout.erb +69 -0
- data/lib/milk_tea/tooling/views/module.erb +97 -0
- data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
- data/lib/milk_tea/tooling.rb +20 -0
- data/lib/milk_tea.rb +8 -0
- metadata +426 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module MilkTea
|
|
6
|
+
class PackageSourceResolverError < StandardError; end
|
|
7
|
+
|
|
8
|
+
class PackageSourceResolver
|
|
9
|
+
attr_reader :resolved_registry_versions
|
|
10
|
+
|
|
11
|
+
RegistryDependencyKey = Data.define(:parent_source_key, :dependency_name)
|
|
12
|
+
|
|
13
|
+
class SourceIdentity
|
|
14
|
+
def kind
|
|
15
|
+
raise NotImplementedError, "#{self.class} must implement #kind"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def lock_attributes
|
|
19
|
+
raise NotImplementedError, "#{self.class} must implement #lock_attributes"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cache_key
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cacheable?
|
|
27
|
+
!cache_key.nil?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class PathIdentity < SourceIdentity
|
|
32
|
+
attr_reader :path
|
|
33
|
+
|
|
34
|
+
def initialize(path)
|
|
35
|
+
@path = File.expand_path(path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def kind
|
|
39
|
+
:path
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def lock_attributes
|
|
43
|
+
{
|
|
44
|
+
"source_path" => @path,
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class RegistryIdentity < SourceIdentity
|
|
50
|
+
attr_reader :package_name, :version
|
|
51
|
+
|
|
52
|
+
def initialize(package_name:, version:)
|
|
53
|
+
@package_name = normalize_required(package_name, "registry package name")
|
|
54
|
+
@version = normalize_required(version, "registry package version")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def kind
|
|
58
|
+
:registry
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def lock_attributes
|
|
62
|
+
{
|
|
63
|
+
"registry_package" => @package_name,
|
|
64
|
+
"registry_version" => @version,
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def cache_key
|
|
69
|
+
"registry/#{@package_name}@#{@version}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def normalize_required(value, label)
|
|
75
|
+
text = value.to_s.strip
|
|
76
|
+
raise PackageSourceResolverError, "#{label} cannot be empty" if text.empty?
|
|
77
|
+
if text.include?(File::SEPARATOR) || (File::ALT_SEPARATOR && text.include?(File::ALT_SEPARATOR))
|
|
78
|
+
raise PackageSourceResolverError, "#{label} cannot contain path separators"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
text
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class GitIdentity < SourceIdentity
|
|
86
|
+
attr_reader :url, :revision, :subdir
|
|
87
|
+
|
|
88
|
+
def initialize(url:, revision:, subdir: nil)
|
|
89
|
+
@url = normalize_required(url, "git source url")
|
|
90
|
+
@revision = normalize_required(revision, "git source revision")
|
|
91
|
+
@subdir = normalize_optional(subdir)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def kind
|
|
95
|
+
:git
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def lock_attributes
|
|
99
|
+
attributes = {
|
|
100
|
+
"git_url" => @url,
|
|
101
|
+
"git_rev" => @revision,
|
|
102
|
+
}
|
|
103
|
+
attributes["git_subdir"] = @subdir if @subdir
|
|
104
|
+
attributes
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def cache_key
|
|
108
|
+
digest = Digest::SHA256.hexdigest([@url, @revision, @subdir].join("\0"))
|
|
109
|
+
"git/#{digest}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def normalize_required(value, label)
|
|
115
|
+
text = value.to_s.strip
|
|
116
|
+
raise PackageSourceResolverError, "#{label} cannot be empty" if text.empty?
|
|
117
|
+
|
|
118
|
+
text
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def normalize_optional(value)
|
|
122
|
+
text = value.to_s.strip
|
|
123
|
+
return nil if text.empty?
|
|
124
|
+
raise PackageSourceResolverError, "git source subdir cannot be absolute" if text.start_with?("/", "\\")
|
|
125
|
+
raise PackageSourceResolverError, "git source subdir cannot be absolute" if text.match?(/\A[A-Za-z]:[\\\/]/)
|
|
126
|
+
|
|
127
|
+
segments = text.split(/[\\\/]+/).reject(&:empty?)
|
|
128
|
+
normalized_segments = []
|
|
129
|
+
segments.each do |segment|
|
|
130
|
+
next if segment == "."
|
|
131
|
+
raise PackageSourceResolverError, "git source subdir cannot escape the repository root" if segment == ".."
|
|
132
|
+
|
|
133
|
+
normalized_segments << segment
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
normalized = normalized_segments.join("/")
|
|
137
|
+
normalized.empty? ? nil : normalized
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
Source = Data.define(:identity, :local_root) do
|
|
142
|
+
def kind
|
|
143
|
+
identity.kind
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def lock_attributes
|
|
147
|
+
identity.lock_attributes
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def cache_key
|
|
151
|
+
identity.cache_key
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def cacheable?
|
|
155
|
+
identity.cacheable?
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
ResolvedPackage = Data.define(:manifest, :source)
|
|
160
|
+
|
|
161
|
+
def self.source_key_for_identity(identity)
|
|
162
|
+
return "path:#{File.expand_path(identity.path)}" if identity.is_a?(PathIdentity)
|
|
163
|
+
return "#{identity.kind}:#{identity.cache_key}" if identity.cache_key
|
|
164
|
+
|
|
165
|
+
raise PackageSourceResolverError, "unsupported source identity #{identity.class} for registry dependency key"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.registry_dependency_key(parent_source_key:, dependency_name:)
|
|
169
|
+
RegistryDependencyKey.new(
|
|
170
|
+
parent_source_key: parent_source_key.to_s,
|
|
171
|
+
dependency_name: dependency_name.to_s,
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def initialize(source_cache: PackageSourceCache.new, remote_resolution: :reject, source_fetcher: nil, resolved_registry_versions: nil)
|
|
176
|
+
@source_cache = source_cache
|
|
177
|
+
@remote_resolution = remote_resolution
|
|
178
|
+
@source_fetcher = source_fetcher
|
|
179
|
+
@resolved_registry_versions = normalize_resolved_registry_versions(resolved_registry_versions)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def supports_dependency_solving?
|
|
183
|
+
@remote_resolution != :reject
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def with_resolved_registry_versions(resolved_registry_versions)
|
|
187
|
+
self.class.new(
|
|
188
|
+
source_cache: @source_cache,
|
|
189
|
+
remote_resolution: @remote_resolution,
|
|
190
|
+
source_fetcher: @source_fetcher,
|
|
191
|
+
resolved_registry_versions:,
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def source_for_manifest(manifest)
|
|
196
|
+
Source.new(
|
|
197
|
+
identity: PathIdentity.new(manifest.root_dir),
|
|
198
|
+
local_root: manifest.root_dir,
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def identity_from_lock(entry, lock_path)
|
|
203
|
+
source_kind = entry["source_kind"].to_s
|
|
204
|
+
|
|
205
|
+
case source_kind
|
|
206
|
+
when "path"
|
|
207
|
+
source_path = entry["source_path"].to_s
|
|
208
|
+
raise PackageSourceResolverError, "package.lock missing source_path in #{lock_path}" if source_path.empty?
|
|
209
|
+
|
|
210
|
+
PathIdentity.new(source_path)
|
|
211
|
+
when "registry"
|
|
212
|
+
RegistryIdentity.new(
|
|
213
|
+
package_name: entry["registry_package"],
|
|
214
|
+
version: entry["registry_version"],
|
|
215
|
+
)
|
|
216
|
+
when "git"
|
|
217
|
+
GitIdentity.new(
|
|
218
|
+
url: entry["git_url"],
|
|
219
|
+
revision: entry["git_rev"],
|
|
220
|
+
subdir: entry["git_subdir"],
|
|
221
|
+
)
|
|
222
|
+
else
|
|
223
|
+
raise PackageSourceResolverError,
|
|
224
|
+
"unsupported package source_kind #{source_kind.inspect} in #{lock_path}; supported kinds are path, registry, and git"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def resolve(dependency, parent_manifest:, parent_source: nil)
|
|
229
|
+
if dependency.path
|
|
230
|
+
manifest = PackageManifest.load(dependency.path)
|
|
231
|
+
validate_fixed_dependency_version_requirement!(dependency, manifest, parent_manifest:)
|
|
232
|
+
return ResolvedPackage.new(manifest:, source: source_for_manifest(manifest))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
return resolve_registry_dependency(dependency, parent_manifest:, parent_source:) if dependency.registry?
|
|
236
|
+
return resolve_git_dependency(dependency, parent_manifest:) if dependency.git
|
|
237
|
+
|
|
238
|
+
raise PackageSourceResolverError,
|
|
239
|
+
"dependency #{dependency.name} in #{parent_manifest.manifest_path} has no supported source resolver"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def source_from_lock(entry, lock_path)
|
|
243
|
+
identity = identity_from_lock(entry, lock_path)
|
|
244
|
+
source_for_identity(identity, lock_path)
|
|
245
|
+
rescue PackageSourceCacheError => e
|
|
246
|
+
raise PackageSourceResolverError, e.message
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def resolve_registry_dependency(dependency, parent_manifest:, parent_source: nil)
|
|
252
|
+
if dependency.exact_registry_version?
|
|
253
|
+
identity = RegistryIdentity.new(package_name: dependency.name, version: dependency.version)
|
|
254
|
+
return resolve_cache_backed_dependency(identity, dependency:, parent_manifest:)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
parent_source_key = registry_dependency_parent_source_key(parent_manifest:, parent_source:)
|
|
258
|
+
dependency_key = self.class.registry_dependency_key(parent_source_key:, dependency_name: dependency.name)
|
|
259
|
+
resolved_version = @resolved_registry_versions[dependency_key] || @resolved_registry_versions[dependency.name]
|
|
260
|
+
if resolved_version
|
|
261
|
+
unless dependency.version_req.matches?(resolved_version)
|
|
262
|
+
raise PackageSourceResolverError,
|
|
263
|
+
"dependency #{dependency.name} in #{parent_manifest.manifest_path} resolved version #{resolved_version.inspect}, which does not satisfy #{dependency.version_req}"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
identity = RegistryIdentity.new(package_name: dependency.name, version: resolved_version)
|
|
267
|
+
return resolve_cache_backed_dependency(identity, dependency:, parent_manifest:)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
raise PackageSourceResolverError,
|
|
271
|
+
"dependency #{dependency.name} in #{parent_manifest.manifest_path} uses version requirement #{dependency.version_req}, but registry dependency solving is not implemented yet; use an exact version for now"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def resolve_git_dependency(dependency, parent_manifest:)
|
|
275
|
+
identity = GitIdentity.new(
|
|
276
|
+
url: dependency.git,
|
|
277
|
+
revision: dependency.git_rev,
|
|
278
|
+
subdir: dependency.git_subdir,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
resolve_cache_backed_dependency(identity, dependency:, parent_manifest:)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def resolve_cache_backed_dependency(identity, dependency:, parent_manifest:)
|
|
285
|
+
context_label = "dependency #{dependency.name} in #{parent_manifest.manifest_path}"
|
|
286
|
+
source = case @remote_resolution
|
|
287
|
+
when :reject
|
|
288
|
+
raise PackageSourceResolverError,
|
|
289
|
+
"#{context_label} uses #{identity.kind} resolution, but live dependency resolution only supports local path dependencies; run mtc deps lock and then use --locked or --frozen"
|
|
290
|
+
when :cache
|
|
291
|
+
source_for_identity(identity, context_label)
|
|
292
|
+
when :materialize
|
|
293
|
+
unless @source_fetcher
|
|
294
|
+
raise PackageSourceResolverError,
|
|
295
|
+
"#{context_label} requested #{identity.kind} materialization without a package source fetcher"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
@source_fetcher.materialize_identity(package_name: dependency.name, identity:)
|
|
299
|
+
source_for_identity(identity, context_label)
|
|
300
|
+
else
|
|
301
|
+
raise PackageSourceResolverError, "unknown remote resolution mode #{@remote_resolution.inspect}"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
manifest = PackageManifest.load(source.local_root)
|
|
305
|
+
if manifest.package_name != dependency.name
|
|
306
|
+
raise PackageSourceResolverError,
|
|
307
|
+
"#{context_label} resolved package #{manifest.package_name} at #{manifest.manifest_path}"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
if identity.is_a?(RegistryIdentity) && manifest.package_version != identity.version
|
|
311
|
+
raise PackageSourceResolverError,
|
|
312
|
+
"#{context_label} resolved version #{manifest.package_version.inspect} at #{manifest.manifest_path}; expected #{identity.version.inspect}"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
ResolvedPackage.new(manifest:, source:)
|
|
316
|
+
rescue PackageManifestError => e
|
|
317
|
+
raise PackageSourceResolverError, e.message
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def validate_fixed_dependency_version_requirement!(dependency, manifest, parent_manifest:)
|
|
321
|
+
return unless dependency.version_req
|
|
322
|
+
|
|
323
|
+
package_version = manifest.package_version
|
|
324
|
+
if package_version.nil?
|
|
325
|
+
raise PackageSourceResolverError,
|
|
326
|
+
"dependency #{dependency.name} in #{parent_manifest.manifest_path} requires version #{dependency.version_req}, but #{manifest.manifest_path} does not declare package.version"
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
return if dependency.version_req.matches?(package_version)
|
|
330
|
+
|
|
331
|
+
raise PackageSourceResolverError,
|
|
332
|
+
"dependency #{dependency.name} in #{parent_manifest.manifest_path} resolved version #{package_version.inspect} at #{manifest.manifest_path}, which does not satisfy #{dependency.version_req}"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def source_for_identity(identity, context_label)
|
|
336
|
+
local_root = if identity.is_a?(PathIdentity)
|
|
337
|
+
identity.path
|
|
338
|
+
else
|
|
339
|
+
manifest_path = @source_cache.manifest_path_for(identity)
|
|
340
|
+
unless File.file?(manifest_path)
|
|
341
|
+
raise PackageSourceResolverError,
|
|
342
|
+
"package source_kind #{identity.kind.inspect} in #{context_label} is not materialized in the source cache: expected #{manifest_path}"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
@source_cache.materialized_root_for(identity)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
Source.new(
|
|
349
|
+
identity:,
|
|
350
|
+
local_root:,
|
|
351
|
+
)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def registry_dependency_parent_source_key(parent_manifest:, parent_source:)
|
|
355
|
+
identity = if parent_source
|
|
356
|
+
parent_source.identity
|
|
357
|
+
else
|
|
358
|
+
source_for_manifest(parent_manifest).identity
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
self.class.source_key_for_identity(identity)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def normalize_resolved_registry_versions(versions)
|
|
365
|
+
return {} unless versions
|
|
366
|
+
|
|
367
|
+
versions.each_with_object({}) do |(key, version), resolved|
|
|
368
|
+
normalized_key = case key
|
|
369
|
+
when RegistryDependencyKey
|
|
370
|
+
self.class.registry_dependency_key(
|
|
371
|
+
parent_source_key: key.parent_source_key,
|
|
372
|
+
dependency_name: key.dependency_name,
|
|
373
|
+
)
|
|
374
|
+
when Array
|
|
375
|
+
if key.length != 2
|
|
376
|
+
raise PackageSourceResolverError,
|
|
377
|
+
"registry dependency key must contain parent manifest path and dependency name"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
self.class.registry_dependency_key(parent_source_key: key[0], dependency_name: key[1])
|
|
381
|
+
else
|
|
382
|
+
key.to_s
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
resolved[normalized_key] = version.to_s
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
class PackageVersionError < StandardError; end
|
|
5
|
+
|
|
6
|
+
class PackageVersion
|
|
7
|
+
include Comparable
|
|
8
|
+
|
|
9
|
+
VERSION_PATTERN = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\z/
|
|
10
|
+
|
|
11
|
+
attr_reader :major, :minor, :patch
|
|
12
|
+
|
|
13
|
+
def self.parse(value, label: "package version")
|
|
14
|
+
return value if value.is_a?(self)
|
|
15
|
+
|
|
16
|
+
text = value.to_s.strip
|
|
17
|
+
raise PackageVersionError, "#{label} cannot be empty" if text.empty?
|
|
18
|
+
|
|
19
|
+
match = VERSION_PATTERN.match(text)
|
|
20
|
+
unless match
|
|
21
|
+
raise PackageVersionError,
|
|
22
|
+
"#{label} #{text.inspect} must use semantic version format MAJOR.MINOR.PATCH"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
new(match[1].to_i, match[2].to_i, match[3].to_i)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(major, minor, patch)
|
|
29
|
+
@major = Integer(major)
|
|
30
|
+
@minor = Integer(minor)
|
|
31
|
+
@patch = Integer(patch)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def <=>(other)
|
|
35
|
+
return nil unless other.is_a?(PackageVersion)
|
|
36
|
+
|
|
37
|
+
[major, minor, patch] <=> [other.major, other.minor, other.patch]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
"#{major}.#{minor}.#{patch}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class PackageVersionReq
|
|
46
|
+
Constraint = Data.define(:operator, :version) do
|
|
47
|
+
def matches?(candidate)
|
|
48
|
+
case operator
|
|
49
|
+
when :eq
|
|
50
|
+
candidate == version
|
|
51
|
+
when :gt
|
|
52
|
+
candidate > version
|
|
53
|
+
when :gte
|
|
54
|
+
candidate >= version
|
|
55
|
+
when :lt
|
|
56
|
+
candidate < version
|
|
57
|
+
when :lte
|
|
58
|
+
candidate <= version
|
|
59
|
+
else
|
|
60
|
+
raise PackageVersionError, "unsupported package version operator #{operator.inspect}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_s
|
|
65
|
+
prefix = case operator
|
|
66
|
+
when :eq then "="
|
|
67
|
+
when :gt then ">"
|
|
68
|
+
when :gte then ">="
|
|
69
|
+
when :lt then "<"
|
|
70
|
+
when :lte then "<="
|
|
71
|
+
else
|
|
72
|
+
raise PackageVersionError, "unsupported package version operator #{operator.inspect}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
"#{prefix}#{version}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
attr_reader :constraints
|
|
80
|
+
|
|
81
|
+
def self.parse(value, label: "package version requirement")
|
|
82
|
+
text = value.to_s.strip
|
|
83
|
+
raise PackageVersionError, "#{label} cannot be empty" if text.empty?
|
|
84
|
+
|
|
85
|
+
segments = text.split(",", -1).map(&:strip)
|
|
86
|
+
if segments.empty? || segments.any?(&:empty?)
|
|
87
|
+
raise PackageVersionError, "#{label} #{text.inspect} contains an empty constraint"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
constraints = segments.flat_map do |segment|
|
|
91
|
+
parse_segment(segment, label:)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
new(constraints)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.parse_segment(segment, label:)
|
|
98
|
+
case segment
|
|
99
|
+
when /\A\^(.+)\z/
|
|
100
|
+
version = PackageVersion.parse(Regexp.last_match(1), label:)
|
|
101
|
+
[Constraint.new(:gte, version), Constraint.new(:lt, caret_upper_bound(version))]
|
|
102
|
+
when /\A~(.+)\z/
|
|
103
|
+
version = PackageVersion.parse(Regexp.last_match(1), label:)
|
|
104
|
+
[Constraint.new(:gte, version), Constraint.new(:lt, tilde_upper_bound(version))]
|
|
105
|
+
when /\A(>=|<=|>|<|=)\s*(.+)\z/
|
|
106
|
+
version = PackageVersion.parse(Regexp.last_match(2), label:)
|
|
107
|
+
[Constraint.new(operator_for(Regexp.last_match(1)), version)]
|
|
108
|
+
else
|
|
109
|
+
[Constraint.new(:eq, PackageVersion.parse(segment, label:))]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
private_class_method :parse_segment
|
|
113
|
+
|
|
114
|
+
def self.operator_for(token)
|
|
115
|
+
case token
|
|
116
|
+
when "=" then :eq
|
|
117
|
+
when ">" then :gt
|
|
118
|
+
when ">=" then :gte
|
|
119
|
+
when "<" then :lt
|
|
120
|
+
when "<=" then :lte
|
|
121
|
+
else
|
|
122
|
+
raise PackageVersionError, "unsupported package version operator #{token.inspect}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
private_class_method :operator_for
|
|
126
|
+
|
|
127
|
+
def self.caret_upper_bound(version)
|
|
128
|
+
if version.major.positive?
|
|
129
|
+
PackageVersion.new(version.major + 1, 0, 0)
|
|
130
|
+
elsif version.minor.positive?
|
|
131
|
+
PackageVersion.new(0, version.minor + 1, 0)
|
|
132
|
+
else
|
|
133
|
+
PackageVersion.new(0, 0, version.patch + 1)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
private_class_method :caret_upper_bound
|
|
137
|
+
|
|
138
|
+
def self.tilde_upper_bound(version)
|
|
139
|
+
PackageVersion.new(version.major, version.minor + 1, 0)
|
|
140
|
+
end
|
|
141
|
+
private_class_method :tilde_upper_bound
|
|
142
|
+
|
|
143
|
+
def initialize(constraints)
|
|
144
|
+
@constraints = constraints.freeze
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def exact?
|
|
148
|
+
constraints.length == 1 && constraints.first.operator == :eq
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def exact_version
|
|
152
|
+
constraints.first.version if exact?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def matches?(version)
|
|
156
|
+
candidate = PackageVersion.parse(version)
|
|
157
|
+
constraints.all? { |constraint| constraint.matches?(candidate) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def to_s
|
|
161
|
+
constraints.map(&:to_s).join(", ")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "packages/version"
|
|
4
|
+
require_relative "packages/manifest"
|
|
5
|
+
require_relative "packages/source_cache"
|
|
6
|
+
require_relative "packages/registry_store"
|
|
7
|
+
require_relative "packages/source_resolver"
|
|
8
|
+
require_relative "packages/source_fetcher"
|
|
9
|
+
require_relative "packages/registry_metadata_provider"
|
|
10
|
+
require_relative "packages/dependency_solver"
|
|
11
|
+
require_relative "packages/graph"
|
|
12
|
+
require_relative "packages/atomic_write"
|
|
13
|
+
require_relative "packages/lock"
|
|
14
|
+
require_relative "packages/manifest_editor"
|
|
15
|
+
require_relative "packages/services"
|
|
16
|
+
require_relative "packages/manager_cli"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module MilkTea
|
|
7
|
+
class AssetPackError < StandardError; end
|
|
8
|
+
|
|
9
|
+
class AssetPack
|
|
10
|
+
MAGIC = "MTAP".b.freeze
|
|
11
|
+
VERSION = 1
|
|
12
|
+
HEADER_FLAGS = 0
|
|
13
|
+
ENTRY_FLAGS_RAW = 0
|
|
14
|
+
HEADER_FORMAT = "a4vvVQ<Q<".freeze
|
|
15
|
+
ENTRY_PREFIX_FORMAT = "VVQ<Q<Q<".freeze
|
|
16
|
+
HEADER_SIZE = [MAGIC, VERSION, HEADER_FLAGS, 0, 0, 0].pack(HEADER_FORMAT).bytesize
|
|
17
|
+
ENTRY_PREFIX_SIZE = [0, ENTRY_FLAGS_RAW, 0, 0, 0].pack(ENTRY_PREFIX_FORMAT).bytesize
|
|
18
|
+
|
|
19
|
+
SourceEntry = Data.define(:source_path, :logical_path, :stored_size, :unpacked_size, :flags)
|
|
20
|
+
Entry = Data.define(:source_path, :logical_path, :data_offset, :stored_size, :unpacked_size, :flags)
|
|
21
|
+
|
|
22
|
+
def self.write(output_path, source_paths)
|
|
23
|
+
new(output_path, source_paths).write
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(output_path, source_paths)
|
|
27
|
+
@output_path = File.expand_path(output_path)
|
|
28
|
+
@source_paths = source_paths.map { |source_path| File.expand_path(source_path) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write
|
|
32
|
+
ensure_output_does_not_overlap_sources!
|
|
33
|
+
|
|
34
|
+
source_entries = collect_source_entries
|
|
35
|
+
index_size = source_entries.sum { |entry| ENTRY_PREFIX_SIZE + entry.logical_path.bytesize }
|
|
36
|
+
data_offset = HEADER_SIZE + index_size
|
|
37
|
+
entries = assign_offsets(source_entries, data_offset)
|
|
38
|
+
|
|
39
|
+
FileUtils.mkdir_p(File.dirname(@output_path))
|
|
40
|
+
Tempfile.create(["milk-tea-asset-pack", ".bin"], File.dirname(@output_path)) do |file|
|
|
41
|
+
file.binmode
|
|
42
|
+
write_header(file, entries.length, index_size, data_offset)
|
|
43
|
+
write_index(file, entries)
|
|
44
|
+
write_data(file, entries)
|
|
45
|
+
file.flush
|
|
46
|
+
file.close
|
|
47
|
+
|
|
48
|
+
FileUtils.rm_f(@output_path)
|
|
49
|
+
FileUtils.mv(file.path, @output_path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@output_path
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def ensure_output_does_not_overlap_sources!
|
|
58
|
+
@source_paths.each do |source_path|
|
|
59
|
+
if File.directory?(source_path)
|
|
60
|
+
next unless path_within?(@output_path, source_path)
|
|
61
|
+
|
|
62
|
+
raise AssetPackError, "asset pack output would be written inside source tree: #{@output_path}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
next unless @output_path == source_path
|
|
66
|
+
|
|
67
|
+
raise AssetPackError, "asset pack output would overwrite source file: #{@output_path}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def collect_source_entries
|
|
72
|
+
@source_paths.flat_map do |source_path|
|
|
73
|
+
collect_source_path(source_path, File.basename(source_path))
|
|
74
|
+
end.sort_by(&:logical_path)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def collect_source_path(source_path, logical_path)
|
|
78
|
+
stat = File.lstat(source_path)
|
|
79
|
+
|
|
80
|
+
if stat.directory?
|
|
81
|
+
Dir.children(source_path).sort.flat_map do |child|
|
|
82
|
+
collect_source_path(File.join(source_path, child), join_logical_path(logical_path, child))
|
|
83
|
+
end
|
|
84
|
+
elsif stat.file?
|
|
85
|
+
file_size = File.size(source_path)
|
|
86
|
+
[SourceEntry.new(source_path:, logical_path:, stored_size: file_size, unpacked_size: file_size, flags: ENTRY_FLAGS_RAW)]
|
|
87
|
+
else
|
|
88
|
+
raise AssetPackError, "asset pack supports only regular files and directories: #{source_path}"
|
|
89
|
+
end
|
|
90
|
+
rescue Errno::ENOENT
|
|
91
|
+
raise AssetPackError, "asset pack source not found: #{source_path}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def assign_offsets(source_entries, first_data_offset)
|
|
95
|
+
next_offset = first_data_offset
|
|
96
|
+
|
|
97
|
+
source_entries.map do |entry|
|
|
98
|
+
packed_entry = Entry.new(
|
|
99
|
+
source_path: entry.source_path,
|
|
100
|
+
logical_path: entry.logical_path,
|
|
101
|
+
data_offset: next_offset,
|
|
102
|
+
stored_size: entry.stored_size,
|
|
103
|
+
unpacked_size: entry.unpacked_size,
|
|
104
|
+
flags: entry.flags,
|
|
105
|
+
)
|
|
106
|
+
next_offset += entry.stored_size
|
|
107
|
+
packed_entry
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def write_header(io, entry_count, index_size, data_offset)
|
|
112
|
+
io.write([MAGIC, VERSION, HEADER_FLAGS, entry_count, index_size, data_offset].pack(HEADER_FORMAT))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def write_index(io, entries)
|
|
116
|
+
entries.each do |entry|
|
|
117
|
+
path_bytes = entry.logical_path.b
|
|
118
|
+
io.write([path_bytes.bytesize, entry.flags, entry.data_offset, entry.stored_size, entry.unpacked_size].pack(ENTRY_PREFIX_FORMAT))
|
|
119
|
+
io.write(path_bytes)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def write_data(io, entries)
|
|
124
|
+
entries.each do |entry|
|
|
125
|
+
File.open(entry.source_path, "rb") do |source_file|
|
|
126
|
+
IO.copy_stream(source_file, io)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def join_logical_path(left, right)
|
|
132
|
+
"#{left}/#{right}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def path_within?(path, root)
|
|
136
|
+
normalized_path = File.expand_path(path)
|
|
137
|
+
normalized_root = File.expand_path(root)
|
|
138
|
+
normalized_path == normalized_root || normalized_path.start_with?(normalized_root + File::SEPARATOR)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|