zuzu-js 0.1.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 (167) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +113 -0
  3. package/bin/zuzu +17 -0
  4. package/bin/zuzu-build-browser-bundle +57 -0
  5. package/bin/zuzu-generate-browser-stdlib +584 -0
  6. package/bin/zuzu-js +23 -0
  7. package/bin/zuzu-js-compile +152 -0
  8. package/bin/zuzu-js-electron +19 -0
  9. package/dist/zuzu-browser-worker.js +45574 -0
  10. package/dist/zuzu-browser.js +45362 -0
  11. package/lib/browser-bundle-entry.js +160 -0
  12. package/lib/browser-gui-renderer.js +387 -0
  13. package/lib/browser-runtime.js +167 -0
  14. package/lib/browser-worker-entry.js +413 -0
  15. package/lib/browser-ztests/runner.html +103 -0
  16. package/lib/browser-ztests/runner.js +369 -0
  17. package/lib/cli.js +350 -0
  18. package/lib/collections.js +367 -0
  19. package/lib/compiler.js +303 -0
  20. package/lib/electron/launcher.js +70 -0
  21. package/lib/electron/main.js +956 -0
  22. package/lib/electron/preload.js +80 -0
  23. package/lib/electron/renderer.html +122 -0
  24. package/lib/electron/renderer.js +24 -0
  25. package/lib/execution-metadata.js +18 -0
  26. package/lib/gui/dom-renderer.js +778 -0
  27. package/lib/host/browser-host.js +278 -0
  28. package/lib/host/capabilities.js +47 -0
  29. package/lib/host/electron-host.js +15 -0
  30. package/lib/host/node-host.js +74 -0
  31. package/lib/paths.js +150 -0
  32. package/lib/runtime-entrypoints.js +60 -0
  33. package/lib/runtime-helpers.js +886 -0
  34. package/lib/runtime.js +3529 -0
  35. package/lib/tap.js +37 -0
  36. package/lib/transpiler-new/ast.js +23 -0
  37. package/lib/transpiler-new/codegen.js +2455 -0
  38. package/lib/transpiler-new/errors.js +28 -0
  39. package/lib/transpiler-new/index.js +26 -0
  40. package/lib/transpiler-new/lexer.js +834 -0
  41. package/lib/transpiler-new/parser.js +2332 -0
  42. package/lib/transpiler-new/validate-bindings.js +326 -0
  43. package/lib/transpiler-utils.js +95 -0
  44. package/lib/transpiler.js +33 -0
  45. package/lib/zuzu.js +53 -0
  46. package/modules/javascript.js +193 -0
  47. package/modules/std/archive.js +603 -0
  48. package/modules/std/clib.js +338 -0
  49. package/modules/std/data/csv.js +1331 -0
  50. package/modules/std/data/json.js +531 -0
  51. package/modules/std/data/xml.js +441 -0
  52. package/modules/std/data/yaml.js +256 -0
  53. package/modules/std/db-worker.js +250 -0
  54. package/modules/std/db.js +664 -0
  55. package/modules/std/digest/_hash.js +443 -0
  56. package/modules/std/digest/md5.js +26 -0
  57. package/modules/std/digest/sha.js +72 -0
  58. package/modules/std/eval.js +10 -0
  59. package/modules/std/gui/objects.js +1519 -0
  60. package/modules/std/internals.js +571 -0
  61. package/modules/std/io/socks-worker.js +318 -0
  62. package/modules/std/io/socks.js +186 -0
  63. package/modules/std/io.js +475 -0
  64. package/modules/std/marshal/cbor.js +463 -0
  65. package/modules/std/marshal/graph.js +1624 -0
  66. package/modules/std/marshal.js +87 -0
  67. package/modules/std/math/bignum.js +91 -0
  68. package/modules/std/math.js +79 -0
  69. package/modules/std/net/dns.js +306 -0
  70. package/modules/std/net/http.js +820 -0
  71. package/modules/std/net/smtp.js +943 -0
  72. package/modules/std/net/url.js +109 -0
  73. package/modules/std/proc.js +602 -0
  74. package/modules/std/secure.js +3724 -0
  75. package/modules/std/string/base64.js +138 -0
  76. package/modules/std/string.js +299 -0
  77. package/modules/std/task.js +914 -0
  78. package/modules/std/time.js +579 -0
  79. package/modules/std/tui.js +188 -0
  80. package/modules/std/worker-thread.js +246 -0
  81. package/modules/std/worker.js +790 -0
  82. package/package.json +67 -0
  83. package/stdlib/modules/javascript.zzm +99 -0
  84. package/stdlib/modules/perl.zzm +105 -0
  85. package/stdlib/modules/std/archive.zzm +132 -0
  86. package/stdlib/modules/std/cache/lru.zzm +174 -0
  87. package/stdlib/modules/std/clib.zzm +112 -0
  88. package/stdlib/modules/std/colour.zzm +220 -0
  89. package/stdlib/modules/std/config.zzm +818 -0
  90. package/stdlib/modules/std/data/cbor.zzm +497 -0
  91. package/stdlib/modules/std/data/csv.zzm +285 -0
  92. package/stdlib/modules/std/data/ini.zzm +472 -0
  93. package/stdlib/modules/std/data/json/schema/core.zzm +573 -0
  94. package/stdlib/modules/std/data/json/schema/format.zzm +581 -0
  95. package/stdlib/modules/std/data/json/schema/model.zzm +255 -0
  96. package/stdlib/modules/std/data/json/schema/output.zzm +272 -0
  97. package/stdlib/modules/std/data/json/schema/relative_pointer.zzm +299 -0
  98. package/stdlib/modules/std/data/json/schema/validation.zzm +1503 -0
  99. package/stdlib/modules/std/data/json/schema.zzm +306 -0
  100. package/stdlib/modules/std/data/json.zzm +102 -0
  101. package/stdlib/modules/std/data/kdl/json.zzm +460 -0
  102. package/stdlib/modules/std/data/kdl/xml.zzm +387 -0
  103. package/stdlib/modules/std/data/kdl.zzm +1631 -0
  104. package/stdlib/modules/std/data/toml.zzm +756 -0
  105. package/stdlib/modules/std/data/toon.zzm +1017 -0
  106. package/stdlib/modules/std/data/xml/escape.zzm +156 -0
  107. package/stdlib/modules/std/data/xml.zzm +276 -0
  108. package/stdlib/modules/std/data/yaml.zzm +94 -0
  109. package/stdlib/modules/std/db.zzm +173 -0
  110. package/stdlib/modules/std/defer.zzm +75 -0
  111. package/stdlib/modules/std/digest/crc32.zzm +196 -0
  112. package/stdlib/modules/std/digest/md5.zzm +54 -0
  113. package/stdlib/modules/std/digest/sha.zzm +83 -0
  114. package/stdlib/modules/std/dump.zzm +317 -0
  115. package/stdlib/modules/std/eval.zzm +63 -0
  116. package/stdlib/modules/std/getopt.zzm +432 -0
  117. package/stdlib/modules/std/gui/dialogue.zzm +592 -0
  118. package/stdlib/modules/std/gui/objects.zzm +123 -0
  119. package/stdlib/modules/std/gui.zzm +1914 -0
  120. package/stdlib/modules/std/internals.zzm +139 -0
  121. package/stdlib/modules/std/io/socks.zzm +139 -0
  122. package/stdlib/modules/std/io.zzm +157 -0
  123. package/stdlib/modules/std/lingua/en.zzm +347 -0
  124. package/stdlib/modules/std/log.zzm +169 -0
  125. package/stdlib/modules/std/mail.zzm +2726 -0
  126. package/stdlib/modules/std/marshal.zzm +138 -0
  127. package/stdlib/modules/std/math/bignum.zzm +98 -0
  128. package/stdlib/modules/std/math/range.zzm +116 -0
  129. package/stdlib/modules/std/math/roman.zzm +156 -0
  130. package/stdlib/modules/std/math.zzm +141 -0
  131. package/stdlib/modules/std/net/dns.zzm +93 -0
  132. package/stdlib/modules/std/net/http.zzm +278 -0
  133. package/stdlib/modules/std/net/smtp.zzm +257 -0
  134. package/stdlib/modules/std/net/url.zzm +69 -0
  135. package/stdlib/modules/std/path/jsonpointer.zzm +526 -0
  136. package/stdlib/modules/std/path/kdl.zzm +1003 -0
  137. package/stdlib/modules/std/path/simple.zzm +520 -0
  138. package/stdlib/modules/std/path/z/context.zzm +147 -0
  139. package/stdlib/modules/std/path/z/evaluate.zzm +549 -0
  140. package/stdlib/modules/std/path/z/functions.zzm +874 -0
  141. package/stdlib/modules/std/path/z/lexer.zzm +490 -0
  142. package/stdlib/modules/std/path/z/node.zzm +1455 -0
  143. package/stdlib/modules/std/path/z/operators.zzm +445 -0
  144. package/stdlib/modules/std/path/z/parser.zzm +359 -0
  145. package/stdlib/modules/std/path/z.zzm +403 -0
  146. package/stdlib/modules/std/path/zz/functions.zzm +828 -0
  147. package/stdlib/modules/std/path/zz/operators.zzm +1036 -0
  148. package/stdlib/modules/std/path/zz.zzm +100 -0
  149. package/stdlib/modules/std/proc.zzm +155 -0
  150. package/stdlib/modules/std/result.zzm +149 -0
  151. package/stdlib/modules/std/secure.zzm +606 -0
  152. package/stdlib/modules/std/string/base64.zzm +66 -0
  153. package/stdlib/modules/std/string/quoted_printable.zzm +485 -0
  154. package/stdlib/modules/std/string.zzm +179 -0
  155. package/stdlib/modules/std/task.zzm +221 -0
  156. package/stdlib/modules/std/template/z.zzm +531 -0
  157. package/stdlib/modules/std/template/zz.zzm +62 -0
  158. package/stdlib/modules/std/time.zzm +188 -0
  159. package/stdlib/modules/std/tui.zzm +89 -0
  160. package/stdlib/modules/std/uuid.zzm +223 -0
  161. package/stdlib/modules/std/web/session.zzm +388 -0
  162. package/stdlib/modules/std/web/static.zzm +329 -0
  163. package/stdlib/modules/std/web.zzm +1942 -0
  164. package/stdlib/modules/std/worker.zzm +202 -0
  165. package/stdlib/modules/std/zuzuzoo.zzm +3960 -0
  166. package/stdlib/modules/test/more.zzm +528 -0
  167. package/stdlib/modules/test/parser.zzm +209 -0
@@ -0,0 +1,3960 @@
1
+ =encoding utf8
2
+
3
+ =head1 NAME
4
+
5
+ std/zuzuzoo - Plan and install Zuzu distributions.
6
+
7
+ =head1 IMPLEMENTATION SUPPORT
8
+
9
+ This module is supported by zuzu.pl, zuzu-rust, and zuzu-js on Node and
10
+ Electron. It is not supported by zuzu-js in the browser.
11
+
12
+ =head1 DESCRIPTION
13
+
14
+ C<std/zuzuzoo> provides the package-management engine used by the
15
+ command-line C<zuzuzoo> tool. It supports installed distribution
16
+ metadata queries, source archive inspection, dependency-aware install
17
+ planning, distribution test execution, file installation, planned
18
+ target-root removal execution, installed metadata writing, standalone
19
+ safe removal planning and execution, installed-file verification,
20
+ latest-version checks, upgrade checks, and canonical pretty JSON
21
+ formatting.
22
+
23
+ The command-line C<zuzuzoo> wrapper delegates package behaviour to this
24
+ module and handles argument parsing, user prompts, output formatting,
25
+ JSON output selection, and exit-code translation.
26
+
27
+ =head1 EXPORTS
28
+
29
+ =head2 Functions
30
+
31
+ =over
32
+
33
+ =item C<< compare_versions(left, right) >>
34
+
35
+ Parameters: C<left> and C<right> are version strings. Returns:
36
+ C<Number>. Compares two versions, returning a negative, zero, or
37
+ positive value.
38
+
39
+ =item C<< list_installed(options?) >>, C<< query(module_name, options?) >>, C<< query_distribution(distribution_name, options?) >>
40
+
41
+ Parameters: names identify installed modules or distributions and
42
+ C<options> controls roots and output. Returns: value. Reads installed
43
+ distribution metadata.
44
+
45
+ =item C<< is_installed(module_name, min_version?, options?) >>, C<< installed_version(module_name, options?) >>
46
+
47
+ Parameters: C<module_name> identifies a module, C<min_version> is
48
+ optional, and C<options> controls roots. Returns: C<Boolean> or
49
+ C<String>/C<null>. Checks installed module state.
50
+
51
+ =item C<< pretty_json(value, options?) >>, C<< format_json(value, options?) >>
52
+
53
+ Parameters: C<value> is JSON-encodable data and C<options> controls
54
+ formatting. Returns: C<String>. Formats canonical JSON output.
55
+
56
+ =item C<< fetch_source(target, options?) >>, C<< load_distribution(target, options?) >>
57
+
58
+ Parameters: C<target> identifies a source archive or distribution and
59
+ C<options> controls fetch/load behaviour. Returns: value. Fetches or
60
+ loads distribution metadata.
61
+
62
+ =item C<< dependency_roots(options?) >>, C<< find_dependency(module_name, min_version?, options?) >>
63
+
64
+ Parameters: C<options> controls search roots and C<module_name> names a
65
+ dependency. Returns: value. Locates dependency sources.
66
+
67
+ =item C<< plan_install(targets, options?) >>, C<< plan_remove(targets, options?) >>
68
+
69
+ Parameters: C<targets> is an array of requested modules or
70
+ distributions. Returns: C<Dict>. Builds an install or removal plan.
71
+
72
+ =item C<< verify(targets, options?) >>, C<< latest(module_name, options?) >>, C<< can_upgrade(module_name, options?) >>
73
+
74
+ Parameters: C<targets> or C<module_name> identify installed or remote
75
+ items. Returns: value. Verifies installation state or checks available
76
+ versions.
77
+
78
+ =item C<< install(targets, options?) >>, C<< remove(targets, options?) >>
79
+
80
+ Parameters: C<targets> is an array of modules or distributions. Returns:
81
+ C<Dict>. Executes installation or removal.
82
+
83
+ =item C<< run_distribution_tests(install_action, options?) >>, C<< execute_removal(removal_action, options?) >>
84
+
85
+ Parameters: action dictionaries come from plans and C<options> controls
86
+ execution. Returns: C<Dict>. Runs tests or executes one removal action.
87
+
88
+ =item C<< format_install_plan(plan, options?) >>, C<< format_remove_plan(plan, options?) >>
89
+
90
+ Parameters: C<plan> is a plan dictionary. Returns: C<String>. Formats a
91
+ plan for display.
92
+
93
+ =back
94
+
95
+ =head2 Classes
96
+
97
+ =over
98
+
99
+ =item C<ZuzuzooLock>
100
+
101
+ Filesystem lock object.
102
+
103
+ =over
104
+
105
+ =item C<< lock.release() >>
106
+
107
+ Parameters: none. Returns: C<null>. Releases the lock.
108
+
109
+ =back
110
+
111
+ =item C<Zuzuzoo>
112
+
113
+ Stateful package-management helper. Its methods correspond to the
114
+ module-level functions and take the same parameters, without the final
115
+ C<options> argument where object configuration already supplies defaults.
116
+
117
+ =over
118
+
119
+ =item C<< zoo.config() >>
120
+
121
+ Parameters: none. Returns: C<Dict>. Returns the effective configuration.
122
+
123
+ =item C<< zoo.acquire_lock(operation, options?) >>
124
+
125
+ Parameters: C<operation> names the operation and C<options> controls
126
+ locking. Returns: C<ZuzuzooLock>. Acquires an operation lock.
127
+
128
+ =item C<< zoo.find_installed_module(module_name, options?) >>
129
+
130
+ Parameters: C<module_name> identifies a module and C<options> controls
131
+ roots. Returns: C<Dict> or C<null>. Finds installed metadata for a
132
+ module.
133
+
134
+ =item C<< zoo.find_installed_distribution(distribution_name, options?) >>
135
+
136
+ Parameters: C<distribution_name> identifies a distribution and
137
+ C<options> controls roots. Returns: C<Dict> or C<null>. Finds installed
138
+ metadata for a distribution.
139
+
140
+ =back
141
+
142
+ =back
143
+
144
+ =head1 COPYRIGHT AND LICENCE
145
+
146
+ B<< std/zuzuzoo >> is copyright Toby Inkster.
147
+
148
+ It is free software; you may redistribute it and/or modify it under
149
+ the terms of either the Artistic License 1.0 or the GNU General Public
150
+ License version 2.
151
+
152
+ =cut
153
+
154
+ from std/data/json import JSON;
155
+ from std/archive import Archive;
156
+ from std/digest/sha import sha256_hex;
157
+ from std/io import Path, STDERR, STDOUT;
158
+ from std/net/http import UserAgent;
159
+ from std/proc import Env, Proc, sleep;
160
+ from std/string import contains, ends_with, join, split, starts_with, substr;
161
+ from std/time import Time;
162
+ from test/parser import parse as parse_tap;
163
+
164
+
165
+ function _opt ( options, key, fallback := null ) {
166
+ if ( options instanceof Dict and options.exists(key) ) {
167
+ return options.get(key);
168
+ }
169
+ return fallback;
170
+ }
171
+
172
+ function _progress ( options, message ) {
173
+ if ( _opt( options, "progress", false ) ) {
174
+ STDERR.say( "zuzuzoo: " _ message );
175
+ }
176
+ return true;
177
+ }
178
+
179
+ function _response_success ( response ) {
180
+ return response.success() if response can "success";
181
+ return true;
182
+ }
183
+
184
+ function _response_status_text ( response ) {
185
+ let status := ( response can "status" ) ? response.status() : "?";
186
+ let reason := ( response can "reason" ) ? response.reason() : "";
187
+ return "" _ status _ " " _ reason;
188
+ }
189
+
190
+ function _archive_url_from_latest ( latest_info ) {
191
+ let metadata := latest_info{remote_metadata};
192
+ if (
193
+ ( metadata instanceof Dict or metadata instanceof PairList ) and
194
+ metadata.exists("archive_url")
195
+ ) {
196
+ return metadata{archive_url};
197
+ }
198
+
199
+ let remote_url := latest_info{remote_url};
200
+ if ( ends_with( remote_url, ".json" ) ) {
201
+ return substr( remote_url, 0, length remote_url - 5 ) _ ".tar.gz";
202
+ }
203
+
204
+ die(
205
+ "Latest metadata URL does not identify a source archive " _
206
+ "(remote_url=" _ remote_url _ ")"
207
+ );
208
+ }
209
+
210
+ function _push_unique_string ( items, seen, value ) {
211
+ let key := "" _ value;
212
+ return false if key eq "";
213
+ return false if seen.exists(key);
214
+ seen.add( key, true );
215
+ items.push(key);
216
+ return true;
217
+ }
218
+
219
+ function _copy_options_with ( options, key, value ) {
220
+ let out := {};
221
+ if ( options instanceof Dict ) {
222
+ for ( let existing_key in options.keys() ) {
223
+ out.set( existing_key, options.get(existing_key) );
224
+ }
225
+ }
226
+ out.set( key, value );
227
+ return out;
228
+ }
229
+
230
+ function _is_windows_platform () {
231
+ if ( "platform" in __system__ ) {
232
+ let platform := lc( "" _ __system__.get("platform") );
233
+ return platform eq "windows" or platform eq "mswin32";
234
+ }
235
+ return false;
236
+ }
237
+
238
+ function _join_dir ( String base, String child, Boolean windows ) {
239
+ let sep := windows ? "\\": "/";
240
+ return base _ sep _ child;
241
+ }
242
+
243
+ function _require_text ( obj, String key, String where ) {
244
+ if ( not( obj instanceof Dict ) or not obj.exists(key) ) {
245
+ die `Invalid installed metadata ${where}: missing ${key}`;
246
+ }
247
+ let value := obj.get(key);
248
+ if ( not( value instanceof String ) or value eq "" ) {
249
+ die `Invalid metadata ${where}: ${key} must be a string`;
250
+ }
251
+ return value;
252
+ }
253
+
254
+ function _require_object ( obj, String key, String where ) {
255
+ if ( not( obj instanceof Dict ) or not obj.exists(key) ) {
256
+ die `Invalid installed metadata ${where}: missing ${key}`;
257
+ }
258
+ let value := obj.get(key);
259
+ if ( not( value instanceof Dict ) ) {
260
+ die `Invalid metadata ${where}: ${key} must be an object`;
261
+ }
262
+ return value;
263
+ }
264
+
265
+ function _require_array ( obj, String key, String where ) {
266
+ if ( not( obj instanceof Dict ) or not obj.exists(key) ) {
267
+ die `Invalid installed metadata ${where}: missing ${key}`;
268
+ }
269
+ let value := obj.get(key);
270
+ if ( not( value instanceof Array ) ) {
271
+ die `Invalid metadata ${where}: ${key} must be an array`;
272
+ }
273
+ return value;
274
+ }
275
+
276
+ function _source_require_text ( obj, String key, String where ) {
277
+ if ( not( obj instanceof Dict ) and not( obj instanceof PairList ) ) {
278
+ die `Invalid source metadata ${where}: root must be an object`;
279
+ }
280
+ if ( not obj.exists(key) ) {
281
+ die `Invalid source metadata ${where}: missing ${key}`;
282
+ }
283
+ let value := obj.get(key);
284
+ if ( not( value instanceof String ) or value eq "" ) {
285
+ die `Invalid source metadata ${where}: ${key} must be a string`;
286
+ }
287
+ return value;
288
+ }
289
+
290
+ function _validate_dependency_pair ( key, value, String where ) {
291
+ if ( not( key instanceof String ) or key eq "" ) {
292
+ die `Invalid source metadata ${where}: bad dependency name`;
293
+ }
294
+ if ( not( value instanceof String ) or value eq "" ) {
295
+ die `Invalid source metadata ${where}: bad dependency version`;
296
+ }
297
+ return true;
298
+ }
299
+
300
+ function _validate_dependencies ( meta, String where ) {
301
+ if ( not meta.exists("dependencies") ) {
302
+ return true;
303
+ }
304
+ let deps := meta.get("dependencies");
305
+ if ( not( deps instanceof Dict ) ) {
306
+ die `Invalid metadata ${where}: dependencies must be an object`;
307
+ }
308
+ for ( let key in deps.keys() ) {
309
+ if ( not( key instanceof String ) or key eq "" ) {
310
+ die `Invalid metadata ${where}: bad dependency name`;
311
+ }
312
+ let value := deps.get(key);
313
+ if ( not( value instanceof String ) or value eq "" ) {
314
+ die `Invalid metadata ${where}: bad dependency version`;
315
+ }
316
+ }
317
+ return true;
318
+ }
319
+
320
+ function _validate_source_dependencies ( meta, String where ) {
321
+ if ( not meta.exists("dependencies") ) {
322
+ return true;
323
+ }
324
+ let deps := meta.get("dependencies");
325
+ if ( deps instanceof Dict ) {
326
+ for ( let key in deps.keys() ) {
327
+ _validate_dependency_pair( key, deps.get(key), where );
328
+ }
329
+ return true;
330
+ }
331
+ if ( deps instanceof PairList ) {
332
+ let keys := deps.keys();
333
+ let values := deps.values();
334
+ let i := 0;
335
+ while ( i < keys.length() ) {
336
+ _validate_dependency_pair( keys[i], values[i], where );
337
+ i++;
338
+ }
339
+ return true;
340
+ }
341
+ die `Invalid source metadata ${where}: dependencies must be an object`;
342
+ }
343
+
344
+ function _validate_source_metadata ( meta, String metadata_file ) {
345
+ if ( not( meta instanceof Dict ) and not( meta instanceof PairList ) ) {
346
+ die `Invalid source metadata ${metadata_file}: root must be an object`;
347
+ }
348
+ if ( meta.exists("installed") ) {
349
+ die `Invalid source metadata ${metadata_file}: installed is not allowed`;
350
+ }
351
+ if ( meta.exists("modules") or meta.exists("scripts") ) {
352
+ die(
353
+ `Invalid source metadata ${metadata_file}: top-level ` _
354
+ "modules/scripts are not allowed"
355
+ );
356
+ }
357
+
358
+ _source_require_text( meta, "name", metadata_file );
359
+ _source_require_text( meta, "version", metadata_file );
360
+ _source_require_text( meta, "author", metadata_file );
361
+ _source_require_text( meta, "license", metadata_file );
362
+
363
+ if ( meta.exists("status") ) {
364
+ let status := _source_require_text( meta, "status", metadata_file );
365
+ if ( status ne "stable" and status ne "trial" ) {
366
+ die `Invalid source metadata ${metadata_file}: bad status ${status}`;
367
+ }
368
+ }
369
+ _validate_source_dependencies( meta, metadata_file );
370
+
371
+ if ( meta instanceof Dict ) {
372
+ meta.set( "metadata_file", metadata_file );
373
+ }
374
+ else {
375
+ meta.add( "metadata_file", metadata_file );
376
+ }
377
+ return meta;
378
+ }
379
+
380
+ function _validate_entry ( entry, String kind, String where ) {
381
+ if ( not( entry instanceof Dict ) ) {
382
+ die `Invalid metadata ${where}: bad ${kind} entry`;
383
+ }
384
+ _require_text( entry, "source", where );
385
+ _require_text( entry, "install_as", where );
386
+ _require_text( entry, "sha256", where );
387
+ if ( kind eq "script" and entry.exists("wrappers") ) {
388
+ let wrappers := entry.get("wrappers");
389
+ if ( not( wrappers instanceof Array ) ) {
390
+ die `Invalid metadata ${where}: bad script wrappers`;
391
+ }
392
+ for ( let wrapper in wrappers ) {
393
+ if ( not( wrapper instanceof String ) or wrapper eq "" ) {
394
+ die `Invalid metadata ${where}: bad wrapper`;
395
+ }
396
+ }
397
+ }
398
+ return true;
399
+ }
400
+
401
+ function _validate_installed_metadata ( meta, String metadata_file ) {
402
+ if ( not( meta instanceof Dict ) ) {
403
+ die `Invalid metadata ${metadata_file}: root must be an object`;
404
+ }
405
+ if ( meta.exists("modules") or meta.exists("scripts") ) {
406
+ die(
407
+ `Invalid metadata ${metadata_file}: top-level ` _
408
+ "modules/scripts are not allowed"
409
+ );
410
+ }
411
+
412
+ _require_text( meta, "name", metadata_file );
413
+ _require_text( meta, "version", metadata_file );
414
+ _require_text( meta, "author", metadata_file );
415
+ _require_text( meta, "license", metadata_file );
416
+ let status := _require_text( meta, "status", metadata_file );
417
+ if ( status ne "stable" and status ne "trial" ) {
418
+ die `Invalid metadata ${metadata_file}: bad status ${status}`;
419
+ }
420
+ _validate_dependencies( meta, metadata_file );
421
+
422
+ let installed := _require_object( meta, "installed", metadata_file );
423
+ let zdf := _require_text( installed, "zdf", metadata_file );
424
+ if ( zdf ne "ZDF-1" ) {
425
+ die `Invalid metadata ${metadata_file}: bad installed.zdf`;
426
+ }
427
+ _require_text( installed, "lib_dir", metadata_file );
428
+ _require_text( installed, "bin_dir", metadata_file );
429
+ _require_text( installed, "meta_dir", metadata_file );
430
+
431
+ let modules := _require_array( installed, "modules", metadata_file );
432
+ let scripts := _require_array( installed, "scripts", metadata_file );
433
+ if ( modules.length() + scripts.length() = 0 ) {
434
+ die `Invalid metadata ${metadata_file}: no installed files`;
435
+ }
436
+
437
+ for ( let module in modules ) {
438
+ _validate_entry( module, "module", metadata_file );
439
+ }
440
+ for ( let script in scripts ) {
441
+ _validate_entry( script, "script", metadata_file );
442
+ }
443
+
444
+ meta.set( "metadata_file", metadata_file );
445
+ return meta;
446
+ }
447
+
448
+ function _parse_version ( version ) {
449
+ let text := "" _ version;
450
+ let nums := [];
451
+ let i := 0;
452
+ let n := length text;
453
+
454
+ while ( i < n and substr( text, i, 1 ) ~ /^[0-9]$/ ) {
455
+ let start := i;
456
+ while ( i < n and substr( text, i, 1 ) ~ /^[0-9]$/ ) {
457
+ i++;
458
+ }
459
+ nums.push( int( substr( text, start, i - start ) ) );
460
+
461
+ if (
462
+ i < n - 1 and
463
+ substr( text, i, 1 ) eq "." and
464
+ substr( text, i + 1, 1 ) ~ /^[0-9]$/
465
+ ) {
466
+ i++;
467
+ next;
468
+ }
469
+ last;
470
+ }
471
+
472
+ return {
473
+ nums: nums,
474
+ suffix: substr( text, i ),
475
+ };
476
+ }
477
+
478
+ function compare_versions ( left, right ) {
479
+ let a := _parse_version(left);
480
+ let b := _parse_version(right);
481
+ let max := a{nums}.length() > b{nums}.length()
482
+ ? a{nums}.length()
483
+ : b{nums}.length();
484
+
485
+ let i := 0;
486
+ while ( i < max ) {
487
+ let av := i < a{nums}.length() ? a{nums}[i]: 0;
488
+ let bv := i < b{nums}.length() ? b{nums}[i]: 0;
489
+ return av <=> bv if av != bv;
490
+ i++;
491
+ }
492
+
493
+ if ( a{suffix} eq "" and b{suffix} ne "" ) {
494
+ return 1;
495
+ }
496
+ if ( a{suffix} ne "" and b{suffix} eq "" ) {
497
+ return -1;
498
+ }
499
+ return a{suffix} cmp b{suffix};
500
+ }
501
+
502
+ function _module_key ( module_name ) {
503
+ let text := "" _ module_name;
504
+ if ( ends_with( text, ".zzm" ) ) {
505
+ return substr( text, 0, length text - 4 );
506
+ }
507
+ return text;
508
+ }
509
+
510
+ function _is_url ( String target ) {
511
+ return (
512
+ starts_with( target, "http://" ) or
513
+ starts_with( target, "https://" )
514
+ );
515
+ }
516
+
517
+ function _trim_right_slash ( String text ) {
518
+ let out := text;
519
+ while ( length out > 0 and substr( out, length out - 1, 1 ) eq "/" ) {
520
+ out := substr( out, 0, length out - 1 );
521
+ }
522
+ return out;
523
+ }
524
+
525
+ function _safe_archive_root ( archive, String where ) {
526
+ if ( not( archive instanceof Dict ) or not( archive.get("entries") instanceof Array ) ) {
527
+ die `Invalid archive ${where}: entries must be an array`;
528
+ }
529
+
530
+ let seen := {};
531
+ let root := null;
532
+ for ( let entry in archive{entries} ) {
533
+ if ( not( entry instanceof Dict ) or not( entry.get("path") instanceof String ) ) {
534
+ die `Invalid archive ${where}: bad entry path`;
535
+ }
536
+ let path := entry{path};
537
+ if ( path eq "" ) {
538
+ die `Invalid archive ${where}: empty path`;
539
+ }
540
+ if ( starts_with( path, "/" ) or contains( path, "\\" ) ) {
541
+ die `Invalid archive ${where}: unsafe path ${path}`;
542
+ }
543
+ if ( seen.exists(path) ) {
544
+ die `Invalid archive ${where}: duplicate path ${path}`;
545
+ }
546
+ seen.add( path, true );
547
+
548
+ let parts := split( path, "/" );
549
+ if ( parts.length() == 0 or parts[0] eq "" ) {
550
+ die `Invalid archive ${where}: empty path component`;
551
+ }
552
+ for ( let part in parts ) {
553
+ if ( part eq "" or part eq "." or part eq ".." ) {
554
+ die `Invalid archive ${where}: unsafe path ${path}`;
555
+ }
556
+ }
557
+ if ( root instanceof Null ) {
558
+ root := parts[0];
559
+ }
560
+ else if ( root ne parts[0] ) {
561
+ die `Invalid archive ${where}: multiple top-level roots`;
562
+ }
563
+ }
564
+
565
+ die `Invalid archive ${where}: no entries` if root instanceof Null;
566
+ return root;
567
+ }
568
+
569
+ function _ensure_dir ( path ) {
570
+ return true if path.exists();
571
+ let parent := path.parent();
572
+ _ensure_dir(parent) if not parent.exists();
573
+ path.mkdir();
574
+ return true;
575
+ }
576
+
577
+ function _mkdir_parent ( path ) {
578
+ _ensure_dir( path.parent() );
579
+ return true;
580
+ }
581
+
582
+ function _extract_archive ( archive, String root_name, root_dir ) {
583
+ for ( let entry in archive{entries} ) {
584
+ let parts := split( entry{path}, "/" );
585
+ let out := root_dir;
586
+ let i := 1;
587
+ while ( i < parts.length() ) {
588
+ out := out.child(parts[i]);
589
+ i++;
590
+ }
591
+ _mkdir_parent(out);
592
+ out.spew(entry{data});
593
+ }
594
+ return true;
595
+ }
596
+
597
+ function _relative_path ( root, path ) {
598
+ let prefix := root.to_String() _ "/";
599
+ let text := path.to_String();
600
+ if ( starts_with( text, prefix ) ) {
601
+ return substr( text, length prefix );
602
+ }
603
+ return path.basename();
604
+ }
605
+
606
+ function _discover_files ( root, dir_name, extension ) {
607
+ let base := root.child(dir_name);
608
+ return [] if not base.exists();
609
+ return [] if not base.is_dir();
610
+
611
+ let found := [];
612
+ function walk ( path ) {
613
+ for ( let child in path.children() ) {
614
+ if ( child.is_dir() ) {
615
+ walk(child);
616
+ }
617
+ else if ( child.is_file() and ends_with( child.basename(), extension ) ) {
618
+ found.push( _relative_path( root, child ) );
619
+ }
620
+ }
621
+ }
622
+ walk(base);
623
+ return found.sort( fn ( a, b ) -> a cmp b );
624
+ }
625
+
626
+ function _has_extension ( String basename ) {
627
+ return contains( basename, "." );
628
+ }
629
+
630
+ function _has_zuzu_shebang ( path ) {
631
+ let text := "";
632
+ try {
633
+ text := path.slurp_utf8();
634
+ }
635
+ catch {
636
+ return false;
637
+ }
638
+
639
+ let lines := split( text, "\n" );
640
+ let first_line := lines.length() == 0 ? "" : lines[0];
641
+ return starts_with( first_line, "#!" ) and contains( first_line, "zuzu" );
642
+ }
643
+
644
+ function _discover_scripts ( root ) {
645
+ let base := root.child("scripts");
646
+ return [] if not base.exists();
647
+ return [] if not base.is_dir();
648
+
649
+ let found := [];
650
+ function walk ( path ) {
651
+ for ( let child in path.children() ) {
652
+ if ( child.is_dir() ) {
653
+ walk(child);
654
+ }
655
+ else if ( child.is_file() ) {
656
+ let basename := child.basename();
657
+ if (
658
+ ends_with( basename, ".zzs" ) or
659
+ (
660
+ not _has_extension(basename) and
661
+ _has_zuzu_shebang(child)
662
+ )
663
+ ) {
664
+ found.push( _relative_path( root, child ) );
665
+ }
666
+ }
667
+ }
668
+ }
669
+ walk(base);
670
+ return found.sort( fn ( a, b ) -> a cmp b );
671
+ }
672
+
673
+ function _module_install_name ( String source ) {
674
+ return substr( source, length "modules/" );
675
+ }
676
+
677
+ function _script_install_name ( String source ) {
678
+ return substr( source, length "scripts/" );
679
+ }
680
+
681
+ function _target_list ( targets ) {
682
+ return targets instanceof Array ? targets : [ targets ];
683
+ }
684
+
685
+ function _root_key ( root ) {
686
+ return (
687
+ root{lib_dir} _ "\n" _
688
+ root{bin_dir} _ "\n" _
689
+ root{meta_dir}
690
+ );
691
+ }
692
+
693
+ function _root_from_config ( String name, String kind, cfg ) {
694
+ return {
695
+ name: name,
696
+ kind: kind,
697
+ lib_dir: cfg{lib_dir},
698
+ bin_dir: cfg{bin_dir},
699
+ meta_dir: cfg{meta_dir},
700
+ global: cfg{global},
701
+ windows: cfg{windows},
702
+ };
703
+ }
704
+
705
+ function _require_root_text ( root, String key, String where ) {
706
+ if ( not( root instanceof Dict ) or not root.exists(key) ) {
707
+ die `Invalid dependency root ${where}: missing ${key}`;
708
+ }
709
+ let value := root.get(key);
710
+ if ( not( value instanceof String ) or value eq "" ) {
711
+ die `Invalid dependency root ${where}: ${key} must be a string`;
712
+ }
713
+ return value;
714
+ }
715
+
716
+ function _custom_root ( root, String where ) {
717
+ return {
718
+ name: _require_root_text( root, "name", where ),
719
+ kind: "custom",
720
+ lib_dir: _require_root_text( root, "lib_dir", where ),
721
+ bin_dir: _require_root_text( root, "bin_dir", where ),
722
+ meta_dir: _require_root_text( root, "meta_dir", where ),
723
+ global: root.exists("global") ? root{global} : false,
724
+ windows: root.exists("windows") ? root{windows} : false,
725
+ };
726
+ }
727
+
728
+ function _root_override ( root, String name, String kind, String where ) {
729
+ return {
730
+ name: name,
731
+ kind: kind,
732
+ lib_dir: _require_root_text( root, "lib_dir", where ),
733
+ bin_dir: _require_root_text( root, "bin_dir", where ),
734
+ meta_dir: _require_root_text( root, "meta_dir", where ),
735
+ global: root.exists("global") ? root{global} : false,
736
+ windows: root.exists("windows") ? root{windows} : false,
737
+ };
738
+ }
739
+
740
+ function _add_root ( roots, seen, root ) {
741
+ let key := _root_key(root);
742
+ return false if seen.exists(key);
743
+ seen.add( key, true );
744
+ roots.push(root);
745
+ return true;
746
+ }
747
+
748
+ function _path_join ( String base, String child, Boolean windows ) {
749
+ let sep := windows ? "\\": "/";
750
+ return base _ sep _ child;
751
+ }
752
+
753
+ function _path_child ( String base, String relative ) {
754
+ let out := new Path(base);
755
+ for ( let part in split( relative, "/" ) ) {
756
+ out := out.child(part) if part ne "";
757
+ }
758
+ return out;
759
+ }
760
+
761
+ function _relative_basename ( String relative ) {
762
+ let parts := split( relative, "/" );
763
+ return parts[ parts.length() - 1 ];
764
+ }
765
+
766
+ function _replace_script_suffix ( String relative, String suffix ) {
767
+ if ( ends_with( relative, ".zzs" ) ) {
768
+ return substr( relative, 0, length relative - 4 ) _ suffix;
769
+ }
770
+ return relative _ suffix;
771
+ }
772
+
773
+ function _installed_at () {
774
+ return ( new Time() ).strftime("%Y-%m-%dT%H:%M:%SZ");
775
+ }
776
+
777
+ function _now_epoch () {
778
+ return ( new Time() ).epoch();
779
+ }
780
+
781
+ function _temp_parent ( options? ) {
782
+ let temp_root := _opt( options, "temp_root", null );
783
+ if ( temp_root instanceof Null ) {
784
+ return null;
785
+ }
786
+ let root := new Path(temp_root);
787
+ _ensure_dir(root);
788
+ return root;
789
+ }
790
+
791
+ function _unique_temp_path ( parent, String prefix, String suffix ) {
792
+ let base := prefix _ Proc.pid() _ "-" _ _now_epoch();
793
+ let i := 0;
794
+ while ( true ) {
795
+ let candidate := parent.child( base _ "-" _ i _ suffix );
796
+ return candidate if not candidate.exists();
797
+ i++;
798
+ }
799
+ }
800
+
801
+ function _new_temp_dir ( options?, String prefix := "zuzuzoo-" ) {
802
+ let parent := _temp_parent(options);
803
+ if ( parent instanceof Null ) {
804
+ return Path.tempdir();
805
+ }
806
+
807
+ let i := 0;
808
+ while ( true ) {
809
+ let candidate := _unique_temp_path(
810
+ parent,
811
+ prefix,
812
+ ".d",
813
+ );
814
+ if ( candidate.mkdir_exclusive() ) {
815
+ return candidate;
816
+ }
817
+ i++;
818
+ die `Could not allocate temporary directory in ${parent}`
819
+ if i > 1000;
820
+ }
821
+ }
822
+
823
+ function _cleanup_path ( path ) {
824
+ return false if path instanceof Null;
825
+ try {
826
+ if ( path.exists() ) {
827
+ if ( path.is_dir() ) {
828
+ path.remove_tree();
829
+ }
830
+ else {
831
+ path.remove();
832
+ }
833
+ return true;
834
+ }
835
+ }
836
+ catch ( Exception e ) {
837
+ return false;
838
+ }
839
+ return false;
840
+ }
841
+
842
+ function _cleanup_source ( source, options? ) {
843
+ return false if source instanceof Null;
844
+ return false if _opt( options, "keep_work_dirs", false );
845
+ if ( source.exists("temp_dir") ) {
846
+ return _cleanup_path(source{temp_dir});
847
+ }
848
+ return false;
849
+ }
850
+
851
+ function _cleanup_install_action ( install_action, options? ) {
852
+ return false if _opt( options, "keep_work_dirs", false );
853
+ _cleanup_source( install_action{source}, options )
854
+ if install_action.exists("source");
855
+ _cleanup_path( install_action{work_dir_obj} )
856
+ if install_action.exists("work_dir_obj");
857
+ return true;
858
+ }
859
+
860
+ function _cleanup_plan_work_dirs ( plan, options? ) {
861
+ return false if plan instanceof Null;
862
+ return false if _opt( options, "keep_work_dirs", false );
863
+ for ( let install_action in plan.get( "installs", [] ) ) {
864
+ _cleanup_install_action( install_action, options );
865
+ }
866
+ return true;
867
+ }
868
+
869
+ function _cleanup_loaded_work_dirs ( loaded, options? ) {
870
+ return false if _opt( options, "keep_work_dirs", false );
871
+ for ( let item in loaded ) {
872
+ _cleanup_install_action( item, options );
873
+ }
874
+ return true;
875
+ }
876
+
877
+ function _atomic_temp_sibling ( path, String suffix := ".tmp" ) {
878
+ return _unique_temp_path(
879
+ path.parent(),
880
+ "." _ path.basename() _ ".",
881
+ suffix,
882
+ );
883
+ }
884
+
885
+ function _atomic_json_write ( path, value ) {
886
+ _mkdir_parent(path);
887
+ let temp := _atomic_temp_sibling(path);
888
+ let codec := new JSON( pretty: true, canonical: true );
889
+ try {
890
+ codec.dump( temp, value );
891
+ codec.load(temp);
892
+ temp.move(path);
893
+ }
894
+ catch ( Exception e ) {
895
+ _cleanup_path(temp);
896
+ throw e;
897
+ }
898
+ return path;
899
+ }
900
+
901
+ function _copy_file_atomic ( source, destination, chmod_mode? ) {
902
+ _mkdir_parent(destination);
903
+ let temp := _atomic_temp_sibling(destination);
904
+ try {
905
+ source.copy(temp);
906
+ let temp_sha := sha256_hex(temp.slurp());
907
+ temp.chmod(chmod_mode) if not( chmod_mode instanceof Null );
908
+ temp.move(destination);
909
+ let final_bytes := destination.slurp();
910
+ let final_sha := sha256_hex(final_bytes);
911
+ if ( final_sha ne temp_sha ) {
912
+ die(
913
+ `Atomic install verification failed for ${destination}: ` _
914
+ `expected ${temp_sha}, got ${final_sha}`
915
+ );
916
+ }
917
+ return {
918
+ sha256: final_sha,
919
+ size: destination.size(),
920
+ };
921
+ }
922
+ catch ( Exception e ) {
923
+ _cleanup_path(temp);
924
+ throw e;
925
+ }
926
+ }
927
+
928
+ function _spew_utf8_atomic ( destination, String text ) {
929
+ _mkdir_parent(destination);
930
+ let temp := _atomic_temp_sibling(destination);
931
+ try {
932
+ temp.spew_utf8(text);
933
+ temp.move(destination);
934
+ }
935
+ catch ( Exception e ) {
936
+ _cleanup_path(temp);
937
+ throw e;
938
+ }
939
+ return destination;
940
+ }
941
+
942
+ function _source_context ( source ) {
943
+ let parts := [
944
+ "target=" _ source{value},
945
+ "source_type=" _ source{type},
946
+ ];
947
+ parts.push( "url=" _ source{url} ) if source.exists("url");
948
+ parts.push( "resolved_url=" _ source{resolved_url} )
949
+ if source.exists("resolved_url");
950
+ parts.push( "path=" _ source{path} ) if source.exists("path");
951
+ return join( ", ", parts );
952
+ }
953
+
954
+ function _corrupt_archive_error ( source, underlying ) {
955
+ return (
956
+ "Corrupt source archive (" _ _source_context(source) _
957
+ "): " _ underlying.to_String()
958
+ );
959
+ }
960
+
961
+ function _cache_key ( String url ) {
962
+ return sha256_hex(to_binary(url));
963
+ }
964
+
965
+ function _cache_paths ( String cache_dir, String url ) {
966
+ let dir := new Path(cache_dir);
967
+ _ensure_dir(dir);
968
+ let key := _cache_key(url);
969
+ return {
970
+ dir: dir,
971
+ key: key,
972
+ archive: dir.child(key _ ".archive"),
973
+ sidecar: dir.child(key _ ".json"),
974
+ };
975
+ }
976
+
977
+ function _delete_cache_entry ( paths ) {
978
+ _cleanup_path(paths{archive});
979
+ _cleanup_path(paths{sidecar});
980
+ return true;
981
+ }
982
+
983
+ function _validate_cache_entry ( paths ) {
984
+ return false if not paths{archive}.exists();
985
+ return false if not paths{sidecar}.exists();
986
+
987
+ let sidecar := null;
988
+ try {
989
+ sidecar := ( new JSON() ).load(paths{sidecar});
990
+ }
991
+ catch ( Exception e ) {
992
+ return false;
993
+ }
994
+
995
+ let bytes := paths{archive}.slurp();
996
+ let actual_sha := sha256_hex(bytes);
997
+ if ( sidecar.get( "archive_sha256", "" ) ne actual_sha ) {
998
+ return false;
999
+ }
1000
+ if ( sidecar.get( "byte_size", -1 ) != paths{archive}.size() ) {
1001
+ return false;
1002
+ }
1003
+ try {
1004
+ Archive.decode(bytes);
1005
+ }
1006
+ catch ( Exception e ) {
1007
+ return false;
1008
+ }
1009
+ return sidecar;
1010
+ }
1011
+
1012
+ function _write_cache_entry ( paths, source, temp_path ) {
1013
+ let bytes := temp_path.slurp();
1014
+ let sidecar := {
1015
+ original_url: source{url},
1016
+ resolved_url: source{resolved_url},
1017
+ downloaded_at: _installed_at(),
1018
+ archive_sha256: sha256_hex(bytes),
1019
+ byte_size: temp_path.size(),
1020
+ };
1021
+ Archive.decode(bytes);
1022
+ let archive_temp := _atomic_temp_sibling(paths{archive});
1023
+ let sidecar_temp := _atomic_temp_sibling(paths{sidecar});
1024
+ try {
1025
+ temp_path.copy(archive_temp);
1026
+ archive_temp.move(paths{archive});
1027
+ ( new JSON( pretty: true, canonical: true ) ).dump(
1028
+ sidecar_temp,
1029
+ sidecar,
1030
+ );
1031
+ ( new JSON() ).load(sidecar_temp);
1032
+ sidecar_temp.move(paths{sidecar});
1033
+ }
1034
+ catch ( Exception e ) {
1035
+ _cleanup_path(archive_temp);
1036
+ _cleanup_path(sidecar_temp);
1037
+ _delete_cache_entry(paths);
1038
+ throw e;
1039
+ }
1040
+ return sidecar;
1041
+ }
1042
+
1043
+ function _check_expected_source_sha ( source, expected_sha256 ) {
1044
+ return true if expected_sha256 instanceof Null;
1045
+ let actual := sha256_hex( ( new Path(source{path}) ).slurp() );
1046
+ if ( actual ne expected_sha256 ) {
1047
+ die(
1048
+ "Source checksum mismatch (" _ _source_context(source) _
1049
+ `): expected ${expected_sha256}, got ${actual}`
1050
+ );
1051
+ }
1052
+ return true;
1053
+ }
1054
+
1055
+ function _dependency_chain ( stack, dependency_of, module_name ) {
1056
+ let chain := [];
1057
+ for ( let item in stack ) {
1058
+ chain.push(item);
1059
+ }
1060
+ if ( not( dependency_of instanceof Null ) ) {
1061
+ chain.push(dependency_of{metadata}{name});
1062
+ }
1063
+ chain.push(module_name);
1064
+ return join( " -> ", chain );
1065
+ }
1066
+
1067
+ function _dependency_conflict_message (
1068
+ dep,
1069
+ dependency_of,
1070
+ stack,
1071
+ planned_action,
1072
+ conflicting_dist
1073
+ ) {
1074
+ let requested_by := dependency_of instanceof Null
1075
+ ? "<requested target>"
1076
+ : dependency_of{metadata}{name};
1077
+ let planned_text := planned_action instanceof Null
1078
+ ? "none"
1079
+ : planned_action{metadata}{name} _ " " _
1080
+ planned_action{metadata}{version};
1081
+ let conflicting_text := conflicting_dist{metadata}{name} _ " " _
1082
+ conflicting_dist{metadata}{version};
1083
+ return (
1084
+ "Dependency conflict: requested dependency " _
1085
+ dep{module_name} _ " >= " _ dep{min_version} _
1086
+ "; requester chain " _
1087
+ _dependency_chain(
1088
+ stack,
1089
+ dependency_of,
1090
+ dep{module_name},
1091
+ ) _
1092
+ "; requested by " _ requested_by _
1093
+ "; planned/provided version " _ planned_text _
1094
+ "; conflicting planned/provided version " _
1095
+ conflicting_text
1096
+ );
1097
+ }
1098
+
1099
+ function _planned_version_conflict_message (
1100
+ String dist_name,
1101
+ existing,
1102
+ dist,
1103
+ String target_text
1104
+ ) {
1105
+ return (
1106
+ `Conflicting planned versions for ${dist_name}: ` _
1107
+ existing{metadata}{version} _ " and " _
1108
+ dist{metadata}{version} _
1109
+ "; existing target " _ existing{target} _
1110
+ "; conflicting target " _ target_text _
1111
+ "; existing metadata " _
1112
+ existing{metadata}{metadata_file} _
1113
+ "; conflicting metadata " _
1114
+ dist{metadata}{metadata_file}
1115
+ );
1116
+ }
1117
+
1118
+ function _copy_dependencies ( metadata ) {
1119
+ let out := {};
1120
+ return out if not metadata.exists("dependencies");
1121
+ let deps := metadata{dependencies};
1122
+ if ( deps instanceof PairList ) {
1123
+ let keys := deps.keys();
1124
+ let values := deps.values();
1125
+ let i := 0;
1126
+ while ( i < keys.length() ) {
1127
+ out.set( keys[i], values[i] );
1128
+ i++;
1129
+ }
1130
+ return out;
1131
+ }
1132
+ for ( let key in deps.keys() ) {
1133
+ out.set( key, deps.get(key) );
1134
+ }
1135
+ return out;
1136
+ }
1137
+
1138
+ function _copy_source_record ( source ) {
1139
+ let out := {
1140
+ type: source{type},
1141
+ value: source{value},
1142
+ };
1143
+ out{url} := source{url} if source.exists("url");
1144
+ out{resolved_url} := source{resolved_url}
1145
+ if source.exists("resolved_url");
1146
+ out{path} := source{path} if source.exists("path");
1147
+ return out;
1148
+ }
1149
+
1150
+ function _copy_source_metadata ( metadata ) {
1151
+ let out := {};
1152
+ for ( let key in metadata.keys() ) {
1153
+ next if key eq "metadata_file";
1154
+ if ( key eq "dependencies" ) {
1155
+ out.set( key, _copy_dependencies(metadata) );
1156
+ }
1157
+ else {
1158
+ out.set( key, metadata.get(key) );
1159
+ }
1160
+ }
1161
+ out.set( "status", "stable" ) if not out.exists("status");
1162
+ out.set( "dependencies", {} ) if not out.exists("dependencies");
1163
+ return out;
1164
+ }
1165
+
1166
+ function _test_ok ( parsed, run_result ) {
1167
+ return false if not Proc.is_success(run_result);
1168
+ return false if parsed{planned} instanceof Null;
1169
+ return false if parsed{assertions}{failed} > 0;
1170
+ return true;
1171
+ }
1172
+
1173
+ function _dependency_entries ( metadata ) {
1174
+ let out := [];
1175
+ return out if not metadata.exists("dependencies");
1176
+
1177
+ let deps := metadata{dependencies};
1178
+ if ( deps instanceof PairList ) {
1179
+ let keys := deps.keys();
1180
+ let values := deps.values();
1181
+ let i := 0;
1182
+ while ( i < keys.length() ) {
1183
+ out.push(
1184
+ {
1185
+ module_name: keys[i],
1186
+ min_version: values[i],
1187
+ },
1188
+ );
1189
+ i++;
1190
+ }
1191
+ return out;
1192
+ }
1193
+
1194
+ let keys := deps.keys();
1195
+ for ( let key in keys ) {
1196
+ out.push(
1197
+ {
1198
+ module_name: key,
1199
+ min_version: deps.get(key),
1200
+ },
1201
+ );
1202
+ }
1203
+ return out;
1204
+ }
1205
+
1206
+ function _version_satisfies ( version, min_version ) {
1207
+ return true if min_version instanceof Null;
1208
+ return true if "" _ min_version eq "0";
1209
+ return compare_versions( version, min_version ) >= 0;
1210
+ }
1211
+
1212
+ function _provides_module ( dist, module_name, min_version ) {
1213
+ return false if not _version_satisfies( dist{version}, min_version );
1214
+ let wanted := _module_key(module_name);
1215
+ for ( let module in dist{installed}{modules} ) {
1216
+ return true if _module_key( module{install_as} ) eq wanted;
1217
+ }
1218
+ return false;
1219
+ }
1220
+
1221
+ function _planned_provides_module ( install_action, module_name, min_version ) {
1222
+ return false if not _version_satisfies(
1223
+ install_action{metadata}{version},
1224
+ min_version,
1225
+ );
1226
+ let wanted := _module_key(module_name);
1227
+ for ( let module in install_action{modules} ) {
1228
+ return true if _module_key( module{install_as} ) eq wanted;
1229
+ }
1230
+ return false;
1231
+ }
1232
+
1233
+ function _loaded_distribution_provides ( dist, module_name, min_version ) {
1234
+ return false if not _version_satisfies(
1235
+ dist{metadata}{version},
1236
+ min_version,
1237
+ );
1238
+ let wanted := _module_key(module_name);
1239
+ for ( let module in dist{modules} ) {
1240
+ return true if _module_key( module{install_as} ) eq wanted;
1241
+ }
1242
+ return false;
1243
+ }
1244
+
1245
+ function _metadata_files_in_dir ( String meta_dir ) {
1246
+ let dir := new Path(meta_dir);
1247
+ return [] if not dir.exists();
1248
+ die `Metadata path is not a directory: ${dir}`
1249
+ if not dir.is_dir();
1250
+
1251
+ let files := [];
1252
+ for ( let child in dir.children() ) {
1253
+ if (
1254
+ child.is_file() and
1255
+ ends_with( child.basename(), ".json" )
1256
+ ) {
1257
+ files.push(child);
1258
+ }
1259
+ }
1260
+ return files.sort(
1261
+ fn ( a, b ) -> a.to_String() cmp b.to_String()
1262
+ );
1263
+ }
1264
+
1265
+ function _list_installed_in_root ( root ) {
1266
+ let codec := new JSON();
1267
+ let installed := [];
1268
+ for ( let file in _metadata_files_in_dir( root{meta_dir} ) ) {
1269
+ let data := codec.load(file);
1270
+ let valid := _validate_installed_metadata(
1271
+ data,
1272
+ file.to_String(),
1273
+ );
1274
+ installed.push(valid);
1275
+ }
1276
+
1277
+ return installed.sort( function ( a, b ) {
1278
+ let name_cmp := a{name} cmp b{name};
1279
+ return name_cmp if name_cmp != 0;
1280
+ return compare_versions( b{version}, a{version} );
1281
+ } );
1282
+ }
1283
+
1284
+ function _candidate_record (
1285
+ String source,
1286
+ module_name,
1287
+ min_version,
1288
+ version,
1289
+ distribution,
1290
+ root,
1291
+ metadata_file,
1292
+ install_action
1293
+ ) {
1294
+ let record := {
1295
+ module_name: "" _ module_name,
1296
+ min_version: min_version instanceof Null ? null : "" _ min_version,
1297
+ version: "" _ version,
1298
+ source: source,
1299
+ distribution: distribution,
1300
+ metadata_file: metadata_file,
1301
+ };
1302
+ record{root} := root if not( root instanceof Null );
1303
+ record{install} := install_action if not( install_action instanceof Null );
1304
+ return record;
1305
+ }
1306
+
1307
+ function _better_dependency_candidate ( current, candidate, root_order ) {
1308
+ return true if current instanceof Null;
1309
+
1310
+ let version_cmp := compare_versions(
1311
+ candidate{version},
1312
+ current{version},
1313
+ );
1314
+ return true if version_cmp > 0;
1315
+ return false if version_cmp < 0;
1316
+
1317
+ let current_order := root_order.exists(current{root}{name})
1318
+ ? root_order.get(current{root}{name})
1319
+ : 1000000;
1320
+ let candidate_order := root_order.exists(candidate{root}{name})
1321
+ ? root_order.get(candidate{root}{name})
1322
+ : 1000000;
1323
+ return true if candidate_order < current_order;
1324
+ return false if candidate_order > current_order;
1325
+
1326
+ return candidate{metadata_file} lt current{metadata_file};
1327
+ }
1328
+
1329
+ function _find_dependency_in_roots ( roots, module_name, min_version ) {
1330
+ let root_order := {};
1331
+ let i := 0;
1332
+ while ( i < roots.length() ) {
1333
+ root_order.add( roots[i]{name}, i );
1334
+ i++;
1335
+ }
1336
+
1337
+ let best := null;
1338
+ for ( let root in roots ) {
1339
+ for ( let dist in _list_installed_in_root(root) ) {
1340
+ if ( _provides_module( dist, module_name, min_version ) ) {
1341
+ let candidate := _candidate_record(
1342
+ "root",
1343
+ module_name,
1344
+ min_version,
1345
+ dist{version},
1346
+ dist{name},
1347
+ root,
1348
+ dist{metadata_file},
1349
+ null,
1350
+ );
1351
+ candidate{installed} := dist;
1352
+ if (
1353
+ _better_dependency_candidate(
1354
+ best,
1355
+ candidate,
1356
+ root_order,
1357
+ )
1358
+ ) {
1359
+ best := candidate;
1360
+ }
1361
+ }
1362
+ }
1363
+ }
1364
+ return best;
1365
+ }
1366
+
1367
+ function _find_dependency_in_planned ( planned, module_name, min_version ) {
1368
+ let best := null;
1369
+ for ( let install_action in planned ) {
1370
+ if ( _planned_provides_module( install_action, module_name, min_version ) ) {
1371
+ let candidate := _candidate_record(
1372
+ "planned",
1373
+ module_name,
1374
+ min_version,
1375
+ install_action{metadata}{version},
1376
+ install_action{metadata}{name},
1377
+ install_action{target_root},
1378
+ install_action{metadata}{metadata_file},
1379
+ install_action,
1380
+ );
1381
+ if (
1382
+ best instanceof Null or
1383
+ compare_versions(
1384
+ candidate{version},
1385
+ best{version},
1386
+ ) > 0 or
1387
+ (
1388
+ compare_versions(
1389
+ candidate{version},
1390
+ best{version},
1391
+ ) == 0 and
1392
+ candidate{metadata_file} lt best{metadata_file}
1393
+ )
1394
+ ) {
1395
+ best := candidate;
1396
+ }
1397
+ }
1398
+ }
1399
+ return best;
1400
+ }
1401
+
1402
+ function _runtime_module_path ( module_name ) {
1403
+ return null if not starts_with( "" _ module_name, "std/" );
1404
+
1405
+ let inc := [];
1406
+ if ( "inc" in __system__ ) {
1407
+ inc := __system__.get("inc");
1408
+ }
1409
+ return null if inc ≡ null;
1410
+ if ( not( inc instanceof Array ) ) {
1411
+ let separator := _is_windows_platform() ? ";" : ":";
1412
+ inc := split( "" _ inc, separator );
1413
+ }
1414
+
1415
+ let relative := module_name _ ".zzm";
1416
+ for ( let root in inc ) {
1417
+ next if root eq "";
1418
+ let path := ( new Path(root) ).child(relative);
1419
+ return path if path.exists() and path.is_file();
1420
+ }
1421
+ return null;
1422
+ }
1423
+
1424
+ function _runtime_include_dirs () {
1425
+ let out := [];
1426
+ let seen := {};
1427
+ let inc := [];
1428
+ if ( "inc" in __system__ ) {
1429
+ inc := __system__.get("inc");
1430
+ }
1431
+ return out if inc ≡ null;
1432
+ if ( not( inc instanceof Array ) ) {
1433
+ let separator := _is_windows_platform() ? ";" : ":";
1434
+ inc := split( "" _ inc, separator );
1435
+ }
1436
+
1437
+ for ( let root in inc ) {
1438
+ next if root eq "";
1439
+ let path := ( new Path(root) ).absolute();
1440
+ _push_unique_string( out, seen, path.to_String() )
1441
+ if path.exists() and path.is_dir();
1442
+ }
1443
+ return out;
1444
+ }
1445
+
1446
+ function _find_dependency_in_runtime ( module_name, min_version ) {
1447
+ return null if not _version_satisfies( "0", min_version );
1448
+
1449
+ let module_path := _runtime_module_path(module_name);
1450
+ return null if module_path instanceof Null;
1451
+
1452
+ let root := {
1453
+ name: "runtime",
1454
+ lib_dir: "",
1455
+ bin_dir: "",
1456
+ meta_dir: "",
1457
+ };
1458
+ let candidate := _candidate_record(
1459
+ "runtime",
1460
+ module_name,
1461
+ min_version,
1462
+ "0",
1463
+ "zuzu-runtime",
1464
+ root,
1465
+ module_path.to_String(),
1466
+ null,
1467
+ );
1468
+ candidate{module_file} := module_path.to_String();
1469
+ return candidate;
1470
+ }
1471
+
1472
+ function _find_dependency_for_plan (
1473
+ planned,
1474
+ roots,
1475
+ module_name,
1476
+ min_version
1477
+ ) {
1478
+ let found := _find_dependency_in_planned(
1479
+ planned,
1480
+ module_name,
1481
+ min_version,
1482
+ );
1483
+ return found if not( found instanceof Null );
1484
+ found := _find_dependency_in_roots( roots, module_name, min_version );
1485
+ return found if not( found instanceof Null );
1486
+ return _find_dependency_in_runtime( module_name, min_version );
1487
+ }
1488
+
1489
+ function _cycle_path ( stack, module_name ) {
1490
+ let path := [];
1491
+ let started := false;
1492
+ for ( let item in stack ) {
1493
+ started := true if item eq module_name;
1494
+ path.push(item) if started;
1495
+ }
1496
+ path.push(module_name);
1497
+ return join( " -> ", path );
1498
+ }
1499
+
1500
+ function _stack_contains ( stack, value ) {
1501
+ for ( let item in stack ) {
1502
+ return true if item eq value;
1503
+ }
1504
+ return false;
1505
+ }
1506
+
1507
+ function _remove_file_kind_order ( String kind ) {
1508
+ return kind eq "metadata" ? 1 : 0;
1509
+ }
1510
+
1511
+ function _remove_file_cmp ( a, b ) {
1512
+ let kind_cmp := _remove_file_kind_order(a{kind}) <=>
1513
+ _remove_file_kind_order(b{kind});
1514
+ return kind_cmp if kind_cmp != 0;
1515
+ let path_cmp := a{path} cmp b{path};
1516
+ return path_cmp if path_cmp != 0;
1517
+ return a{kind} cmp b{kind};
1518
+ }
1519
+
1520
+ function _removal_cmp ( a, b ) {
1521
+ let name_cmp := a{name} cmp b{name};
1522
+ return name_cmp if name_cmp != 0;
1523
+ let version_cmp := compare_versions( b{version}, a{version} );
1524
+ return version_cmp if version_cmp != 0;
1525
+ return a{metadata_file} cmp b{metadata_file};
1526
+ }
1527
+
1528
+ function _owner_record ( dist ) {
1529
+ return {
1530
+ name: dist{name},
1531
+ version: dist{version},
1532
+ metadata_file: dist{metadata_file},
1533
+ };
1534
+ }
1535
+
1536
+ function _same_owner ( left, right ) {
1537
+ return left{metadata_file} eq right{metadata_file};
1538
+ }
1539
+
1540
+ function _owner_list_contains ( owners, owner ) {
1541
+ for ( let item in owners ) {
1542
+ return true if _same_owner( item, owner );
1543
+ }
1544
+ return false;
1545
+ }
1546
+
1547
+ function _add_owner_to_list ( owners, owner ) {
1548
+ return false if _owner_list_contains( owners, owner );
1549
+ owners.push(owner);
1550
+ return true;
1551
+ }
1552
+
1553
+ function _add_owner_for_path ( by_path, String path, owner ) {
1554
+ if ( not by_path.exists(path) ) {
1555
+ by_path.add( path, [] );
1556
+ }
1557
+ _add_owner_to_list( by_path.get(path), owner );
1558
+ return true;
1559
+ }
1560
+
1561
+ function _remove_target_record ( target, options? ) {
1562
+ if ( target instanceof Dict ) {
1563
+ let type := target.exists("type") ? "" _ target{type} : "";
1564
+ let value := target.exists("value") ? "" _ target{value} : "";
1565
+ return {
1566
+ type: type,
1567
+ value: value,
1568
+ raw: target,
1569
+ };
1570
+ }
1571
+ return {
1572
+ type: _opt( options, "dist", false ) ? "distribution" : "module",
1573
+ value: "" _ target,
1574
+ raw: target,
1575
+ };
1576
+ }
1577
+
1578
+ function _latest_module_name ( module_name ) {
1579
+ if ( not( module_name instanceof String ) ) {
1580
+ die "latest target must be a module name";
1581
+ }
1582
+ if ( module_name eq "" or _is_url(module_name) ) {
1583
+ die "latest target must be a module name";
1584
+ }
1585
+ let path := new Path(module_name);
1586
+ if ( path.exists() ) {
1587
+ die "latest target must be a module name";
1588
+ }
1589
+ return module_name;
1590
+ }
1591
+
1592
+ function _latest_status ( installed_version, remote_version ) {
1593
+ return "not-installed" if installed_version instanceof Null;
1594
+ let comparison := compare_versions( installed_version, remote_version );
1595
+ return "current" if comparison == 0;
1596
+ return comparison < 0 ? "outdated" : "newer-local";
1597
+ }
1598
+
1599
+ function _add_verify_target_error ( result, code, target, message ) {
1600
+ result{errors}.push(
1601
+ {
1602
+ code: code,
1603
+ target: target,
1604
+ message: message,
1605
+ },
1606
+ );
1607
+ return true;
1608
+ }
1609
+
1610
+ class ZuzuzooLock {
1611
+ let lock_path := null;
1612
+ let owner_file := null;
1613
+ let acquired := false;
1614
+
1615
+ method release () {
1616
+ return false if not acquired;
1617
+ _cleanup_path(owner_file);
1618
+ _cleanup_path(lock_path);
1619
+ acquired := false;
1620
+ return true;
1621
+ }
1622
+ }
1623
+
1624
+ class Zuzuzoo {
1625
+ let lib_dir := null;
1626
+ let bin_dir := null;
1627
+ let meta_dir := null;
1628
+ let global := false;
1629
+ let windows := null;
1630
+ let home := null;
1631
+ let userprofile := null;
1632
+ let base_url := "https://zuzulang.org";
1633
+ let user_agent := null;
1634
+ let zuzu_command := "zuzu";
1635
+ let dependency_roots := [];
1636
+ let global_root := null;
1637
+
1638
+ method _user_agent () {
1639
+ return not( user_agent instanceof Null ) ? user_agent : new UserAgent();
1640
+ }
1641
+
1642
+ method config () {
1643
+ let resolved_windows := windows instanceof Null
1644
+ ? _is_windows_platform()
1645
+ : windows;
1646
+ let resolved_global := global ? true: false;
1647
+
1648
+ if ( resolved_windows and resolved_global ) {
1649
+ die "Global Windows installs are not supported";
1650
+ }
1651
+
1652
+ let base_home := not( home instanceof Null ) ? home : Env.get( "HOME", "" );
1653
+ let base_userprofile := not( userprofile instanceof Null )
1654
+ ? userprofile
1655
+ : Env.get( "USERPROFILE", "" );
1656
+
1657
+ let resolved_lib := lib_dir;
1658
+ let resolved_bin := bin_dir;
1659
+ let resolved_meta := meta_dir;
1660
+ let needs_default := (
1661
+ resolved_lib instanceof Null or
1662
+ resolved_bin instanceof Null or
1663
+ resolved_meta instanceof Null
1664
+ );
1665
+
1666
+ if ( resolved_windows ) {
1667
+ if ( needs_default ) {
1668
+ die "USERPROFILE required for Windows install"
1669
+ if base_userprofile eq "";
1670
+ let root := _join_dir(
1671
+ base_userprofile,
1672
+ ".zuzu",
1673
+ true,
1674
+ );
1675
+ resolved_lib := _join_dir(
1676
+ root,
1677
+ "modules",
1678
+ true,
1679
+ )
1680
+ if resolved_lib instanceof Null;
1681
+ resolved_bin := _join_dir( root, "bin", true )
1682
+ if resolved_bin instanceof Null;
1683
+ resolved_meta := _join_dir( root, "meta", true )
1684
+ if resolved_meta instanceof Null;
1685
+ }
1686
+ }
1687
+ else if ( resolved_global ) {
1688
+ resolved_lib := "/var/lib/zuzu/modules"
1689
+ if resolved_lib instanceof Null;
1690
+ resolved_bin := "/usr/local/bin"
1691
+ if resolved_bin instanceof Null;
1692
+ resolved_meta := "/var/lib/zuzu/meta"
1693
+ if resolved_meta instanceof Null;
1694
+ }
1695
+ else {
1696
+ if ( needs_default ) {
1697
+ die "HOME required for POSIX user install"
1698
+ if base_home eq "";
1699
+ let root := _join_dir(
1700
+ base_home,
1701
+ ".zuzu",
1702
+ false,
1703
+ );
1704
+ resolved_lib := _join_dir(
1705
+ root,
1706
+ "modules",
1707
+ false,
1708
+ )
1709
+ if resolved_lib instanceof Null;
1710
+ resolved_bin := _join_dir( root, "bin", false )
1711
+ if resolved_bin instanceof Null;
1712
+ resolved_meta := _join_dir(
1713
+ root,
1714
+ "meta",
1715
+ false,
1716
+ )
1717
+ if resolved_meta instanceof Null;
1718
+ }
1719
+ }
1720
+
1721
+ return {
1722
+ lib_dir: resolved_lib,
1723
+ bin_dir: resolved_bin,
1724
+ meta_dir: resolved_meta,
1725
+ global: resolved_global,
1726
+ windows: resolved_windows,
1727
+ };
1728
+ }
1729
+
1730
+ method acquire_lock ( operation, options? ) {
1731
+ if ( not _opt( options, "lock", true ) ) {
1732
+ return new ZuzuzooLock( acquired: false );
1733
+ }
1734
+
1735
+ let cfg := self.config();
1736
+ let meta_path := new Path(cfg{meta_dir});
1737
+ _ensure_dir(meta_path);
1738
+ let lock_path := meta_path.child(".zuzuzoo.lock");
1739
+ let owner_file := lock_path.child("owner.json");
1740
+ let timeout := _opt( options, "lock_timeout", 30 );
1741
+ let poll := _opt( options, "lock_poll", 0.1 );
1742
+ let started := _now_epoch();
1743
+
1744
+ while ( true ) {
1745
+ if ( lock_path.mkdir_exclusive() ) {
1746
+ let owner := {
1747
+ pid: Proc.pid(),
1748
+ created_at: _installed_at(),
1749
+ meta_dir: cfg{meta_dir},
1750
+ operation: "" _ operation,
1751
+ };
1752
+ try {
1753
+ ( new JSON( pretty: true, canonical: true ) ).dump(
1754
+ owner_file,
1755
+ owner,
1756
+ );
1757
+ }
1758
+ catch ( Exception e ) {
1759
+ _cleanup_path(lock_path);
1760
+ throw e;
1761
+ }
1762
+ return new ZuzuzooLock(
1763
+ lock_path: lock_path,
1764
+ owner_file: owner_file,
1765
+ acquired: true,
1766
+ );
1767
+ }
1768
+
1769
+ if ( _now_epoch() - started >= timeout ) {
1770
+ let owner_text := "";
1771
+ try {
1772
+ owner_text := owner_file.slurp_utf8();
1773
+ }
1774
+ catch ( Exception e ) {
1775
+ owner_text := "<unreadable>";
1776
+ }
1777
+ die(
1778
+ `Timed out waiting for Zuzuzoo lock ${lock_path} ` _
1779
+ `after ${timeout} seconds; owner=${owner_text}`
1780
+ );
1781
+ }
1782
+ sleep(poll);
1783
+ }
1784
+ }
1785
+
1786
+ method _metadata_files () {
1787
+ return _metadata_files_in_dir( self.config(){meta_dir} );
1788
+ }
1789
+
1790
+ method list_installed ( options? ) {
1791
+ let codec := new JSON();
1792
+ let installed := [];
1793
+ for ( let file in self._metadata_files() ) {
1794
+ let data := codec.load(file);
1795
+ let valid := _validate_installed_metadata(
1796
+ data,
1797
+ file.to_String(),
1798
+ );
1799
+ installed.push(valid);
1800
+ }
1801
+
1802
+ return installed.sort( function ( a, b ) {
1803
+ let name_cmp := a{name} cmp b{name};
1804
+ return name_cmp if name_cmp != 0;
1805
+ return compare_versions( b{version}, a{version} );
1806
+ } );
1807
+ }
1808
+
1809
+ method find_installed_module ( module_name, options? ) {
1810
+ let wanted := _module_key(module_name);
1811
+ for ( let dist in self.list_installed(options) ) {
1812
+ for ( let module in dist{installed}{modules} ) {
1813
+ let installed_as := _module_key(
1814
+ module{install_as},
1815
+ );
1816
+ if ( installed_as eq wanted ) {
1817
+ return dist;
1818
+ }
1819
+ }
1820
+ }
1821
+ return null;
1822
+ }
1823
+
1824
+ method find_installed_distribution ( distribution_name, options? ) {
1825
+ let wanted := "" _ distribution_name;
1826
+ for ( let dist in self.list_installed(options) ) {
1827
+ return dist if dist{name} eq wanted;
1828
+ }
1829
+ return null;
1830
+ }
1831
+
1832
+ method query ( module_name, options? ) {
1833
+ return self.find_installed_module( module_name, options );
1834
+ }
1835
+
1836
+ method query_distribution ( distribution_name, options? ) {
1837
+ return self.find_installed_distribution(
1838
+ distribution_name,
1839
+ options,
1840
+ );
1841
+ }
1842
+
1843
+ method is_installed ( module_name, min_version? ) {
1844
+ let found := self.find_installed_module(module_name);
1845
+ return false if found instanceof Null;
1846
+ if ( not( min_version instanceof Null ) ) {
1847
+ let comparison := compare_versions(
1848
+ found{version},
1849
+ min_version,
1850
+ );
1851
+ return comparison >= 0;
1852
+ }
1853
+ return true;
1854
+ }
1855
+
1856
+ method installed_version ( module_name ) {
1857
+ let found := self.find_installed_module(module_name);
1858
+ return found instanceof Null ? null : found{version};
1859
+ }
1860
+
1861
+ method pretty_json ( value ) {
1862
+ let codec := new JSON( pretty: true, canonical: true );
1863
+ return codec.encode(value);
1864
+ }
1865
+
1866
+ method format_json ( value, options? ) {
1867
+ return self.pretty_json(value);
1868
+ }
1869
+
1870
+ method _verify_module_file ( result, dist, entry ) {
1871
+ let path := _path_child(
1872
+ dist{installed}{lib_dir},
1873
+ entry{install_as},
1874
+ );
1875
+ let file := {
1876
+ kind: "module",
1877
+ distribution: dist{name},
1878
+ version: dist{version},
1879
+ install_as: entry{install_as},
1880
+ path: path.to_String(),
1881
+ exists: path.exists(),
1882
+ expected_sha256: entry{sha256},
1883
+ expected_size: entry.get( "size", null ),
1884
+ actual_sha256: null,
1885
+ actual_size: null,
1886
+ hash_ok: false,
1887
+ size_ok: null,
1888
+ };
1889
+ if ( path.exists() ) {
1890
+ let bytes := path.slurp();
1891
+ file{actual_sha256} := sha256_hex(bytes);
1892
+ file{actual_size} := path.size();
1893
+ file{hash_ok} := file{actual_sha256} eq entry{sha256};
1894
+ if ( file{expected_size} != null ) {
1895
+ file{size_ok} := file{actual_size} == file{expected_size};
1896
+ if ( not file{size_ok} ) {
1897
+ result{size_mismatches}.push(file);
1898
+ }
1899
+ }
1900
+ if ( not file{hash_ok} ) {
1901
+ result{hash_mismatches}.push(file);
1902
+ }
1903
+ }
1904
+ else {
1905
+ result{missing_files}.push(file);
1906
+ }
1907
+ result{files}.push(file);
1908
+ result{checked_files}.push(file);
1909
+ return file;
1910
+ }
1911
+
1912
+ method _verify_script_file ( result, dist, entry ) {
1913
+ let path := _path_child(
1914
+ dist{installed}{bin_dir},
1915
+ entry{install_as},
1916
+ );
1917
+ let file := {
1918
+ kind: "script",
1919
+ distribution: dist{name},
1920
+ version: dist{version},
1921
+ install_as: entry{install_as},
1922
+ path: path.to_String(),
1923
+ exists: path.exists(),
1924
+ expected_sha256: entry{sha256},
1925
+ expected_size: entry.get( "size", null ),
1926
+ actual_sha256: null,
1927
+ actual_size: null,
1928
+ hash_ok: false,
1929
+ size_ok: null,
1930
+ };
1931
+ if ( path.exists() ) {
1932
+ let bytes := path.slurp();
1933
+ file{actual_sha256} := sha256_hex(bytes);
1934
+ file{actual_size} := path.size();
1935
+ file{hash_ok} := file{actual_sha256} eq entry{sha256};
1936
+ if ( file{expected_size} != null ) {
1937
+ file{size_ok} := file{actual_size} == file{expected_size};
1938
+ if ( not file{size_ok} ) {
1939
+ result{size_mismatches}.push(file);
1940
+ }
1941
+ }
1942
+ if ( not file{hash_ok} ) {
1943
+ result{hash_mismatches}.push(file);
1944
+ }
1945
+ }
1946
+ else {
1947
+ result{missing_files}.push(file);
1948
+ }
1949
+ result{files}.push(file);
1950
+ result{checked_files}.push(file);
1951
+
1952
+ for ( let wrapper in entry.get( "wrappers", [] ) ) {
1953
+ let wrapper_path := _path_child(
1954
+ dist{installed}{bin_dir},
1955
+ wrapper,
1956
+ );
1957
+ let wrapper_file := {
1958
+ kind: "wrapper",
1959
+ distribution: dist{name},
1960
+ version: dist{version},
1961
+ install_as: wrapper,
1962
+ path: wrapper_path.to_String(),
1963
+ exists: wrapper_path.exists(),
1964
+ expected_sha256: null,
1965
+ actual_sha256: null,
1966
+ hash_ok: null,
1967
+ };
1968
+ if ( not wrapper_file{exists} ) {
1969
+ result{missing_files}.push(wrapper_file);
1970
+ }
1971
+ result{files}.push(wrapper_file);
1972
+ result{wrapper_files}.push(wrapper_file);
1973
+ }
1974
+ return file;
1975
+ }
1976
+
1977
+ method verify ( targets, options? ) {
1978
+ let roots := self.dependency_roots(options);
1979
+ let target_root := roots[0];
1980
+ let installed := _list_installed_in_root(target_root);
1981
+ let result := {
1982
+ ok: true,
1983
+ target_root: target_root,
1984
+ targets: [],
1985
+ distributions: [],
1986
+ files: [],
1987
+ checked_files: [],
1988
+ wrapper_files: [],
1989
+ missing_files: [],
1990
+ hash_mismatches: [],
1991
+ size_mismatches: [],
1992
+ skipped_duplicates: [],
1993
+ errors: [],
1994
+ };
1995
+
1996
+ let seen := {};
1997
+ for ( let target in _target_list(targets) ) {
1998
+ let record := _remove_target_record( target, options );
1999
+ if ( record{type} eq "dist" ) {
2000
+ record{type} := "distribution";
2001
+ }
2002
+ record{matches} := [];
2003
+ result{targets}.push(record);
2004
+
2005
+ if (
2006
+ (
2007
+ record{type} ne "module" and
2008
+ record{type} ne "distribution"
2009
+ ) or
2010
+ record{value} eq ""
2011
+ ) {
2012
+ _add_verify_target_error(
2013
+ result,
2014
+ "invalid-target",
2015
+ record,
2016
+ "verify target must be a module or distribution",
2017
+ );
2018
+ next;
2019
+ }
2020
+
2021
+ let matches := record{type} eq "module"
2022
+ ? self._module_remove_matches(
2023
+ installed,
2024
+ record{value},
2025
+ )
2026
+ : self._distribution_remove_matches(
2027
+ installed,
2028
+ record{value},
2029
+ );
2030
+ for ( let match in matches ) {
2031
+ record{matches}.push(
2032
+ {
2033
+ name: match{name},
2034
+ version: match{version},
2035
+ metadata_file: match{metadata_file},
2036
+ },
2037
+ );
2038
+ }
2039
+
2040
+ if ( matches.length() == 0 ) {
2041
+ _add_verify_target_error(
2042
+ result,
2043
+ "missing-target",
2044
+ record,
2045
+ "verify target is not installed",
2046
+ );
2047
+ next;
2048
+ }
2049
+ if ( record{type} eq "module" and matches.length() > 1 ) {
2050
+ _add_verify_target_error(
2051
+ result,
2052
+ "ambiguous-target",
2053
+ record,
2054
+ "module target has multiple owners",
2055
+ );
2056
+ result{errors}[ result{errors}.length() - 1 ]{matches} :=
2057
+ record{matches};
2058
+ next;
2059
+ }
2060
+
2061
+ for ( let dist in matches ) {
2062
+ let key := dist{metadata_file};
2063
+ if ( seen.exists(key) ) {
2064
+ result{skipped_duplicates}.push(
2065
+ {
2066
+ target: record,
2067
+ name: dist{name},
2068
+ version: dist{version},
2069
+ metadata_file: dist{metadata_file},
2070
+ },
2071
+ );
2072
+ next;
2073
+ }
2074
+ seen.add( key, true );
2075
+ result{distributions}.push(
2076
+ {
2077
+ name: dist{name},
2078
+ version: dist{version},
2079
+ metadata_file: dist{metadata_file},
2080
+ },
2081
+ );
2082
+ for ( let module in dist{installed}{modules} ) {
2083
+ self._verify_module_file(
2084
+ result,
2085
+ dist,
2086
+ module,
2087
+ );
2088
+ }
2089
+ for ( let script in dist{installed}{scripts} ) {
2090
+ self._verify_script_file(
2091
+ result,
2092
+ dist,
2093
+ script,
2094
+ );
2095
+ }
2096
+ }
2097
+ }
2098
+
2099
+ result{ok} := (
2100
+ result{errors}.length() == 0 and
2101
+ result{missing_files}.length() == 0 and
2102
+ result{hash_mismatches}.length() == 0 and
2103
+ result{size_mismatches}.length() == 0
2104
+ );
2105
+ return result;
2106
+ }
2107
+
2108
+ method latest ( module_name, options? ) {
2109
+ let module := _latest_module_name(module_name);
2110
+ let base := _trim_right_slash(
2111
+ "" _ _opt( options, "base_url", base_url ),
2112
+ );
2113
+ let url := base _ "/module/" _ module _ ".json";
2114
+ let ua := _opt( options, "user_agent", self._user_agent() );
2115
+ let req := ua.build_request( "GET", url );
2116
+ _progress( options, "downloading latest metadata " _ url );
2117
+ let response := ua.send(req);
2118
+ response.expect_success();
2119
+ let remote_url := url;
2120
+ if ( response can "url" ) {
2121
+ remote_url := response.url();
2122
+ }
2123
+ _progress( options, "received latest metadata " _ remote_url );
2124
+
2125
+ let metadata := _validate_source_metadata(
2126
+ response.json(),
2127
+ remote_url,
2128
+ );
2129
+ if ( not metadata.exists("status") ) {
2130
+ metadata{status} := "stable";
2131
+ }
2132
+ if (
2133
+ metadata.exists("status") and
2134
+ metadata{status} eq "trial"
2135
+ ) {
2136
+ die(
2137
+ "Trial distributions are not available through " _
2138
+ "module-name latest endpoints"
2139
+ );
2140
+ }
2141
+
2142
+ let installed := self.find_installed_module( module, options );
2143
+ let installed_version := installed instanceof Null
2144
+ ? null
2145
+ : installed{version};
2146
+ let status := _latest_status(
2147
+ installed_version,
2148
+ metadata{version},
2149
+ );
2150
+
2151
+ return {
2152
+ ok: true,
2153
+ module_name: module,
2154
+ status: status,
2155
+ can_upgrade: status eq "outdated",
2156
+ installed_version: installed_version,
2157
+ remote_version: metadata{version},
2158
+ remote_distribution: metadata{name},
2159
+ remote_status: metadata{status},
2160
+ url: url,
2161
+ remote_url: remote_url,
2162
+ installed: installed,
2163
+ remote_metadata: metadata,
2164
+ };
2165
+ }
2166
+
2167
+ method can_upgrade ( module_name, options? ) {
2168
+ return self.latest( module_name, options );
2169
+ }
2170
+
2171
+ method fetch_source ( target, options? ) {
2172
+ let value := "" _ target;
2173
+ let file := new Path(value);
2174
+ if ( file.exists() and file.is_file() ) {
2175
+ _progress( options, "using local archive " _ file.to_String() );
2176
+ let source := {
2177
+ type: "file",
2178
+ value: value,
2179
+ path: file.to_String(),
2180
+ path_obj: file,
2181
+ };
2182
+ _check_expected_source_sha(
2183
+ source,
2184
+ _opt( options, "expected_sha256", null ),
2185
+ );
2186
+ return source;
2187
+ }
2188
+
2189
+ let source_type := _is_url(value) ? "url" : "module";
2190
+ let url := value;
2191
+ if ( source_type eq "module" ) {
2192
+ let base := _trim_right_slash(
2193
+ "" _ _opt( options, "base_url", base_url ),
2194
+ );
2195
+ url := base _ "/module/" _ value;
2196
+ }
2197
+
2198
+ let cache_dir := _opt( options, "cache_dir", null );
2199
+ if ( not( cache_dir instanceof Null ) ) {
2200
+ let paths := _cache_paths( cache_dir, url );
2201
+ let cached := _validate_cache_entry(paths);
2202
+ if ( cached ) {
2203
+ _progress(
2204
+ options,
2205
+ "using cached archive for " _ value _ " from " _
2206
+ paths{archive}.to_String(),
2207
+ );
2208
+ let source := {
2209
+ type: source_type,
2210
+ value: value,
2211
+ url: url,
2212
+ resolved_url: cached{resolved_url},
2213
+ path: paths{archive}.to_String(),
2214
+ path_obj: paths{archive},
2215
+ cache_hit: true,
2216
+ cache_sidecar: paths{sidecar}.to_String(),
2217
+ };
2218
+ _check_expected_source_sha(
2219
+ source,
2220
+ _opt( options, "expected_sha256", null ),
2221
+ );
2222
+ return source;
2223
+ }
2224
+ _delete_cache_entry(paths);
2225
+ }
2226
+
2227
+ let temp_dir := _new_temp_dir( options, "zuzuzoo-source-" );
2228
+ let temp_path := temp_dir.child("source.archive");
2229
+ let ua := _opt( options, "user_agent", self._user_agent() );
2230
+ let req := ua.build_request( "GET", url )
2231
+ .download_to( temp_path.to_String() );
2232
+ _progress( options, "downloading " _ value _ " from " _ url );
2233
+ let response := ua.send(req);
2234
+
2235
+ let download_url := url;
2236
+ if ( not _response_success(response) and source_type eq "module" ) {
2237
+ _progress(
2238
+ options,
2239
+ "module archive download failed from " _ url _ " (" _
2240
+ _response_status_text(response) _
2241
+ "); checking latest metadata",
2242
+ );
2243
+ _cleanup_path(temp_path);
2244
+
2245
+ let latest_info := self.latest( value, options );
2246
+ let archive_url := _archive_url_from_latest(latest_info);
2247
+ req := ua.build_request( "GET", archive_url )
2248
+ .download_to( temp_path.to_String() );
2249
+ _progress(
2250
+ options,
2251
+ "downloading " _ value _ " from " _ archive_url,
2252
+ );
2253
+ response := ua.send(req);
2254
+ download_url := archive_url;
2255
+ }
2256
+
2257
+ if ( not _response_success(response) ) {
2258
+ _cleanup_path(temp_dir);
2259
+ die(
2260
+ "Source download failed (target=" _ value _
2261
+ ", source_type=" _ source_type _
2262
+ ", url=" _ download_url _
2263
+ "): HTTP request failed (" _
2264
+ _response_status_text(response) _ ")"
2265
+ );
2266
+ }
2267
+
2268
+ let resolved_url := download_url;
2269
+ if ( response can "url" ) {
2270
+ resolved_url := response.url();
2271
+ }
2272
+ _progress(
2273
+ options,
2274
+ "downloaded " _ value _ " from " _ resolved_url _ " to " _
2275
+ temp_path.to_String(),
2276
+ );
2277
+
2278
+ let source := {
2279
+ type: source_type,
2280
+ value: value,
2281
+ url: url,
2282
+ resolved_url: resolved_url,
2283
+ path: temp_path.to_String(),
2284
+ path_obj: temp_path,
2285
+ temp_dir: temp_dir,
2286
+ };
2287
+ _check_expected_source_sha(
2288
+ source,
2289
+ _opt( options, "expected_sha256", null ),
2290
+ );
2291
+
2292
+ if ( not( cache_dir instanceof Null ) ) {
2293
+ let paths := _cache_paths( cache_dir, url );
2294
+ try {
2295
+ let sidecar := _write_cache_entry(
2296
+ paths,
2297
+ source,
2298
+ temp_path,
2299
+ );
2300
+ source{path} := paths{archive}.to_String();
2301
+ source{path_obj} := paths{archive};
2302
+ source{cache_hit} := false;
2303
+ source{cache_sidecar} := paths{sidecar}.to_String();
2304
+ source{cache_metadata} := sidecar;
2305
+ _progress(
2306
+ options,
2307
+ "cached " _ value _ " at " _
2308
+ paths{archive}.to_String(),
2309
+ );
2310
+ _cleanup_path(temp_dir);
2311
+ }
2312
+ catch ( Exception e ) {
2313
+ _cleanup_path(temp_dir);
2314
+ die _corrupt_archive_error( source, e );
2315
+ }
2316
+ }
2317
+
2318
+ return source;
2319
+ }
2320
+
2321
+ method load_distribution ( target, options? ) {
2322
+ let source := null;
2323
+ let work_dir := null;
2324
+ try {
2325
+ source := self.fetch_source( target, options );
2326
+ let source_path := new Path( source{path} );
2327
+ let archive := null;
2328
+ try {
2329
+ archive := Archive.decode( source_path.slurp() );
2330
+ }
2331
+ catch ( Exception e ) {
2332
+ die _corrupt_archive_error( source, e );
2333
+ }
2334
+ let root_name := _safe_archive_root( archive, source{path} );
2335
+ work_dir := _new_temp_dir( options, "zuzuzoo-work-" );
2336
+ let root_dir := work_dir.child(root_name);
2337
+ root_dir.mkdir();
2338
+ _progress(
2339
+ options,
2340
+ "extracting " _ source{path} _ " to " _
2341
+ work_dir.to_String(),
2342
+ );
2343
+ _extract_archive( archive, root_name, root_dir );
2344
+
2345
+ let metadata_file := root_dir.child("zuzu-distribution.json");
2346
+ if ( not metadata_file.exists() ) {
2347
+ die(
2348
+ `Invalid archive ${source{path}}: ` _
2349
+ "missing zuzu-distribution.json"
2350
+ );
2351
+ }
2352
+ let codec := new JSON( pairlists: true );
2353
+ let metadata := _validate_source_metadata(
2354
+ codec.load(metadata_file),
2355
+ metadata_file.to_String(),
2356
+ );
2357
+
2358
+ let expected_root := metadata{name} _ "-" _ metadata{version};
2359
+ if ( root_name ne expected_root ) {
2360
+ die(
2361
+ `Invalid archive ${source{path}}: root ${root_name} ` _
2362
+ `does not match ${expected_root}`
2363
+ );
2364
+ }
2365
+
2366
+ let build_result := null;
2367
+ let build_file := root_dir.child("Build.zzs");
2368
+ if ( build_file.exists() and build_file.is_file() ) {
2369
+ _progress(
2370
+ options,
2371
+ "building " _ metadata{name} _ " " _
2372
+ metadata{version} _ " with Build.zzs",
2373
+ );
2374
+ build_result := Proc.run(
2375
+ _opt( options, "zuzu_command", zuzu_command ),
2376
+ [ "Build.zzs" ],
2377
+ {
2378
+ cwd: root_dir.to_String(),
2379
+ capture_stdout: true,
2380
+ capture_stderr: true,
2381
+ },
2382
+ );
2383
+ if ( not Proc.is_success(build_result) ) {
2384
+ die(
2385
+ "Build.zzs failed: " _
2386
+ Proc.status_text(build_result)
2387
+ );
2388
+ }
2389
+ metadata := _validate_source_metadata(
2390
+ codec.load(metadata_file),
2391
+ metadata_file.to_String(),
2392
+ );
2393
+ let post_build_root := metadata{name} _ "-" _
2394
+ metadata{version};
2395
+ if ( root_name ne post_build_root ) {
2396
+ die(
2397
+ `Invalid archive ${source{path}}: root ${root_name} ` _
2398
+ `does not match ${post_build_root}`
2399
+ );
2400
+ }
2401
+ }
2402
+
2403
+ let module_sources := _discover_files(
2404
+ root_dir,
2405
+ "modules",
2406
+ ".zzm",
2407
+ );
2408
+ let script_sources := _discover_scripts(root_dir);
2409
+ let tests := _discover_files( root_dir, "tests", ".zzs" );
2410
+ let modules := module_sources.map( function ( source ) {
2411
+ return {
2412
+ source: source,
2413
+ install_as: _module_install_name(source),
2414
+ };
2415
+ } );
2416
+ let scripts := script_sources.map( function ( source ) {
2417
+ return {
2418
+ source: source,
2419
+ install_as: _script_install_name(source),
2420
+ };
2421
+ } );
2422
+
2423
+ let loaded := {
2424
+ source: source,
2425
+ archive: archive,
2426
+ work_dir: work_dir.to_String(),
2427
+ root: root_dir.to_String(),
2428
+ work_dir_obj: work_dir,
2429
+ root_obj: root_dir,
2430
+ root_name: root_name,
2431
+ metadata: metadata,
2432
+ modules: modules,
2433
+ scripts: scripts,
2434
+ tests: tests,
2435
+ build: build_result,
2436
+ };
2437
+ if ( not _opt( options, "keep_work_dirs", false ) ) {
2438
+ _cleanup_source( source, options );
2439
+ _cleanup_path(work_dir);
2440
+ }
2441
+ return loaded;
2442
+ }
2443
+ catch ( Exception e ) {
2444
+ _cleanup_source( source, options );
2445
+ _cleanup_path(work_dir)
2446
+ if not _opt( options, "keep_work_dirs", false );
2447
+ throw e;
2448
+ }
2449
+ }
2450
+
2451
+ method dependency_roots ( options? ) {
2452
+ let cfg := self.config();
2453
+ let target := _root_from_config( "target", "target", cfg );
2454
+ let roots := [];
2455
+ let seen := {};
2456
+ _add_root( roots, seen, target );
2457
+
2458
+ if ( not cfg{global} and not cfg{windows} ) {
2459
+ let global_override := _opt(
2460
+ options,
2461
+ "global_root",
2462
+ global_root,
2463
+ );
2464
+ let global_dependency_root := null;
2465
+ if ( not( global_override instanceof Null ) ) {
2466
+ global_dependency_root := _root_override(
2467
+ global_override,
2468
+ "global",
2469
+ "global",
2470
+ "global",
2471
+ );
2472
+ }
2473
+ else {
2474
+ let global_cfg := new Zuzuzoo(
2475
+ global: true,
2476
+ windows: false,
2477
+ home: home,
2478
+ userprofile: userprofile,
2479
+ ).config();
2480
+ global_dependency_root := _root_from_config(
2481
+ "global",
2482
+ "global",
2483
+ global_cfg,
2484
+ );
2485
+ }
2486
+ _add_root(
2487
+ roots,
2488
+ seen,
2489
+ global_dependency_root,
2490
+ );
2491
+ }
2492
+
2493
+ let user_cfg := new Zuzuzoo(
2494
+ global: false,
2495
+ windows: cfg{windows},
2496
+ home: home,
2497
+ userprofile: userprofile,
2498
+ ).config();
2499
+ _add_root(
2500
+ roots,
2501
+ seen,
2502
+ _root_from_config( "user", "user", user_cfg ),
2503
+ );
2504
+
2505
+ let ctor_roots := dependency_roots instanceof Null
2506
+ ? []
2507
+ : dependency_roots;
2508
+ for ( let root in ctor_roots ) {
2509
+ let where := (
2510
+ root instanceof Dict and root.exists("name")
2511
+ )
2512
+ ? root{name}
2513
+ : "constructor";
2514
+ _add_root(
2515
+ roots,
2516
+ seen,
2517
+ _custom_root( root, where ),
2518
+ );
2519
+ }
2520
+
2521
+ let option_roots := _opt( options, "dependency_roots", [] );
2522
+ for ( let root in option_roots ) {
2523
+ let where := (
2524
+ root instanceof Dict and root.exists("name")
2525
+ )
2526
+ ? root{name}
2527
+ : "options";
2528
+ _add_root(
2529
+ roots,
2530
+ seen,
2531
+ _custom_root( root, where ),
2532
+ );
2533
+ }
2534
+
2535
+ return roots;
2536
+ }
2537
+
2538
+ method find_dependency ( module_name, min_version?, options? ) {
2539
+ let planned := _opt( options, "planned_installs", [] );
2540
+ return _find_dependency_for_plan(
2541
+ planned,
2542
+ self.dependency_roots(options),
2543
+ module_name,
2544
+ min_version instanceof Null ? "0" : min_version,
2545
+ );
2546
+ }
2547
+
2548
+ method _add_removal ( removals, removal_seen, dist, root, reason ) {
2549
+ let key := dist{metadata_file};
2550
+ if ( removal_seen.exists(key) ) {
2551
+ removal_seen.get(key){reasons}.push(reason);
2552
+ return false;
2553
+ }
2554
+ let removal := {
2555
+ name: dist{name},
2556
+ version: dist{version},
2557
+ metadata_file: dist{metadata_file},
2558
+ root: root,
2559
+ reasons: [ reason ],
2560
+ distribution: dist,
2561
+ };
2562
+ removal_seen.add( key, removal );
2563
+ removals.push(removal);
2564
+ return true;
2565
+ }
2566
+
2567
+ method _plan_target_root_removals (
2568
+ plan,
2569
+ target_installed,
2570
+ target_root
2571
+ ) {
2572
+ let removal_seen := {};
2573
+ for ( let install_action in plan{installs} ) {
2574
+ for ( let dist in target_installed ) {
2575
+ if ( dist{name} eq install_action{metadata}{name} ) {
2576
+ let reason := dist{version} eq install_action{metadata}{version}
2577
+ ? "reinstall"
2578
+ : "prior-version";
2579
+ self._add_removal(
2580
+ plan{removals},
2581
+ removal_seen,
2582
+ dist,
2583
+ target_root,
2584
+ reason,
2585
+ );
2586
+ }
2587
+ }
2588
+ }
2589
+
2590
+ for ( let install_action in plan{installs} ) {
2591
+ for ( let module in install_action{modules} ) {
2592
+ let destination := _path_join(
2593
+ target_root{lib_dir},
2594
+ module{install_as},
2595
+ target_root{windows},
2596
+ );
2597
+ for ( let dist in target_installed ) {
2598
+ for ( let owned in dist{installed}{modules} ) {
2599
+ if ( owned{install_as} eq module{install_as} ) {
2600
+ plan{ownership_conflicts}.push(
2601
+ {
2602
+ kind: "module",
2603
+ install_as: module{install_as},
2604
+ destination: destination,
2605
+ planned_distribution: install_action{metadata}{name},
2606
+ planned_version: install_action{metadata}{version},
2607
+ owner_distribution: dist{name},
2608
+ owner_version: dist{version},
2609
+ owner_metadata_file: dist{metadata_file},
2610
+ root: target_root,
2611
+ },
2612
+ );
2613
+ self._add_removal(
2614
+ plan{removals},
2615
+ removal_seen,
2616
+ dist,
2617
+ target_root,
2618
+ "owner-conflict",
2619
+ );
2620
+ }
2621
+ }
2622
+ }
2623
+ }
2624
+
2625
+ for ( let script in install_action{scripts} ) {
2626
+ let destination := _path_join(
2627
+ target_root{bin_dir},
2628
+ script{install_as},
2629
+ target_root{windows},
2630
+ );
2631
+ for ( let dist in target_installed ) {
2632
+ for ( let owned in dist{installed}{scripts} ) {
2633
+ if ( owned{install_as} eq script{install_as} ) {
2634
+ plan{ownership_conflicts}.push(
2635
+ {
2636
+ kind: "script",
2637
+ install_as: script{install_as},
2638
+ destination: destination,
2639
+ planned_distribution: install_action{metadata}{name},
2640
+ planned_version: install_action{metadata}{version},
2641
+ owner_distribution: dist{name},
2642
+ owner_version: dist{version},
2643
+ owner_metadata_file: dist{metadata_file},
2644
+ root: target_root,
2645
+ },
2646
+ );
2647
+ self._add_removal(
2648
+ plan{removals},
2649
+ removal_seen,
2650
+ dist,
2651
+ target_root,
2652
+ "owner-conflict",
2653
+ );
2654
+ }
2655
+ }
2656
+ }
2657
+ }
2658
+ }
2659
+ return plan;
2660
+ }
2661
+
2662
+ method _module_remove_matches ( installed, module_name ) {
2663
+ let wanted := _module_key(module_name);
2664
+ let matches := [];
2665
+ for ( let dist in installed ) {
2666
+ for ( let module in dist{installed}{modules} ) {
2667
+ if ( _module_key( module{install_as} ) eq wanted ) {
2668
+ matches.push(dist);
2669
+ last;
2670
+ }
2671
+ }
2672
+ }
2673
+ return matches;
2674
+ }
2675
+
2676
+ method _distribution_remove_matches ( installed, distribution_name ) {
2677
+ let wanted := "" _ distribution_name;
2678
+ let matches := [];
2679
+ for ( let dist in installed ) {
2680
+ matches.push(dist) if dist{name} eq wanted;
2681
+ }
2682
+ return matches;
2683
+ }
2684
+
2685
+ method _remove_owner_map ( installed, target_root ) {
2686
+ let by_path := {};
2687
+ for ( let dist in installed ) {
2688
+ let owner := _owner_record(dist);
2689
+ for ( let module in dist{installed}{modules} ) {
2690
+ _add_owner_for_path(
2691
+ by_path,
2692
+ _path_join(
2693
+ target_root{lib_dir},
2694
+ module{install_as},
2695
+ target_root{windows},
2696
+ ),
2697
+ owner,
2698
+ );
2699
+ }
2700
+ for ( let script in dist{installed}{scripts} ) {
2701
+ _add_owner_for_path(
2702
+ by_path,
2703
+ _path_join(
2704
+ target_root{bin_dir},
2705
+ script{install_as},
2706
+ target_root{windows},
2707
+ ),
2708
+ owner,
2709
+ );
2710
+ for ( let wrapper in script.get( "wrappers", [] ) ) {
2711
+ _add_owner_for_path(
2712
+ by_path,
2713
+ _path_join(
2714
+ target_root{bin_dir},
2715
+ wrapper,
2716
+ target_root{windows},
2717
+ ),
2718
+ owner,
2719
+ );
2720
+ }
2721
+ }
2722
+ }
2723
+ return by_path;
2724
+ }
2725
+
2726
+ method _add_remove_file (
2727
+ plan,
2728
+ file_seen,
2729
+ owner_map,
2730
+ planned_metadata,
2731
+ kind,
2732
+ path,
2733
+ dist,
2734
+ install_as
2735
+ ) {
2736
+ return false if file_seen.exists(path);
2737
+ file_seen.add( path, true );
2738
+
2739
+ let owners := kind eq "metadata"
2740
+ ? [ _owner_record(dist) ]
2741
+ : owner_map.get( path, [ _owner_record(dist) ] );
2742
+ let planned_owners := [];
2743
+ let kept_owners := [];
2744
+ for ( let owner in owners ) {
2745
+ if ( planned_metadata.exists(owner{metadata_file}) ) {
2746
+ planned_owners.push(owner);
2747
+ }
2748
+ else {
2749
+ kept_owners.push(owner);
2750
+ }
2751
+ }
2752
+
2753
+ let blocked := kind ne "metadata" and kept_owners.length() > 0;
2754
+ let file := {
2755
+ kind: kind,
2756
+ path: path,
2757
+ exists: ( new Path(path) ).exists(),
2758
+ owners: owners,
2759
+ planned_owners: planned_owners,
2760
+ kept_owners: kept_owners,
2761
+ blocked: blocked,
2762
+ };
2763
+ file{install_as} := install_as if not( install_as instanceof Null );
2764
+ plan{files}.push(file);
2765
+
2766
+ if ( blocked ) {
2767
+ let conflict := {
2768
+ kind: kind,
2769
+ path: path,
2770
+ install_as: install_as,
2771
+ owners: owners,
2772
+ planned_owners: planned_owners,
2773
+ kept_owners: kept_owners,
2774
+ };
2775
+ plan{shared_file_conflicts}.push(conflict);
2776
+ plan{errors}.push(
2777
+ {
2778
+ code: "shared-file-conflict",
2779
+ message: "file is also owned by a kept distribution",
2780
+ path: path,
2781
+ owners: owners,
2782
+ kept_owners: kept_owners,
2783
+ },
2784
+ );
2785
+ }
2786
+ return true;
2787
+ }
2788
+
2789
+ method _build_remove_files ( plan, installed, target_root ) {
2790
+ let owner_map := self._remove_owner_map( installed, target_root );
2791
+ let planned_metadata := {};
2792
+ for ( let removal in plan{removals} ) {
2793
+ planned_metadata.add( removal{metadata_file}, true );
2794
+ }
2795
+
2796
+ let file_seen := {};
2797
+ for ( let removal in plan{removals} ) {
2798
+ let dist := removal{distribution};
2799
+ for ( let module in dist{installed}{modules} ) {
2800
+ self._add_remove_file(
2801
+ plan,
2802
+ file_seen,
2803
+ owner_map,
2804
+ planned_metadata,
2805
+ "module",
2806
+ _path_join(
2807
+ target_root{lib_dir},
2808
+ module{install_as},
2809
+ target_root{windows},
2810
+ ),
2811
+ dist,
2812
+ module{install_as},
2813
+ );
2814
+ }
2815
+ for ( let script in dist{installed}{scripts} ) {
2816
+ self._add_remove_file(
2817
+ plan,
2818
+ file_seen,
2819
+ owner_map,
2820
+ planned_metadata,
2821
+ "script",
2822
+ _path_join(
2823
+ target_root{bin_dir},
2824
+ script{install_as},
2825
+ target_root{windows},
2826
+ ),
2827
+ dist,
2828
+ script{install_as},
2829
+ );
2830
+ for ( let wrapper in script.get( "wrappers", [] ) ) {
2831
+ self._add_remove_file(
2832
+ plan,
2833
+ file_seen,
2834
+ owner_map,
2835
+ planned_metadata,
2836
+ "wrapper",
2837
+ _path_join(
2838
+ target_root{bin_dir},
2839
+ wrapper,
2840
+ target_root{windows},
2841
+ ),
2842
+ dist,
2843
+ wrapper,
2844
+ );
2845
+ }
2846
+ }
2847
+ }
2848
+
2849
+ for ( let removal in plan{removals} ) {
2850
+ self._add_remove_file(
2851
+ plan,
2852
+ file_seen,
2853
+ owner_map,
2854
+ planned_metadata,
2855
+ "metadata",
2856
+ removal{metadata_file},
2857
+ removal{distribution},
2858
+ null,
2859
+ );
2860
+ }
2861
+
2862
+ plan{files} := plan{files}.sort(_remove_file_cmp);
2863
+ plan{shared_file_conflicts} :=
2864
+ plan{shared_file_conflicts}.sort(
2865
+ fn ( a, b ) -> a{path} cmp b{path},
2866
+ );
2867
+ return plan;
2868
+ }
2869
+
2870
+ method plan_remove ( targets, options? ) {
2871
+ let roots := self.dependency_roots(options);
2872
+ let target_root := roots[0];
2873
+ let installed := _list_installed_in_root(target_root);
2874
+ let plan := {
2875
+ ok: true,
2876
+ target_root: target_root,
2877
+ targets: [],
2878
+ removals: [],
2879
+ files: [],
2880
+ shared_file_conflicts: [],
2881
+ skipped_duplicates: [],
2882
+ errors: [],
2883
+ };
2884
+
2885
+ let removal_seen := {};
2886
+ for ( let target in _target_list(targets) ) {
2887
+ let record := _remove_target_record( target, options );
2888
+ if ( record{type} eq "dist" ) {
2889
+ record{type} := "distribution";
2890
+ }
2891
+ record{matches} := [];
2892
+ plan{targets}.push(record);
2893
+
2894
+ if (
2895
+ (
2896
+ record{type} ne "module" and
2897
+ record{type} ne "distribution"
2898
+ ) or
2899
+ record{value} eq ""
2900
+ ) {
2901
+ plan{errors}.push(
2902
+ {
2903
+ code: "invalid-target",
2904
+ target: record,
2905
+ message: "remove target must be a module or distribution",
2906
+ },
2907
+ );
2908
+ next;
2909
+ }
2910
+
2911
+ let matches := record{type} eq "module"
2912
+ ? self._module_remove_matches(
2913
+ installed,
2914
+ record{value},
2915
+ )
2916
+ : self._distribution_remove_matches(
2917
+ installed,
2918
+ record{value},
2919
+ );
2920
+ for ( let match in matches ) {
2921
+ record{matches}.push(
2922
+ {
2923
+ name: match{name},
2924
+ version: match{version},
2925
+ metadata_file: match{metadata_file},
2926
+ },
2927
+ );
2928
+ }
2929
+
2930
+ if ( matches.length() == 0 ) {
2931
+ plan{errors}.push(
2932
+ {
2933
+ code: "missing-target",
2934
+ target: record,
2935
+ message: "remove target is not installed",
2936
+ },
2937
+ );
2938
+ next;
2939
+ }
2940
+ if ( record{type} eq "module" and matches.length() > 1 ) {
2941
+ plan{errors}.push(
2942
+ {
2943
+ code: "ambiguous-target",
2944
+ target: record,
2945
+ message: "module target has multiple owners",
2946
+ matches: record{matches},
2947
+ },
2948
+ );
2949
+ next;
2950
+ }
2951
+
2952
+ for ( let dist in matches ) {
2953
+ let added := self._add_removal(
2954
+ plan{removals},
2955
+ removal_seen,
2956
+ dist,
2957
+ target_root,
2958
+ record{type} eq "module"
2959
+ ? "requested-module"
2960
+ : "requested-distribution",
2961
+ );
2962
+ if ( not added ) {
2963
+ plan{skipped_duplicates}.push(
2964
+ {
2965
+ target: record,
2966
+ name: dist{name},
2967
+ version: dist{version},
2968
+ metadata_file: dist{metadata_file},
2969
+ },
2970
+ );
2971
+ }
2972
+ }
2973
+ }
2974
+
2975
+ plan{removals} := plan{removals}.sort(_removal_cmp);
2976
+ self._build_remove_files( plan, installed, target_root );
2977
+ plan{ok} := (
2978
+ plan{errors}.length() == 0 and
2979
+ plan{shared_file_conflicts}.length() == 0
2980
+ );
2981
+ return plan;
2982
+ }
2983
+
2984
+ method plan_install ( targets, options? ) {
2985
+ let roots := self.dependency_roots(options);
2986
+ let target_root := roots[0];
2987
+ let plan := {
2988
+ target_root: target_root,
2989
+ dependency_roots: roots,
2990
+ installs: [],
2991
+ removals: [],
2992
+ satisfied_dependencies: [],
2993
+ dependency_graph: {
2994
+ nodes: [],
2995
+ edges: [],
2996
+ },
2997
+ ownership_conflicts: [],
2998
+ };
2999
+
3000
+ let planned := [];
3001
+ let planned_by_name := {};
3002
+ let status_by_name := {};
3003
+ let seen_targets := {};
3004
+ let loaded_work := [];
3005
+
3006
+ function resolve ( target, requested, dependency_of, min_version, stack ) {
3007
+ let target_text := "" _ target;
3008
+ if ( requested ) {
3009
+ return null if seen_targets.exists(target_text);
3010
+ seen_targets.add( target_text, true );
3011
+ }
3012
+ else {
3013
+ let found := _find_dependency_for_plan(
3014
+ planned,
3015
+ roots,
3016
+ target_text,
3017
+ min_version,
3018
+ );
3019
+ if ( not( found instanceof Null ) ) {
3020
+ if (
3021
+ found{source} eq "planned" and
3022
+ status_by_name.get(found{distribution}, "") eq "visiting"
3023
+ ) {
3024
+ die "Dependency cycle detected: " _
3025
+ _cycle_path( stack, target_text );
3026
+ }
3027
+ let satisfied := found;
3028
+ satisfied{requested_by} := dependency_of instanceof Null
3029
+ ? null
3030
+ : dependency_of{metadata}{name};
3031
+ plan{satisfied_dependencies}.push(satisfied);
3032
+ return null;
3033
+ }
3034
+ if ( _stack_contains( stack, target_text ) ) {
3035
+ die "Dependency cycle detected: " _
3036
+ _cycle_path( stack, target_text );
3037
+ }
3038
+ }
3039
+
3040
+ let dist := self.load_distribution(
3041
+ target_text,
3042
+ _copy_options_with( options, "keep_work_dirs", true ),
3043
+ );
3044
+ loaded_work.push(dist);
3045
+ if (
3046
+ dist{source}{type} eq "module" and
3047
+ dist{metadata}.exists("status") and
3048
+ dist{metadata}{status} eq "trial"
3049
+ ) {
3050
+ die(
3051
+ "Trial distributions are not available through " _
3052
+ "module-name endpoints"
3053
+ );
3054
+ }
3055
+ if (
3056
+ not requested and
3057
+ not _loaded_distribution_provides(
3058
+ dist,
3059
+ target_text,
3060
+ min_version,
3061
+ )
3062
+ ) {
3063
+ let dep := {
3064
+ module_name: target_text,
3065
+ min_version: min_version,
3066
+ };
3067
+ _cleanup_source( dist{source}, options );
3068
+ _cleanup_path(dist{work_dir_obj})
3069
+ if not _opt( options, "keep_work_dirs", false );
3070
+ die _dependency_conflict_message(
3071
+ dep,
3072
+ dependency_of,
3073
+ stack,
3074
+ null,
3075
+ dist,
3076
+ );
3077
+ }
3078
+
3079
+ let dist_name := dist{metadata}{name};
3080
+ if ( planned_by_name.exists(dist_name) ) {
3081
+ let existing := planned_by_name.get(dist_name);
3082
+ if (
3083
+ existing{metadata}{version} ne
3084
+ dist{metadata}{version}
3085
+ ) {
3086
+ _cleanup_source( dist{source}, options );
3087
+ _cleanup_path(dist{work_dir_obj})
3088
+ if not _opt( options, "keep_work_dirs", false );
3089
+ if ( requested ) {
3090
+ die _planned_version_conflict_message(
3091
+ dist_name,
3092
+ existing,
3093
+ dist,
3094
+ target_text,
3095
+ );
3096
+ }
3097
+ let dep := {
3098
+ module_name: target_text,
3099
+ min_version: min_version,
3100
+ };
3101
+ die _dependency_conflict_message(
3102
+ dep,
3103
+ dependency_of,
3104
+ stack,
3105
+ existing,
3106
+ dist,
3107
+ );
3108
+ }
3109
+ _cleanup_source( dist{source}, options );
3110
+ _cleanup_path(dist{work_dir_obj})
3111
+ if not _opt( options, "keep_work_dirs", false );
3112
+ return existing;
3113
+ }
3114
+
3115
+ let dependencies := _dependency_entries(dist{metadata});
3116
+ let install_action := {
3117
+ action: "install",
3118
+ target: target_text,
3119
+ requested: requested ? true : false,
3120
+ dependency_of: dependency_of instanceof Null
3121
+ ? null
3122
+ : dependency_of{metadata}{name},
3123
+ source: dist{source},
3124
+ metadata: dist{metadata},
3125
+ modules: dist{modules},
3126
+ scripts: dist{scripts},
3127
+ tests: dist{tests},
3128
+ dependencies: dependencies,
3129
+ target_root: target_root,
3130
+ root: dist{root},
3131
+ work_dir: dist{work_dir},
3132
+ root_obj: dist{root_obj},
3133
+ work_dir_obj: dist{work_dir_obj},
3134
+ };
3135
+ planned.push(install_action);
3136
+ planned_by_name.add( dist_name, install_action );
3137
+ status_by_name.add( dist_name, "visiting" );
3138
+ plan{dependency_graph}{nodes}.push(
3139
+ {
3140
+ name: dist_name,
3141
+ version: dist{metadata}{version},
3142
+ target: target_text,
3143
+ requested: requested ? true : false,
3144
+ },
3145
+ );
3146
+
3147
+ let next_stack := stack;
3148
+ if ( dist{source}{type} eq "module" ) {
3149
+ next_stack := [];
3150
+ for ( let item in stack ) {
3151
+ next_stack.push(item);
3152
+ }
3153
+ next_stack.push(target_text);
3154
+ }
3155
+
3156
+ for ( let dep in dependencies ) {
3157
+ let found := _find_dependency_for_plan(
3158
+ planned,
3159
+ roots,
3160
+ dep{module_name},
3161
+ dep{min_version},
3162
+ );
3163
+ if ( not( found instanceof Null ) ) {
3164
+ if (
3165
+ found{source} eq "planned" and
3166
+ status_by_name.get(found{distribution}, "") eq "visiting"
3167
+ ) {
3168
+ die "Dependency cycle detected: " _
3169
+ _cycle_path( next_stack, dep{module_name} );
3170
+ }
3171
+ let satisfied := found;
3172
+ satisfied{requested_by} := dist_name;
3173
+ plan{satisfied_dependencies}.push(satisfied);
3174
+ plan{dependency_graph}{edges}.push(
3175
+ {
3176
+ from: dist_name,
3177
+ module_name: dep{module_name},
3178
+ min_version: dep{min_version},
3179
+ status: found{source} eq "planned"
3180
+ ? "planned"
3181
+ : "satisfied",
3182
+ to: found{distribution},
3183
+ root: found.exists("root") ? found{root} : null,
3184
+ },
3185
+ );
3186
+ }
3187
+ else {
3188
+ let dep_install := resolve(
3189
+ dep{module_name},
3190
+ false,
3191
+ install_action,
3192
+ dep{min_version},
3193
+ next_stack,
3194
+ );
3195
+ plan{dependency_graph}{edges}.push(
3196
+ {
3197
+ from: dist_name,
3198
+ module_name: dep{module_name},
3199
+ min_version: dep{min_version},
3200
+ status: "planned",
3201
+ to: dep_install instanceof Null
3202
+ ? null
3203
+ : dep_install{metadata}{name},
3204
+ root: target_root,
3205
+ },
3206
+ );
3207
+ }
3208
+ }
3209
+
3210
+ status_by_name.set( dist_name, "done" );
3211
+ plan{installs}.push(install_action);
3212
+ return install_action;
3213
+ }
3214
+
3215
+ try {
3216
+ for ( let target in _target_list(targets) ) {
3217
+ resolve( target, true, null, "0", [] );
3218
+ }
3219
+ }
3220
+ catch ( Exception e ) {
3221
+ _cleanup_loaded_work_dirs(loaded_work, options);
3222
+ throw e;
3223
+ }
3224
+
3225
+ self._plan_target_root_removals(
3226
+ plan,
3227
+ _list_installed_in_root(target_root),
3228
+ target_root,
3229
+ );
3230
+
3231
+ return plan;
3232
+ }
3233
+
3234
+ method run_distribution_tests ( install_action, options? ) {
3235
+ let results := [];
3236
+ let include_dirs := [];
3237
+ let seen_include_dirs := {};
3238
+ let own_modules := _path_child( install_action{root}, "modules" );
3239
+ _push_unique_string(
3240
+ include_dirs,
3241
+ seen_include_dirs,
3242
+ own_modules.to_String(),
3243
+ ) if own_modules.exists() and own_modules.is_dir();
3244
+ for ( let include_dir in _opt( options, "test_include_dirs", [] ) ) {
3245
+ _push_unique_string(
3246
+ include_dirs,
3247
+ seen_include_dirs,
3248
+ include_dir,
3249
+ );
3250
+ }
3251
+
3252
+ for ( let test in install_action{tests} ) {
3253
+ _progress(
3254
+ options,
3255
+ "testing " _ install_action{metadata}{name} _ " " _
3256
+ install_action{metadata}{version} _ ": " _ test,
3257
+ );
3258
+ let argv := include_dirs.map( fn d -> "-I" _ d );
3259
+ argv.push(test);
3260
+ let run_result := Proc.run(
3261
+ _opt( options, "zuzu_command", zuzu_command ),
3262
+ argv,
3263
+ {
3264
+ cwd: install_action{root},
3265
+ capture_stdout: true,
3266
+ capture_stderr: true,
3267
+ },
3268
+ );
3269
+ let parsed := parse_tap(run_result{stdout});
3270
+ results.push(
3271
+ {
3272
+ test: test,
3273
+ ok: _test_ok( parsed, run_result ),
3274
+ status: Proc.status_text(run_result),
3275
+ result: run_result,
3276
+ tap: parsed,
3277
+ stdout: run_result{stdout},
3278
+ stderr: run_result{stderr},
3279
+ },
3280
+ );
3281
+ }
3282
+ let ok := true;
3283
+ for ( let result in results ) {
3284
+ ok := false if not result{ok};
3285
+ }
3286
+ return {
3287
+ ok: ok,
3288
+ tests: results,
3289
+ distribution: install_action{metadata}{name},
3290
+ version: install_action{metadata}{version},
3291
+ };
3292
+ }
3293
+
3294
+ method execute_removal ( removal_action, options? ) {
3295
+ _progress(
3296
+ options,
3297
+ "removing " _ removal_action{name} _ " " _
3298
+ removal_action{version},
3299
+ );
3300
+ let warnings := [];
3301
+ let removed := [];
3302
+ let dist := removal_action{distribution};
3303
+ let root := removal_action{root};
3304
+
3305
+ function remove_file ( path, kind ) {
3306
+ if ( path.exists() ) {
3307
+ path.remove();
3308
+ removed.push(
3309
+ {
3310
+ kind: kind,
3311
+ path: path.to_String(),
3312
+ },
3313
+ );
3314
+ }
3315
+ else {
3316
+ warnings.push(
3317
+ {
3318
+ kind: kind,
3319
+ path: path.to_String(),
3320
+ message: "missing file",
3321
+ },
3322
+ );
3323
+ }
3324
+ }
3325
+
3326
+ for ( let module in dist{installed}{modules} ) {
3327
+ remove_file(
3328
+ _path_child( root{lib_dir}, module{install_as} ),
3329
+ "module",
3330
+ );
3331
+ }
3332
+ for ( let script in dist{installed}{scripts} ) {
3333
+ remove_file(
3334
+ _path_child( root{bin_dir}, script{install_as} ),
3335
+ "script",
3336
+ );
3337
+ for ( let wrapper in script.get( "wrappers", [] ) ) {
3338
+ remove_file(
3339
+ _path_child( root{bin_dir}, wrapper ),
3340
+ "wrapper",
3341
+ );
3342
+ }
3343
+ }
3344
+ remove_file( new Path( removal_action{metadata_file} ), "metadata" );
3345
+
3346
+ return {
3347
+ ok: true,
3348
+ name: removal_action{name},
3349
+ version: removal_action{version},
3350
+ removed: removed,
3351
+ warnings: warnings,
3352
+ };
3353
+ }
3354
+
3355
+ method _install_action ( install_action, installed_at, options? ) {
3356
+ let root := install_action{target_root};
3357
+ let module_records := [];
3358
+ let script_records := [];
3359
+
3360
+ for ( let module in install_action{modules} ) {
3361
+ let source := _path_child(
3362
+ install_action{root},
3363
+ module{source},
3364
+ );
3365
+ let destination := _path_child(
3366
+ root{lib_dir},
3367
+ module{install_as},
3368
+ );
3369
+ _progress(
3370
+ options,
3371
+ "installing module " _ module{install_as} _ " to " _
3372
+ destination.to_String(),
3373
+ );
3374
+ let written := _copy_file_atomic(source, destination, null);
3375
+ module_records.push(
3376
+ {
3377
+ source: module{source},
3378
+ install_as: module{install_as},
3379
+ sha256: written{sha256},
3380
+ size: written{size},
3381
+ },
3382
+ );
3383
+ }
3384
+
3385
+ for ( let script in install_action{scripts} ) {
3386
+ let source := _path_child(
3387
+ install_action{root},
3388
+ script{source},
3389
+ );
3390
+ let destination := _path_child(
3391
+ root{bin_dir},
3392
+ script{install_as},
3393
+ );
3394
+ _progress(
3395
+ options,
3396
+ "installing script " _ script{install_as} _ " to " _
3397
+ destination.to_String(),
3398
+ );
3399
+ let written := _copy_file_atomic(
3400
+ source,
3401
+ destination,
3402
+ root{windows} ? null : 493,
3403
+ );
3404
+
3405
+ let wrappers := [];
3406
+ if ( root{windows} ) {
3407
+ let wrapper_name := _replace_script_suffix(
3408
+ script{install_as},
3409
+ ".cmd",
3410
+ );
3411
+ let wrapper := _path_child( root{bin_dir}, wrapper_name );
3412
+ let script_base := _relative_basename(script{install_as});
3413
+ _progress(
3414
+ options,
3415
+ "installing command wrapper " _ wrapper_name _
3416
+ " to " _ wrapper.to_String(),
3417
+ );
3418
+ _spew_utf8_atomic(
3419
+ wrapper,
3420
+ "@echo off\r\n" _
3421
+ "zuzu \"%~dp0" _ script_base _ "\" %*\r\n",
3422
+ );
3423
+ wrappers.push(wrapper_name);
3424
+ }
3425
+
3426
+ script_records.push(
3427
+ {
3428
+ source: script{source},
3429
+ install_as: script{install_as},
3430
+ sha256: written{sha256},
3431
+ size: written{size},
3432
+ wrappers: wrappers,
3433
+ },
3434
+ );
3435
+ }
3436
+
3437
+ let metadata := _copy_source_metadata(install_action{metadata});
3438
+ metadata{installed} := {
3439
+ zdf: "ZDF-1",
3440
+ lib_dir: root{lib_dir},
3441
+ bin_dir: root{bin_dir},
3442
+ meta_dir: root{meta_dir},
3443
+ installed_at: installed_at,
3444
+ source: _copy_source_record(install_action{source}),
3445
+ modules: module_records,
3446
+ scripts: script_records,
3447
+ };
3448
+
3449
+ let metadata_file := _path_child(
3450
+ root{meta_dir},
3451
+ metadata{name} _ "-" _ metadata{version} _ ".json",
3452
+ );
3453
+ _progress(
3454
+ options,
3455
+ "writing metadata for " _ metadata{name} _ " " _
3456
+ metadata{version} _ " to " _ metadata_file.to_String(),
3457
+ );
3458
+ _atomic_json_write( metadata_file, metadata );
3459
+
3460
+ return {
3461
+ name: metadata{name},
3462
+ version: metadata{version},
3463
+ metadata_file: metadata_file.to_String(),
3464
+ modules: module_records,
3465
+ scripts: script_records,
3466
+ metadata: metadata,
3467
+ };
3468
+ }
3469
+
3470
+ method format_install_plan ( plan ) {
3471
+ let lines := [
3472
+ "Install target:",
3473
+ " lib: " _ plan{target_root}{lib_dir},
3474
+ " bin: " _ plan{target_root}{bin_dir},
3475
+ " meta: " _ plan{target_root}{meta_dir},
3476
+ "",
3477
+ "Removals:",
3478
+ ];
3479
+
3480
+ let removal_lines := [];
3481
+ for ( let removal in plan{removals} ) {
3482
+ removal_lines.push(
3483
+ " - " _ removal{name} _ " " _ removal{version} _
3484
+ " [" _ join(
3485
+ ", ",
3486
+ removal{reasons}.sort( fn ( a, b ) -> a cmp b ),
3487
+ ) _ "]"
3488
+ );
3489
+ }
3490
+ removal_lines := removal_lines.sort( fn ( a, b ) -> a cmp b );
3491
+ if ( removal_lines.length() == 0 ) {
3492
+ lines.push(" none");
3493
+ }
3494
+ else {
3495
+ for ( let line in removal_lines ) {
3496
+ lines.push(line);
3497
+ }
3498
+ }
3499
+
3500
+ lines.push("");
3501
+ lines.push("Installs:");
3502
+ let install_lines := [];
3503
+ for ( let install_action in plan{installs} ) {
3504
+ install_lines.push(
3505
+ " - " _ install_action{metadata}{name} _ " " _
3506
+ install_action{metadata}{version}
3507
+ );
3508
+ for ( let module in install_action{modules} ) {
3509
+ install_lines.push(
3510
+ " module " _ module{install_as} _ " -> " _
3511
+ _path_join(
3512
+ plan{target_root}{lib_dir},
3513
+ module{install_as},
3514
+ plan{target_root}{windows},
3515
+ )
3516
+ );
3517
+ }
3518
+ for ( let script in install_action{scripts} ) {
3519
+ install_lines.push(
3520
+ " script " _ script{install_as} _ " -> " _
3521
+ _path_join(
3522
+ plan{target_root}{bin_dir},
3523
+ script{install_as},
3524
+ plan{target_root}{windows},
3525
+ )
3526
+ );
3527
+ }
3528
+ }
3529
+ if ( install_lines.length() == 0 ) {
3530
+ lines.push(" none");
3531
+ }
3532
+ else {
3533
+ for ( let line in install_lines ) {
3534
+ lines.push(line);
3535
+ }
3536
+ }
3537
+
3538
+ lines.push("");
3539
+ lines.push("Conflicts:");
3540
+ let conflict_lines := [];
3541
+ for ( let conflict in plan{ownership_conflicts} ) {
3542
+ conflict_lines.push(
3543
+ " - " _ conflict{kind} _ " " _
3544
+ conflict{install_as} _ " owned by " _
3545
+ conflict{owner_distribution} _ " " _
3546
+ conflict{owner_version} _ "; replacing with " _
3547
+ conflict{planned_distribution} _ " " _
3548
+ conflict{planned_version} _ "; destination " _
3549
+ conflict{destination} _ "; owner metadata " _
3550
+ conflict{owner_metadata_file}
3551
+ );
3552
+ }
3553
+ conflict_lines := conflict_lines.sort( fn ( a, b ) -> a cmp b );
3554
+ if ( conflict_lines.length() == 0 ) {
3555
+ lines.push(" none");
3556
+ }
3557
+ else {
3558
+ for ( let line in conflict_lines ) {
3559
+ lines.push(line);
3560
+ }
3561
+ }
3562
+
3563
+ return join( "\n", lines ) _ "\n";
3564
+ }
3565
+
3566
+ method format_remove_plan ( plan ) {
3567
+ let lines := [
3568
+ "Remove target:",
3569
+ " lib: " _ plan{target_root}{lib_dir},
3570
+ " bin: " _ plan{target_root}{bin_dir},
3571
+ " meta: " _ plan{target_root}{meta_dir},
3572
+ "",
3573
+ "Removals:",
3574
+ ];
3575
+
3576
+ if ( plan{removals}.length() == 0 ) {
3577
+ lines.push(" none");
3578
+ }
3579
+ else {
3580
+ for ( let removal in plan{removals} ) {
3581
+ lines.push(
3582
+ " - " _ removal{name} _ " " _
3583
+ removal{version}
3584
+ );
3585
+ }
3586
+ }
3587
+
3588
+ lines.push("");
3589
+ lines.push("Files:");
3590
+ if ( plan{files}.length() == 0 ) {
3591
+ lines.push(" none");
3592
+ }
3593
+ else {
3594
+ for ( let file in plan{files} ) {
3595
+ let status := file{blocked}
3596
+ ? "blocked"
3597
+ : ( file{exists} ? "exists" : "missing" );
3598
+ lines.push(
3599
+ " - " _ file{kind} _ " " _
3600
+ file{path} _ " [" _ status _ "]"
3601
+ );
3602
+ }
3603
+ }
3604
+
3605
+ lines.push("");
3606
+ lines.push("Shared file conflicts:");
3607
+ if ( plan{shared_file_conflicts}.length() == 0 ) {
3608
+ lines.push(" none");
3609
+ }
3610
+ else {
3611
+ for ( let conflict in plan{shared_file_conflicts} ) {
3612
+ let kept := conflict{kept_owners}.map(
3613
+ fn o -> o{name} _ " " _ o{version},
3614
+ ).sort( fn ( a, b ) -> a cmp b );
3615
+ lines.push(
3616
+ " - " _ conflict{path} _
3617
+ " kept by " _ join( ", ", kept )
3618
+ );
3619
+ }
3620
+ }
3621
+
3622
+ lines.push("");
3623
+ lines.push("Errors:");
3624
+ if ( plan{errors}.length() == 0 ) {
3625
+ lines.push(" none");
3626
+ }
3627
+ else {
3628
+ for ( let error in plan{errors} ) {
3629
+ lines.push(
3630
+ " - " _ error{code} _ ": " _
3631
+ error{message}
3632
+ );
3633
+ }
3634
+ }
3635
+
3636
+ return join( "\n", lines ) _ "\n";
3637
+ }
3638
+
3639
+ method _install_unlocked ( targets, options? ) {
3640
+ _progress(
3641
+ options,
3642
+ "planning install for " _ join( ", ", _target_list(targets) ),
3643
+ );
3644
+ let plan := self.plan_install( targets, options );
3645
+ let plan_text := self.format_install_plan(plan);
3646
+ STDOUT.print(plan_text) if _opt( options, "print_plan", false );
3647
+
3648
+ if ( _opt( options, "dry_run", false ) ) {
3649
+ _cleanup_plan_work_dirs(plan, options);
3650
+ return {
3651
+ ok: true,
3652
+ dry_run: true,
3653
+ plan: plan,
3654
+ plan_text: plan_text,
3655
+ tests: [],
3656
+ removals: [],
3657
+ installs: [],
3658
+ warnings: [],
3659
+ };
3660
+ }
3661
+
3662
+ let test_results := [];
3663
+ let tests_ok := true;
3664
+ if ( not _opt( options, "no_test", false ) ) {
3665
+ let test_include_dirs := [];
3666
+ let seen_test_include_dirs := {};
3667
+ for ( let install_action in plan{installs} ) {
3668
+ let modules_dir := _path_child(
3669
+ install_action{root},
3670
+ "modules",
3671
+ );
3672
+ _push_unique_string(
3673
+ test_include_dirs,
3674
+ seen_test_include_dirs,
3675
+ modules_dir.to_String(),
3676
+ ) if modules_dir.exists() and modules_dir.is_dir();
3677
+ }
3678
+ for ( let include_dir in _runtime_include_dirs() ) {
3679
+ _push_unique_string(
3680
+ test_include_dirs,
3681
+ seen_test_include_dirs,
3682
+ include_dir,
3683
+ );
3684
+ }
3685
+ for ( let root in self.dependency_roots(options) ) {
3686
+ _push_unique_string(
3687
+ test_include_dirs,
3688
+ seen_test_include_dirs,
3689
+ root{lib_dir},
3690
+ ) if root{lib_dir} ne "";
3691
+ }
3692
+ let test_options := _copy_options_with(
3693
+ options,
3694
+ "test_include_dirs",
3695
+ test_include_dirs,
3696
+ );
3697
+ for ( let install_action in plan{installs} ) {
3698
+ let test_result := self.run_distribution_tests(
3699
+ install_action,
3700
+ test_options,
3701
+ );
3702
+ test_results.push(test_result);
3703
+ tests_ok := false if not test_result{ok};
3704
+ }
3705
+ }
3706
+
3707
+ if ( not tests_ok and not _opt( options, "force", false ) ) {
3708
+ _cleanup_plan_work_dirs(plan, options);
3709
+ return {
3710
+ ok: false,
3711
+ error: "distribution tests failed",
3712
+ dry_run: false,
3713
+ plan: plan,
3714
+ plan_text: plan_text,
3715
+ tests: test_results,
3716
+ removals: [],
3717
+ installs: [],
3718
+ warnings: [],
3719
+ };
3720
+ }
3721
+
3722
+ try {
3723
+ let removals := [];
3724
+ let warnings := [];
3725
+ for ( let removal in plan{removals} ) {
3726
+ let result := self.execute_removal( removal, options );
3727
+ removals.push(result);
3728
+ for ( let warning in result{warnings} ) {
3729
+ warnings.push(warning);
3730
+ }
3731
+ }
3732
+
3733
+ let installs := [];
3734
+ let installed_at := _installed_at();
3735
+ for ( let install_action in plan{installs} ) {
3736
+ installs.push( self._install_action(
3737
+ install_action,
3738
+ installed_at,
3739
+ options,
3740
+ ) );
3741
+ }
3742
+
3743
+ _cleanup_plan_work_dirs(plan, options);
3744
+ return {
3745
+ ok: true,
3746
+ dry_run: false,
3747
+ forced: not tests_ok and _opt( options, "force", false ),
3748
+ plan: plan,
3749
+ plan_text: plan_text,
3750
+ tests: test_results,
3751
+ removals: removals,
3752
+ installs: installs,
3753
+ warnings: warnings,
3754
+ };
3755
+ }
3756
+ catch ( Exception e ) {
3757
+ _cleanup_plan_work_dirs(plan, options);
3758
+ throw e;
3759
+ }
3760
+ }
3761
+
3762
+ method install ( targets, options? ) {
3763
+ let lock := self.acquire_lock( "install", options );
3764
+ try {
3765
+ let result := self._install_unlocked( targets, options );
3766
+ lock.release();
3767
+ return result;
3768
+ }
3769
+ catch ( Exception e ) {
3770
+ lock.release();
3771
+ throw e;
3772
+ }
3773
+ }
3774
+
3775
+ method _remove_unlocked ( targets, options? ) {
3776
+ let plan := self.plan_remove( targets, options );
3777
+ let plan_text := self.format_remove_plan(plan);
3778
+ STDOUT.print(plan_text) if _opt( options, "print_plan", false );
3779
+
3780
+ if ( not plan{ok} ) {
3781
+ return {
3782
+ ok: false,
3783
+ dry_run: false,
3784
+ plan: plan,
3785
+ plan_text: plan_text,
3786
+ removed: [],
3787
+ warnings: [],
3788
+ errors: plan{errors},
3789
+ };
3790
+ }
3791
+
3792
+ if ( _opt( options, "dry_run", false ) ) {
3793
+ return {
3794
+ ok: true,
3795
+ dry_run: true,
3796
+ plan: plan,
3797
+ plan_text: plan_text,
3798
+ removed: [],
3799
+ warnings: [],
3800
+ errors: [],
3801
+ };
3802
+ }
3803
+
3804
+ let removed := [];
3805
+ let warnings := [];
3806
+ for ( let file in plan{files} ) {
3807
+ let path := new Path(file{path});
3808
+ if ( path.exists() ) {
3809
+ path.remove();
3810
+ removed.push(
3811
+ {
3812
+ kind: file{kind},
3813
+ path: file{path},
3814
+ },
3815
+ );
3816
+ }
3817
+ else {
3818
+ warnings.push(
3819
+ {
3820
+ kind: file{kind},
3821
+ path: file{path},
3822
+ message: "missing file",
3823
+ },
3824
+ );
3825
+ }
3826
+ }
3827
+
3828
+ return {
3829
+ ok: true,
3830
+ dry_run: false,
3831
+ plan: plan,
3832
+ plan_text: plan_text,
3833
+ removed: removed,
3834
+ warnings: warnings,
3835
+ errors: [],
3836
+ };
3837
+ }
3838
+
3839
+ method remove ( targets, options? ) {
3840
+ let lock := self.acquire_lock( "remove", options );
3841
+ try {
3842
+ let result := self._remove_unlocked( targets, options );
3843
+ lock.release();
3844
+ return result;
3845
+ }
3846
+ catch ( Exception e ) {
3847
+ lock.release();
3848
+ throw e;
3849
+ }
3850
+ }
3851
+ }
3852
+
3853
+ function _zoo ( options? ) {
3854
+ return new Zuzuzoo(
3855
+ lib_dir: _opt( options, "lib_dir", null ),
3856
+ bin_dir: _opt( options, "bin_dir", null ),
3857
+ meta_dir: _opt( options, "meta_dir", null ),
3858
+ global: _opt( options, "global", false ),
3859
+ windows: _opt( options, "windows", null ),
3860
+ home: _opt( options, "home", null ),
3861
+ userprofile: _opt( options, "userprofile", null ),
3862
+ base_url: _opt( options, "base_url", "https://zuzulang.org" ),
3863
+ user_agent: _opt( options, "user_agent", null ),
3864
+ zuzu_command: _opt( options, "zuzu_command", "zuzu" ),
3865
+ dependency_roots: _opt( options, "dependency_roots", [] ),
3866
+ global_root: _opt( options, "global_root", null ),
3867
+ );
3868
+ }
3869
+
3870
+ function list_installed ( options? ) {
3871
+ return _zoo(options).list_installed(options);
3872
+ }
3873
+
3874
+ function query ( module_name, options? ) {
3875
+ return _zoo(options).query( module_name, options );
3876
+ }
3877
+
3878
+ function query_distribution ( distribution_name, options? ) {
3879
+ return _zoo(options).query_distribution( distribution_name, options );
3880
+ }
3881
+
3882
+ function is_installed ( module_name, min_version?, options? ) {
3883
+ return _zoo(options).is_installed( module_name, min_version );
3884
+ }
3885
+
3886
+ function installed_version ( module_name, options? ) {
3887
+ return _zoo(options).installed_version(module_name);
3888
+ }
3889
+
3890
+ function pretty_json ( value, options? ) {
3891
+ return _zoo(options).pretty_json(value);
3892
+ }
3893
+
3894
+ function format_json ( value, options? ) {
3895
+ return _zoo(options).format_json( value, options );
3896
+ }
3897
+
3898
+ function fetch_source ( target, options? ) {
3899
+ return _zoo(options).fetch_source( target, options );
3900
+ }
3901
+
3902
+ function load_distribution ( target, options? ) {
3903
+ return _zoo(options).load_distribution( target, options );
3904
+ }
3905
+
3906
+ function dependency_roots ( options? ) {
3907
+ return _zoo(options).dependency_roots(options);
3908
+ }
3909
+
3910
+ function find_dependency ( module_name, min_version?, options? ) {
3911
+ return _zoo(options).find_dependency(
3912
+ module_name,
3913
+ min_version,
3914
+ options,
3915
+ );
3916
+ }
3917
+
3918
+ function plan_install ( targets, options? ) {
3919
+ return _zoo(options).plan_install( targets, options );
3920
+ }
3921
+
3922
+ function plan_remove ( targets, options? ) {
3923
+ return _zoo(options).plan_remove( targets, options );
3924
+ }
3925
+
3926
+ function verify ( targets, options? ) {
3927
+ return _zoo(options).verify( targets, options );
3928
+ }
3929
+
3930
+ function latest ( module_name, options? ) {
3931
+ return _zoo(options).latest( module_name, options );
3932
+ }
3933
+
3934
+ function can_upgrade ( module_name, options? ) {
3935
+ return _zoo(options).can_upgrade( module_name, options );
3936
+ }
3937
+
3938
+ function install ( targets, options? ) {
3939
+ return _zoo(options).install( targets, options );
3940
+ }
3941
+
3942
+ function remove ( targets, options? ) {
3943
+ return _zoo(options).remove( targets, options );
3944
+ }
3945
+
3946
+ function run_distribution_tests ( install_action, options? ) {
3947
+ return _zoo(options).run_distribution_tests( install_action, options );
3948
+ }
3949
+
3950
+ function execute_removal ( removal_action, options? ) {
3951
+ return _zoo(options).execute_removal( removal_action, options );
3952
+ }
3953
+
3954
+ function format_install_plan ( plan, options? ) {
3955
+ return _zoo(options).format_install_plan(plan);
3956
+ }
3957
+
3958
+ function format_remove_plan ( plan, options? ) {
3959
+ return _zoo(options).format_remove_plan(plan);
3960
+ }