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.
Files changed (258) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/AUTHORS +3 -0
  4. data/Gemfile +19 -0
  5. data/Gemfile.lock +82 -0
  6. data/LICENSE +21 -0
  7. data/README.md +1208 -0
  8. data/Rakefile +332 -0
  9. data/bin/mtc +8 -0
  10. data/bin/profile-mtc-checks +133 -0
  11. data/bin/tracy-profiler +0 -0
  12. data/docs/build-guide.md +536 -0
  13. data/docs/index.html +2778 -0
  14. data/docs/language-design.md +1519 -0
  15. data/docs/language-manual.md +1534 -0
  16. data/lib/milk_tea/base.rb +49 -0
  17. data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
  18. data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
  19. data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
  20. data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
  21. data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
  22. data/lib/milk_tea/bindings/bindgen.rb +207 -0
  23. data/lib/milk_tea/bindings/cli.rb +121 -0
  24. data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
  25. data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
  26. data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
  27. data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
  28. data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
  29. data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
  30. data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
  31. data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
  32. data/lib/milk_tea/bindings/steamworks.rb +629 -0
  33. data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
  34. data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
  35. data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
  36. data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
  37. data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
  38. data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
  39. data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
  40. data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
  41. data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
  42. data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
  43. data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
  44. data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
  45. data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
  46. data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
  47. data/lib/milk_tea/bindings.rb +23 -0
  48. data/lib/milk_tea/core/ast.rb +311 -0
  49. data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
  50. data/lib/milk_tea/core/binding_types.rb +18 -0
  51. data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
  52. data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
  53. data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
  54. data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
  55. data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
  56. data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
  57. data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
  58. data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
  59. data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
  60. data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
  61. data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
  62. data/lib/milk_tea/core/c_backend/statements.rb +563 -0
  63. data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
  64. data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
  65. data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
  66. data/lib/milk_tea/core/c_backend.rb +287 -0
  67. data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
  68. data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
  69. data/lib/milk_tea/core/compile_time.rb +329 -0
  70. data/lib/milk_tea/core/control_flow/builder.rb +582 -0
  71. data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
  72. data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
  73. data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
  74. data/lib/milk_tea/core/control_flow/graph.rb +90 -0
  75. data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
  76. data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
  77. data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
  78. data/lib/milk_tea/core/control_flow/termination.rb +31 -0
  79. data/lib/milk_tea/core/control_flow.rb +34 -0
  80. data/lib/milk_tea/core/cst.rb +48 -0
  81. data/lib/milk_tea/core/cst_builder.rb +19 -0
  82. data/lib/milk_tea/core/ir.rb +85 -0
  83. data/lib/milk_tea/core/keywords.rb +97 -0
  84. data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
  85. data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
  86. data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
  87. data/lib/milk_tea/core/lexer/indentation.rb +62 -0
  88. data/lib/milk_tea/core/lexer/numbers.rb +96 -0
  89. data/lib/milk_tea/core/lexer/recovery.rb +32 -0
  90. data/lib/milk_tea/core/lexer/strings.rb +167 -0
  91. data/lib/milk_tea/core/lexer/symbols.rb +71 -0
  92. data/lib/milk_tea/core/lexer/trivia.rb +53 -0
  93. data/lib/milk_tea/core/lexer.rb +430 -0
  94. data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
  95. data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
  96. data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
  97. data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
  98. data/lib/milk_tea/core/lowering/async.rb +714 -0
  99. data/lib/milk_tea/core/lowering/block.rb +1052 -0
  100. data/lib/milk_tea/core/lowering/calls.rb +1565 -0
  101. data/lib/milk_tea/core/lowering/declarations.rb +214 -0
  102. data/lib/milk_tea/core/lowering/dyn.rb +206 -0
  103. data/lib/milk_tea/core/lowering/events.rb +1054 -0
  104. data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
  105. data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
  106. data/lib/milk_tea/core/lowering/functions.rb +242 -0
  107. data/lib/milk_tea/core/lowering/loops.rb +1087 -0
  108. data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
  109. data/lib/milk_tea/core/lowering/proc.rb +419 -0
  110. data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
  111. data/lib/milk_tea/core/lowering/scans.rb +220 -0
  112. data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
  113. data/lib/milk_tea/core/lowering/utils.rb +1453 -0
  114. data/lib/milk_tea/core/lowering.rb +378 -0
  115. data/lib/milk_tea/core/module_binder.rb +181 -0
  116. data/lib/milk_tea/core/module_loader/errors.rb +21 -0
  117. data/lib/milk_tea/core/module_loader.rb +479 -0
  118. data/lib/milk_tea/core/module_path_resolver.rb +153 -0
  119. data/lib/milk_tea/core/module_roots.rb +88 -0
  120. data/lib/milk_tea/core/parser/attributes.rb +71 -0
  121. data/lib/milk_tea/core/parser/blocks.rb +119 -0
  122. data/lib/milk_tea/core/parser/declarations.rb +749 -0
  123. data/lib/milk_tea/core/parser/expressions.rb +624 -0
  124. data/lib/milk_tea/core/parser/recovery.rb +131 -0
  125. data/lib/milk_tea/core/parser/statements.rb +756 -0
  126. data/lib/milk_tea/core/parser/types.rb +271 -0
  127. data/lib/milk_tea/core/parser.rb +400 -0
  128. data/lib/milk_tea/core/prelude_installer.rb +29 -0
  129. data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
  130. data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
  131. data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
  132. data/lib/milk_tea/core/pretty_printer.rb +17 -0
  133. data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
  134. data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
  135. data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
  136. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
  137. data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
  138. data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
  139. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
  140. data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
  141. data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
  142. data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
  143. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
  144. data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
  145. data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
  146. data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
  147. data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
  148. data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
  149. data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
  150. data/lib/milk_tea/core/token.rb +26 -0
  151. data/lib/milk_tea/core/token_stream.rb +30 -0
  152. data/lib/milk_tea/core/types/layout.rb +243 -0
  153. data/lib/milk_tea/core/types/predicates.rb +609 -0
  154. data/lib/milk_tea/core/types/registry.rb +83 -0
  155. data/lib/milk_tea/core/types/types.rb +1696 -0
  156. data/lib/milk_tea/core/types/visitor.rb +442 -0
  157. data/lib/milk_tea/core.rb +25 -0
  158. data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
  159. data/lib/milk_tea/dap/protocol.rb +58 -0
  160. data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
  161. data/lib/milk_tea/dap/server/debug_map.rb +291 -0
  162. data/lib/milk_tea/dap/server/handlers.rb +374 -0
  163. data/lib/milk_tea/dap/server/launch.rb +261 -0
  164. data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
  165. data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
  166. data/lib/milk_tea/dap/server/utilities.rb +160 -0
  167. data/lib/milk_tea/dap/server/wire.rb +55 -0
  168. data/lib/milk_tea/dap/server.rb +131 -0
  169. data/lib/milk_tea/dap/session.rb +152 -0
  170. data/lib/milk_tea/dap.rb +6 -0
  171. data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
  172. data/lib/milk_tea/lsp/diagnostics.rb +611 -0
  173. data/lib/milk_tea/lsp/protocol.rb +104 -0
  174. data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
  175. data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
  176. data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
  177. data/lib/milk_tea/lsp/server/completion.rb +1099 -0
  178. data/lib/milk_tea/lsp/server/configuration.rb +167 -0
  179. data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
  180. data/lib/milk_tea/lsp/server/definition.rb +779 -0
  181. data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
  182. data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
  183. data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
  184. data/lib/milk_tea/lsp/server/formatting.rb +575 -0
  185. data/lib/milk_tea/lsp/server/hover.rb +1465 -0
  186. data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
  187. data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
  188. data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
  189. data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
  190. data/lib/milk_tea/lsp/server/progress.rb +47 -0
  191. data/lib/milk_tea/lsp/server/references.rb +433 -0
  192. data/lib/milk_tea/lsp/server/rename.rb +598 -0
  193. data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
  194. data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
  195. data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
  196. data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
  197. data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
  198. data/lib/milk_tea/lsp/server/utilities.rb +520 -0
  199. data/lib/milk_tea/lsp/server.rb +415 -0
  200. data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
  201. data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
  202. data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
  203. data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
  204. data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
  205. data/lib/milk_tea/lsp/workspace/store.rb +154 -0
  206. data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
  207. data/lib/milk_tea/lsp/workspace.rb +127 -0
  208. data/lib/milk_tea/lsp.rb +13 -0
  209. data/lib/milk_tea/packages/atomic_write.rb +63 -0
  210. data/lib/milk_tea/packages/dependency_solver.rb +194 -0
  211. data/lib/milk_tea/packages/graph.rb +139 -0
  212. data/lib/milk_tea/packages/lock.rb +421 -0
  213. data/lib/milk_tea/packages/manager_cli.rb +485 -0
  214. data/lib/milk_tea/packages/manifest.rb +356 -0
  215. data/lib/milk_tea/packages/manifest_editor.rb +134 -0
  216. data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
  217. data/lib/milk_tea/packages/registry_store.rb +420 -0
  218. data/lib/milk_tea/packages/services.rb +41 -0
  219. data/lib/milk_tea/packages/source_cache.rb +71 -0
  220. data/lib/milk_tea/packages/source_fetcher.rb +124 -0
  221. data/lib/milk_tea/packages/source_resolver.rb +389 -0
  222. data/lib/milk_tea/packages/version.rb +164 -0
  223. data/lib/milk_tea/packages.rb +16 -0
  224. data/lib/milk_tea/tooling/asset_pack.rb +141 -0
  225. data/lib/milk_tea/tooling/build.rb +1124 -0
  226. data/lib/milk_tea/tooling/build_cache.rb +240 -0
  227. data/lib/milk_tea/tooling/cli.rb +2688 -0
  228. data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
  229. data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
  230. data/lib/milk_tea/tooling/debug_map.rb +248 -0
  231. data/lib/milk_tea/tooling/docs_app.rb +369 -0
  232. data/lib/milk_tea/tooling/error_formatter.rb +86 -0
  233. data/lib/milk_tea/tooling/formatter.rb +787 -0
  234. data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
  235. data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
  236. data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
  237. data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
  238. data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
  239. data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
  240. data/lib/milk_tea/tooling/linter/rules.rb +593 -0
  241. data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
  242. data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
  243. data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
  244. data/lib/milk_tea/tooling/linter.rb +798 -0
  245. data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
  246. data/lib/milk_tea/tooling/public/css/docs.css +166 -0
  247. data/lib/milk_tea/tooling/public/js/docs.js +94 -0
  248. data/lib/milk_tea/tooling/run.rb +408 -0
  249. data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
  250. data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
  251. data/lib/milk_tea/tooling/views/404.erb +7 -0
  252. data/lib/milk_tea/tooling/views/index.erb +87 -0
  253. data/lib/milk_tea/tooling/views/layout.erb +69 -0
  254. data/lib/milk_tea/tooling/views/module.erb +97 -0
  255. data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
  256. data/lib/milk_tea/tooling.rb +20 -0
  257. data/lib/milk_tea.rb +8 -0
  258. metadata +426 -0
@@ -0,0 +1,420 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi/escape"
4
+ require "fileutils"
5
+ require "net/http"
6
+ require "rubygems/package"
7
+ require "stringio"
8
+ require "tmpdir"
9
+ require "uri"
10
+ require "zlib"
11
+
12
+ module MilkTea
13
+ class PackageRegistryStoreError < StandardError; end
14
+
15
+ class PackageRegistryStore
16
+ Result = Data.define(:package_name, :version, :path)
17
+
18
+ def self.default_root(env: ENV, home: nil)
19
+ explicit_root = env.fetch("MILK_TEA_PACKAGE_REGISTRY", "").to_s.strip
20
+ return File.expand_path(explicit_root) unless explicit_root.empty?
21
+
22
+ xdg_data_home = env.fetch("XDG_DATA_HOME", "").to_s.strip
23
+ base_root = if xdg_data_home.empty?
24
+ File.join(home || Dir.home, ".local", "share")
25
+ else
26
+ File.expand_path(xdg_data_home)
27
+ end
28
+
29
+ File.join(base_root, "milk_tea", "registry")
30
+ end
31
+
32
+ def self.default_upstream_root(env: ENV)
33
+ explicit_root = env.fetch("MILK_TEA_PACKAGE_REGISTRY_UPSTREAM", "").to_s.strip
34
+ return nil if explicit_root.empty?
35
+
36
+ http_url?(explicit_root) ? explicit_root : File.expand_path(explicit_root)
37
+ end
38
+
39
+ def self.http_url?(value)
40
+ uri = URI.parse(value.to_s)
41
+ %w[http https].include?(uri.scheme)
42
+ rescue URI::InvalidURIError
43
+ false
44
+ end
45
+
46
+ attr_reader :root, :upstream_root
47
+
48
+ def initialize(root: self.class.default_root, upstream_root: self.class.default_upstream_root)
49
+ @root = File.expand_path(root)
50
+ @upstream_root = normalize_optional_root(upstream_root)
51
+ end
52
+
53
+ def package_root_for(package_or_identity, version = nil)
54
+ package_name, package_version = extract_package_version(package_or_identity, version)
55
+
56
+ File.join(@root, "packages", normalize_component(package_name, "package name"), normalize_component(package_version, "package version"))
57
+ end
58
+
59
+ def manifest_path_for(package_or_identity, version = nil)
60
+ File.join(package_root_for(package_or_identity, version), "package.toml")
61
+ end
62
+
63
+ def published?(package_or_identity, version = nil)
64
+ File.file?(manifest_path_for(package_or_identity, version))
65
+ end
66
+
67
+ def published_root_for(package_or_identity, version = nil, sync: false)
68
+ package_name, package_version = extract_package_version(package_or_identity, version)
69
+ sync(package_name, package_version) if sync && !published?(package_name, package_version) && upstream_configured?
70
+ manifest_path = manifest_path_for(package_name, package_version)
71
+ unless File.file?(manifest_path)
72
+ raise PackageRegistryStoreError,
73
+ "registry package #{package_name} version #{package_version} not found in #{@root}"
74
+ end
75
+
76
+ package_root_for(package_name, package_version)
77
+ end
78
+
79
+ def available_versions(package_name)
80
+ versions = package_versions_for_root(@root, package_name)
81
+ if upstream_configured?
82
+ upstream_versions = if upstream_http?
83
+ package_versions_for_http(package_name)
84
+ else
85
+ package_versions_for_root(@upstream_root, package_name)
86
+ end
87
+ versions = upstream_versions + versions
88
+ end
89
+
90
+ versions.uniq
91
+ end
92
+
93
+ def publish(path, target: :local)
94
+ manifest = PackageManifest.load(path)
95
+ package_version = manifest.package_version.to_s.strip
96
+ if package_version.empty?
97
+ raise PackageRegistryStoreError,
98
+ "package #{manifest.package_name} at #{manifest.manifest_path} must declare package.version before publishing"
99
+ end
100
+
101
+ target_root = registry_root_for_target(target)
102
+ package_root = package_root_for_root(target_root, manifest.package_name, package_version)
103
+ if File.exist?(package_root)
104
+ raise PackageRegistryStoreError,
105
+ "package #{manifest.package_name} version #{package_version} already published in registry: #{package_root}"
106
+ end
107
+
108
+ materialize_package_copy!(manifest.root_dir, package_root)
109
+ write_registry_artifacts_with_rollback!(target_root, manifest.package_name, package_version, package_root)
110
+
111
+ Result.new(package_name: manifest.package_name, version: package_version, path: package_root)
112
+ rescue PackageManifestError => e
113
+ raise PackageRegistryStoreError, e.message
114
+ rescue SystemCallError => e
115
+ raise PackageRegistryStoreError, "failed to publish package to #{target_root || @root}: #{e.message}"
116
+ end
117
+
118
+ def sync(package_or_identity, version = nil)
119
+ package_name, package_version = extract_package_version(package_or_identity, version)
120
+ raise PackageRegistryStoreError, "registry upstream is not configured" unless upstream_configured?
121
+
122
+ local_root = package_root_for(package_name, package_version)
123
+ return Result.new(package_name:, version: package_version, path: local_root) if published?(package_name, package_version)
124
+
125
+ return sync_from_http(package_name, package_version, local_root) if upstream_http?
126
+
127
+ upstream_package_root = package_root_for_root(@upstream_root, package_name, package_version)
128
+ upstream_manifest_path = File.join(upstream_package_root, "package.toml")
129
+ unless File.file?(upstream_manifest_path)
130
+ raise PackageRegistryStoreError,
131
+ "registry package #{package_name} version #{package_version} not found in upstream registry #{@upstream_root}"
132
+ end
133
+
134
+ upstream_manifest = PackageManifest.load(upstream_package_root)
135
+ if upstream_manifest.package_name != package_name || upstream_manifest.package_version != package_version
136
+ raise PackageRegistryStoreError,
137
+ "upstream registry package at #{upstream_manifest.manifest_path} resolved to #{upstream_manifest.package_name}@#{upstream_manifest.package_version.inspect}; expected #{package_name}@#{package_version}"
138
+ end
139
+
140
+ materialize_package_copy!(upstream_package_root, local_root)
141
+ write_registry_artifacts_with_rollback!(@root, package_name, package_version, local_root)
142
+
143
+ Result.new(package_name:, version: package_version, path: local_root)
144
+ rescue PackageManifestError => e
145
+ raise PackageRegistryStoreError, e.message
146
+ rescue SystemCallError => e
147
+ raise PackageRegistryStoreError, "failed to sync package from upstream registry #{@upstream_root}: #{e.message}"
148
+ end
149
+
150
+ def upstream_configured?
151
+ !@upstream_root.nil?
152
+ end
153
+
154
+ def upstream_http?
155
+ self.class.http_url?(@upstream_root)
156
+ end
157
+
158
+ private
159
+
160
+ def extract_package_version(package_or_identity, version)
161
+ if package_or_identity.respond_to?(:package_name) && package_or_identity.respond_to?(:version)
162
+ [package_or_identity.package_name, package_or_identity.version]
163
+ else
164
+ [package_or_identity, version]
165
+ end
166
+ end
167
+
168
+ def normalize_component(value, label)
169
+ text = value.to_s.strip
170
+ raise PackageRegistryStoreError, "#{label} cannot be empty" if text.empty?
171
+
172
+ separators = [File::SEPARATOR, File::ALT_SEPARATOR].compact
173
+ if separators.any? { |separator| text.include?(separator) } || text == "." || text == ".."
174
+ raise PackageRegistryStoreError, "#{label} contains an unsupported path separator: #{text.inspect}"
175
+ end
176
+
177
+ text
178
+ end
179
+
180
+ def package_root_for_root(root, package_or_identity, version = nil)
181
+ package_name, package_version = extract_package_version(package_or_identity, version)
182
+
183
+ File.join(root, "packages", normalize_component(package_name, "package name"), normalize_component(package_version, "package version"))
184
+ end
185
+
186
+ def archive_path_for_root(root, package_name, package_version)
187
+ package_dir = File.join(root, "packages", normalize_component(package_name, "package name"))
188
+ File.join(package_dir, "#{normalize_component(package_version, "package version")}.tar.gz")
189
+ end
190
+
191
+ def versions_index_path_for_root(root, package_name)
192
+ File.join(root, "packages", normalize_component(package_name, "package name"), "versions.txt")
193
+ end
194
+
195
+ def package_versions_for_root(root, package_name)
196
+ return [] if root.nil?
197
+
198
+ package_dir = File.join(root, "packages", normalize_component(package_name, "package name"))
199
+ return [] unless File.directory?(package_dir)
200
+
201
+ Dir.children(package_dir).sort.each_with_object([]) do |entry, versions|
202
+ next if entry.start_with?(".") || entry == "versions.txt" || entry.end_with?(".tar.gz")
203
+
204
+ manifest_path = File.join(package_dir, entry, "package.toml")
205
+ versions << entry if File.file?(manifest_path)
206
+ end
207
+ rescue SystemCallError => e
208
+ raise PackageRegistryStoreError, "failed to inspect registry package versions under #{package_dir}: #{e.message}"
209
+ end
210
+
211
+ def package_versions_for_http(package_name)
212
+ response = http_get_response(package_versions_url(package_name))
213
+ return [] if response.is_a?(Net::HTTPNotFound)
214
+ raise_http_error!(response, "list registry package versions") unless response.is_a?(Net::HTTPSuccess)
215
+
216
+ response.body.lines.map(&:strip).reject(&:empty?)
217
+ end
218
+
219
+ def sync_from_http(package_name, package_version, local_root)
220
+ response = http_get_response(package_archive_url(package_name, package_version))
221
+ if response.is_a?(Net::HTTPNotFound)
222
+ raise PackageRegistryStoreError,
223
+ "registry package #{package_name} version #{package_version} not found in upstream registry #{@upstream_root}"
224
+ end
225
+ raise_http_error!(response, "download registry package archive") unless response.is_a?(Net::HTTPSuccess)
226
+
227
+ FileUtils.rm_rf(local_root) if File.exist?(local_root)
228
+ FileUtils.mkdir_p(File.dirname(local_root))
229
+
230
+ Dir.mktmpdir("milk-tea-http-registry", File.dirname(local_root)) do |tmpdir|
231
+ extract_archive_to_root(response.body, local_root, tmpdir)
232
+ end
233
+
234
+ validate_synced_package!(local_root, package_name, package_version)
235
+ write_registry_artifacts_with_rollback!(@root, package_name, package_version, local_root)
236
+
237
+ Result.new(package_name:, version: package_version, path: local_root)
238
+ rescue PackageManifestError => e
239
+ raise PackageRegistryStoreError, e.message
240
+ rescue SystemCallError => e
241
+ raise PackageRegistryStoreError, "failed to sync package from upstream registry #{@upstream_root}: #{e.message}"
242
+ end
243
+
244
+ def validate_synced_package!(package_root, package_name, package_version)
245
+ manifest = PackageManifest.load(package_root)
246
+ if manifest.package_name != package_name || manifest.package_version != package_version
247
+ raise PackageRegistryStoreError,
248
+ "upstream registry package at #{manifest.manifest_path} resolved to #{manifest.package_name}@#{manifest.package_version.inspect}; expected #{package_name}@#{package_version}"
249
+ end
250
+ end
251
+
252
+ def write_registry_artifacts!(root, package_name, package_version, package_root)
253
+ write_package_archive!(archive_path_for_root(root, package_name, package_version), package_root)
254
+ write_versions_index!(root, package_name)
255
+ end
256
+
257
+ def write_registry_artifacts_with_rollback!(root, package_name, package_version, package_root)
258
+ write_registry_artifacts!(root, package_name, package_version, package_root)
259
+ rescue StandardError
260
+ cleanup_registry_entry!(root, package_name, package_version, package_root)
261
+ raise
262
+ end
263
+
264
+ def write_package_archive!(archive_path, package_root)
265
+ tar_buffer = StringIO.new(+"")
266
+ Gem::Package::TarWriter.new(tar_buffer) do |tar|
267
+ add_directory_to_archive(tar, package_root, package_root)
268
+ end
269
+ tar_buffer.rewind
270
+
271
+ PackageAtomicWrite.open(archive_path, binmode: true) do |file|
272
+ gzip = Zlib::GzipWriter.new(file)
273
+ gzip.write(tar_buffer.string)
274
+ gzip.finish
275
+ end
276
+ end
277
+
278
+ def add_directory_to_archive(tar, root_path, path)
279
+ Dir.children(path).sort.each do |entry|
280
+ next if entry.start_with?(".")
281
+
282
+ child_path = File.join(path, entry)
283
+ relative_path = child_path.delete_prefix(root_path + File::SEPARATOR)
284
+ stat = File.lstat(child_path)
285
+ if stat.directory?
286
+ tar.mkdir(relative_path, stat.mode)
287
+ add_directory_to_archive(tar, root_path, child_path)
288
+ elsif stat.file?
289
+ tar.add_file(relative_path, stat.mode) do |file|
290
+ File.open(child_path, "rb") do |source|
291
+ IO.copy_stream(source, file)
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
297
+
298
+ def write_versions_index!(root, package_name)
299
+ path = versions_index_path_for_root(root, package_name)
300
+ versions = package_versions_for_root(root, package_name)
301
+ PackageAtomicWrite.write(path, versions.join("\n") + (versions.empty? ? "" : "\n"))
302
+ end
303
+
304
+ def extract_archive_to_root(archive_body, destination_root, scratch_dir)
305
+ archive_path = File.join(scratch_dir, "package.tar.gz")
306
+ extract_root = File.join(File.dirname(archive_path), "extract")
307
+ File.binwrite(archive_path, archive_body)
308
+ FileUtils.mkdir_p(extract_root)
309
+
310
+ Zlib::GzipReader.open(archive_path) do |gzip|
311
+ Gem::Package::TarReader.new(gzip) do |tar|
312
+ tar.each do |entry|
313
+ relative_path = entry.full_name
314
+ next if relative_path.nil? || relative_path.empty?
315
+
316
+ destination_path = File.expand_path(relative_path, extract_root)
317
+ unless destination_path == extract_root || destination_path.start_with?(extract_root + File::SEPARATOR)
318
+ raise PackageRegistryStoreError, "registry package archive contains an invalid path #{relative_path.inspect}"
319
+ end
320
+
321
+ if entry.directory?
322
+ FileUtils.mkdir_p(destination_path)
323
+ elsif entry.file?
324
+ FileUtils.mkdir_p(File.dirname(destination_path))
325
+ File.open(destination_path, "wb", entry.header.mode) do |file|
326
+ IO.copy_stream(entry, file)
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ File.rename(extract_root, destination_root)
334
+ end
335
+
336
+ def materialize_package_copy!(source_root, destination_root)
337
+ staging_root = "#{destination_root}.tmp.#{$$}.#{Thread.current.object_id}"
338
+
339
+ FileUtils.rm_rf(staging_root)
340
+ FileUtils.mkdir_p(File.dirname(destination_root))
341
+ FileUtils.copy_entry(source_root, staging_root)
342
+ File.rename(staging_root, destination_root)
343
+ rescue StandardError
344
+ FileUtils.rm_rf(staging_root)
345
+ raise
346
+ end
347
+
348
+ def cleanup_registry_entry!(root, package_name, package_version, package_root)
349
+ FileUtils.rm_rf(package_root)
350
+ FileUtils.rm_f(archive_path_for_root(root, package_name, package_version))
351
+ end
352
+
353
+ def package_versions_url(package_name)
354
+ join_upstream_url("packages", package_name, "versions.txt")
355
+ end
356
+
357
+ def package_archive_url(package_name, package_version)
358
+ join_upstream_url("packages", package_name, "#{package_version}.tar.gz")
359
+ end
360
+
361
+ def join_upstream_url(*segments)
362
+ encoded_segments = segments.map do |segment|
363
+ CGI.escape(segment.to_s).gsub("+", "%20")
364
+ end
365
+ base = @upstream_root.to_s.sub(%r{/+\z}, "")
366
+ "#{base}/#{encoded_segments.join("/")}"
367
+ end
368
+
369
+ def http_get_response(url, redirects_remaining = 5)
370
+ uri = URI.parse(url)
371
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
372
+ http.request(Net::HTTP::Get.new(uri.request_uri))
373
+ end
374
+
375
+ if response.is_a?(Net::HTTPRedirection)
376
+ raise PackageRegistryStoreError, "too many HTTP redirects while fetching #{url}" if redirects_remaining <= 0
377
+
378
+ location = response["location"]
379
+ raise PackageRegistryStoreError, "missing redirect location while fetching #{url}" if location.to_s.strip.empty?
380
+
381
+ return http_get_response(URI.join(url, location).to_s, redirects_remaining - 1)
382
+ end
383
+
384
+ response
385
+ rescue URI::InvalidURIError => e
386
+ raise PackageRegistryStoreError, "invalid upstream registry URL #{url.inspect}: #{e.message}"
387
+ rescue SocketError, IOError, SystemCallError => e
388
+ raise PackageRegistryStoreError, "failed to fetch #{url}: #{e.message}"
389
+ end
390
+
391
+ def raise_http_error!(response, action)
392
+ raise PackageRegistryStoreError,
393
+ "failed to #{action} from upstream registry #{@upstream_root}: HTTP #{response.code} #{response.message}"
394
+ end
395
+
396
+ def normalize_optional_root(root)
397
+ text = root.to_s.strip
398
+ return nil if text.empty?
399
+
400
+ self.class.http_url?(text) ? text.sub(%r{/+\z}, "") : File.expand_path(text)
401
+ end
402
+
403
+ def registry_root_for_target(target)
404
+ case target
405
+ when :local
406
+ @root
407
+ when :upstream
408
+ raise PackageRegistryStoreError, "registry upstream is not configured" unless upstream_configured?
409
+ if upstream_http?
410
+ raise PackageRegistryStoreError,
411
+ "publishing directly to an HTTP registry upstream is not supported; publish to a filesystem mirror instead"
412
+ end
413
+
414
+ @upstream_root
415
+ else
416
+ raise PackageRegistryStoreError, "unknown registry publish target #{target.inspect}"
417
+ end
418
+ end
419
+ end
420
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class PackageServices
5
+ attr_reader :source_cache, :registry_store
6
+
7
+ def initialize(source_cache: PackageSourceCache.new, registry_store: PackageRegistryStore.new)
8
+ @source_cache = source_cache
9
+ @registry_store = registry_store
10
+ @source_resolvers = {}
11
+ end
12
+
13
+ def source_fetcher
14
+ @source_fetcher ||= PackageSourceFetcher.new(
15
+ source_cache: source_cache,
16
+ source_resolver: source_resolver(:reject),
17
+ registry_store: registry_store,
18
+ )
19
+ end
20
+
21
+ def source_resolver(mode)
22
+ @source_resolvers[mode] ||= case mode
23
+ when :reject
24
+ PackageSourceResolver.new(source_cache: source_cache)
25
+ when :cache
26
+ PackageSourceResolver.new(
27
+ source_cache: source_cache,
28
+ remote_resolution: :cache,
29
+ )
30
+ when :materialize
31
+ PackageSourceResolver.new(
32
+ source_cache: source_cache,
33
+ remote_resolution: :materialize,
34
+ source_fetcher: source_fetcher,
35
+ )
36
+ else
37
+ raise ArgumentError, "unknown dependency source resolution mode #{mode.inspect}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class PackageSourceCacheError < StandardError; end
5
+
6
+ class PackageSourceCache
7
+ def self.default_root(env: ENV, home: nil)
8
+ xdg_cache_home = env.fetch("XDG_CACHE_HOME", "").to_s.strip
9
+ base_root = if xdg_cache_home.empty?
10
+ File.join(home || Dir.home, ".cache")
11
+ else
12
+ File.expand_path(xdg_cache_home)
13
+ end
14
+
15
+ File.join(base_root, "milk_tea", "package_sources")
16
+ end
17
+
18
+ attr_reader :root
19
+
20
+ def initialize(root: self.class.default_root)
21
+ @root = File.expand_path(root)
22
+ end
23
+
24
+ def path_for(source_or_identity)
25
+ identity = extract_identity(source_or_identity)
26
+ cache_key = identity.cache_key
27
+ unless cache_key
28
+ raise PackageSourceCacheError, "package source kind #{identity.kind.inspect} does not use the shared source cache"
29
+ end
30
+
31
+ File.join(@root, *cache_key.split("/"))
32
+ end
33
+
34
+ def materialized_root_for(source_or_identity)
35
+ identity = extract_identity(source_or_identity)
36
+ base_path = path_for(identity)
37
+
38
+ if identity.respond_to?(:subdir) && identity.subdir
39
+ segments = identity.subdir.split(/[\\\/]+/).reject(&:empty?)
40
+ materialized_path = File.expand_path(File.join(base_path, *segments))
41
+ expanded_base_path = File.expand_path(base_path)
42
+ unless materialized_path == expanded_base_path || materialized_path.start_with?(expanded_base_path + File::SEPARATOR)
43
+ raise PackageSourceCacheError,
44
+ "git source subdir escapes the materialized package root: #{identity.subdir.inspect}"
45
+ end
46
+
47
+ return materialized_path
48
+ end
49
+
50
+ base_path
51
+ end
52
+
53
+ def manifest_path_for(source_or_identity)
54
+ File.join(materialized_root_for(source_or_identity), "package.toml")
55
+ end
56
+
57
+ def materialized?(source_or_identity)
58
+ File.file?(manifest_path_for(source_or_identity))
59
+ end
60
+
61
+ private
62
+
63
+ def extract_identity(source_or_identity)
64
+ if source_or_identity.respond_to?(:identity)
65
+ source_or_identity.identity
66
+ else
67
+ source_or_identity
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "open3"
5
+
6
+ module MilkTea
7
+ class PackageSourceFetcherError < StandardError; end
8
+
9
+ class PackageSourceFetcher
10
+ Result = Data.define(:package_name, :identity, :status, :path)
11
+
12
+ def initialize(source_cache: PackageSourceCache.new, source_resolver: PackageSourceResolver.new(source_cache: source_cache), registry_store: PackageRegistryStore.new)
13
+ @source_cache = source_cache
14
+ @source_resolver = source_resolver
15
+ @registry_store = registry_store
16
+ end
17
+
18
+ def fetch_locked_sources(path)
19
+ locked_packages = PackageLock.locked_packages(path, source_resolver: @source_resolver)
20
+ locked_packages.select { |package| package.identity.cacheable? }.map do |package|
21
+ fetch_locked_package(package)
22
+ end
23
+ rescue PackageLockError => e
24
+ raise PackageSourceFetcherError, e.message
25
+ end
26
+
27
+ def materialize_identity(package_name:, identity:)
28
+ expected_manifest_path = @source_cache.manifest_path_for(identity)
29
+
30
+ case identity
31
+ when PackageSourceResolver::RegistryIdentity
32
+ fetch_registry_package(package_name, identity, expected_manifest_path)
33
+ when PackageSourceResolver::GitIdentity
34
+ fetch_git_package(package_name, identity, expected_manifest_path)
35
+ else
36
+ raise PackageSourceFetcherError,
37
+ "package source kind #{identity.kind.inspect} is not fetchable"
38
+ end
39
+ rescue PackageSourceCacheError => e
40
+ raise PackageSourceFetcherError, e.message
41
+ rescue PackageRegistryStoreError => e
42
+ raise PackageSourceFetcherError, e.message
43
+ end
44
+
45
+ private
46
+
47
+ def fetch_locked_package(package)
48
+ identity = package.identity
49
+ expected_manifest_path = @source_cache.manifest_path_for(identity)
50
+ if package.manifest_path != expected_manifest_path
51
+ raise PackageSourceFetcherError,
52
+ "package.lock manifest_path for #{package.package_name} does not match the expected cache path #{expected_manifest_path}"
53
+ end
54
+
55
+ materialize_identity(package_name: package.package_name, identity:)
56
+ end
57
+
58
+ def fetch_git_package(package_name, identity, expected_manifest_path)
59
+ checkout_root = @source_cache.path_for(identity)
60
+ materialized_root = @source_cache.materialized_root_for(identity)
61
+
62
+ if git_checkout_current?(checkout_root, identity.revision) && File.file?(expected_manifest_path)
63
+ return Result.new(package_name:, identity:, status: :present, path: materialized_root)
64
+ end
65
+
66
+ FileUtils.rm_rf(checkout_root) if File.exist?(checkout_root)
67
+ FileUtils.mkdir_p(File.dirname(checkout_root))
68
+
69
+ run_git!("clone", identity.url, checkout_root)
70
+ run_git!("-C", checkout_root, "checkout", "--detach", identity.revision)
71
+
72
+ unless File.file?(expected_manifest_path)
73
+ raise PackageSourceFetcherError,
74
+ "git source for #{package_name} is missing package.toml at #{expected_manifest_path}"
75
+ end
76
+
77
+ Result.new(package_name:, identity:, status: :materialized, path: materialized_root)
78
+ end
79
+
80
+ def fetch_registry_package(package_name, identity, expected_manifest_path)
81
+ cache_root = @source_cache.path_for(identity)
82
+ materialized_root = @source_cache.materialized_root_for(identity)
83
+
84
+ if File.file?(expected_manifest_path)
85
+ return Result.new(package_name:, identity:, status: :present, path: materialized_root)
86
+ end
87
+
88
+ published_root = @registry_store.published_root_for(identity, sync: true)
89
+
90
+ FileUtils.rm_rf(cache_root) if File.exist?(cache_root)
91
+ FileUtils.mkdir_p(File.dirname(cache_root))
92
+ FileUtils.copy_entry(published_root, cache_root)
93
+
94
+ unless File.file?(expected_manifest_path)
95
+ raise PackageSourceFetcherError,
96
+ "registry source for #{package_name} is missing package.toml at #{expected_manifest_path}"
97
+ end
98
+
99
+ Result.new(package_name:, identity:, status: :materialized, path: materialized_root)
100
+ end
101
+
102
+ def git_checkout_current?(checkout_root, revision)
103
+ return false unless File.directory?(checkout_root)
104
+
105
+ stdout, _stderr, status = Open3.capture3("git", "-C", checkout_root, "rev-parse", "HEAD")
106
+ return false unless status.success?
107
+
108
+ stdout.strip == revision
109
+ rescue Errno::ENOENT => e
110
+ raise PackageSourceFetcherError, "git not found while materializing package sources: #{e.message}"
111
+ end
112
+
113
+ def run_git!(*args)
114
+ stdout, stderr, status = Open3.capture3("git", *args)
115
+ return if status.success?
116
+
117
+ details = [stdout, stderr].reject(&:empty?).join
118
+ raise PackageSourceFetcherError,
119
+ details.empty? ? "failed to materialize git package source" : "failed to materialize git package source:\n#{details}"
120
+ rescue Errno::ENOENT => e
121
+ raise PackageSourceFetcherError, "git not found while materializing package sources: #{e.message}"
122
+ end
123
+ end
124
+ end