@amxts/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +18 -23
  2. package/README.ru.md +19 -22
  3. package/as/amxts.d.ts +63 -3
  4. package/as/constants.ts +684 -116
  5. package/as/entities.ts +1905 -1364
  6. package/as/events.ts +1694 -905
  7. package/as/facade.ts +1240 -220
  8. package/as/fetch.ts +1519 -0
  9. package/as/flags.ts +159 -145
  10. package/as/hlds.ts +1071 -0
  11. package/as/hooks.ts +5226 -5379
  12. package/as/kit.ts +15 -5
  13. package/as/lib/check.ts +1 -1
  14. package/as/natives.ts +2658 -2499
  15. package/as/remote.ts +39 -15
  16. package/as/tsconfig.json +6 -2
  17. package/as/vector.ts +12 -9
  18. package/includes/order.txt +22 -29
  19. package/includes/sources.json +0 -8
  20. package/node_modules/assemblyscript/std/assembly/index.d.ts +20 -11
  21. package/package.json +29 -7
  22. package/runtime/deps/assemblyscript/dist/asc.js +86 -16
  23. package/runtime/deps/assemblyscript/dist/assemblyscript.js +29 -29
  24. package/runtime/licenses/MPL-2.0.txt +373 -0
  25. package/runtime/licenses/README.md +5 -1
  26. package/runtime/licenses/curl-COPYING +22 -0
  27. package/runtime/licenses/libssh2-COPYING +43 -0
  28. package/runtime/licenses/mbedTLS-LICENSE +553 -0
  29. package/runtime/natives.txt +108 -105
  30. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +752 -296
  31. package/runtime/patches/wamr-2.4.5-amxts.patch +148 -0
  32. package/scripts/apply-docs.ts +2 -2
  33. package/scripts/auto-imports.ts +2 -2
  34. package/scripts/build-identity.ts +115 -0
  35. package/scripts/build-wasm.ts +101 -50
  36. package/scripts/check.ts +17 -3
  37. package/scripts/client-messages.ts +246 -0
  38. package/scripts/compile-pool.ts +128 -0
  39. package/scripts/compile-worker.ts +23 -0
  40. package/scripts/compile.ts +156 -59
  41. package/scripts/docker-server.ts +13 -3
  42. package/scripts/docs/as/amxts.ts +86 -0
  43. package/scripts/docs/as/facade.ts +688 -188
  44. package/scripts/docs/as/fetch.ts +805 -0
  45. package/scripts/docs/entities.ts +43 -144
  46. package/scripts/docs/events.ts +15 -26
  47. package/scripts/docs/game.ts +68 -22
  48. package/scripts/docs/messages.ts +330 -16
  49. package/scripts/editor-api.ts +101 -0
  50. package/scripts/hlds-events.ts +370 -0
  51. package/scripts/includes.ts +12 -10
  52. package/scripts/player-fields.ts +16 -15
  53. package/scripts/prebuilt.ts +65 -23
  54. package/scripts/prepare.ts +39 -28
  55. package/scripts/project.ts +120 -20
  56. package/scripts/reapi-events.ts +98 -0
  57. package/scripts/shared-modules.ts +85 -23
  58. package/scripts/source-map.ts +242 -0
  59. package/scripts/system.ts +1 -1
  60. package/scripts/typed-commands.ts +263 -0
  61. package/scripts/typed-configs.ts +23 -12
  62. package/scripts/ui.ts +3 -1
  63. package/scripts/upgrade-menus.ts +274 -0
  64. package/scripts/upgrade-names.ts +384 -0
  65. package/scripts/upgrade.ts +830 -0
  66. package/src/cli-api.mjs +37 -4
  67. package/src/system.mjs +74 -3
  68. package/src/testing/compile.ts +8 -5
  69. package/src/testing/natives.ts +199 -5
  70. package/src/testing/network.ts +531 -0
  71. package/src/testing/server.ts +565 -47
  72. package/src/testing/tables.ts +25 -0
  73. package/as/modules/http.ts +0 -184
  74. package/scripts/docs/as/modules/http.ts +0 -66
@@ -162,6 +162,60 @@ index 771ef87..7bdb74d 100644
162
162
  module->float_plt_count++;
163
163
  }
164
164
  }
165
+ diff --git a/core/iwasm/aot/aot_runtime.c b/core/iwasm/aot/aot_runtime.c
166
+ index d2621fb..47178c4 100644
167
+ --- a/core/iwasm/aot/aot_runtime.c
168
+ +++ b/core/iwasm/aot/aot_runtime.c
169
+ @@ -2682,9 +2682,9 @@ aot_call_function(WASMExecEnv *exec_env, AOTFunctionInstance *function,
170
+ }
171
+ #endif
172
+ #if WASM_ENABLE_DUMP_CALL_STACK != 0
173
+ - if (aot_create_call_stack(exec_env)) {
174
+ - aot_dump_call_stack(exec_env, true, NULL, 0);
175
+ - }
176
+ + /* amxts: the host reads the frames (wasm_runtime_take_trap_frames)
177
+ + and prints them its own way, rather than to stdout here */
178
+ + aot_create_call_stack(exec_env);
179
+ #endif
180
+ }
181
+
182
+ @@ -2755,9 +2755,9 @@ aot_call_function(WASMExecEnv *exec_env, AOTFunctionInstance *function,
183
+ }
184
+ #endif
185
+ #if WASM_ENABLE_DUMP_CALL_STACK != 0
186
+ - if (aot_create_call_stack(exec_env)) {
187
+ - aot_dump_call_stack(exec_env, true, NULL, 0);
188
+ - }
189
+ + /* amxts: the host reads the frames (wasm_runtime_take_trap_frames)
190
+ + and prints them its own way, rather than to stdout here */
191
+ + aot_create_call_stack(exec_env);
192
+ #endif
193
+ }
194
+
195
+ @@ -3414,6 +3414,11 @@ aot_call_indirect(WASMExecEnv *exec_env, uint32 tbl_idx, uint32 table_elem_idx,
196
+ #endif
197
+ ret = invoke_native_internal(exec_env, func_ptr, func_type, signature,
198
+ attachment, argv1, argc, argv);
199
+ +#if WASM_ENABLE_DUMP_CALL_STACK != 0
200
+ + /* amxts: the frames of a trap, as aot_call_function keeps them */
201
+ + if (!ret)
202
+ + aot_create_call_stack(exec_env);
203
+ +#endif
204
+ #if WASM_ENABLE_AOT_STACK_FRAME != 0
205
+ /* Free all frames allocated, note that some frames
206
+ may be allocated in AOT code and haven't been
207
+ @@ -3474,6 +3479,11 @@ aot_call_indirect(WASMExecEnv *exec_env, uint32 tbl_idx, uint32 table_elem_idx,
208
+ #endif
209
+ ret = invoke_native_internal(exec_env, func_ptr, func_type, signature,
210
+ attachment, argv, argc, argv);
211
+ +#if WASM_ENABLE_DUMP_CALL_STACK != 0
212
+ + /* amxts: the frames of a trap, as aot_call_function keeps them */
213
+ + if (!ret)
214
+ + aot_create_call_stack(exec_env);
215
+ +#endif
216
+ #if WASM_ENABLE_AOT_STACK_FRAME != 0
217
+ /* Free all frames allocated, note that some frames
218
+ may be allocated in AOT code and haven't been
165
219
  diff --git a/core/iwasm/aot/arch/aot_reloc_x86_32.c b/core/iwasm/aot/arch/aot_reloc_x86_32.c
166
220
  index f7f2421..89ba8b9 100644
167
221
  --- a/core/iwasm/aot/arch/aot_reloc_x86_32.c
@@ -218,6 +272,100 @@ index 060bb2c..28c182d 100644
218
272
  return NULL;
219
273
  }
220
274
  else
275
+ diff --git a/core/iwasm/common/wasm_runtime_common.c b/core/iwasm/common/wasm_runtime_common.c
276
+ index 943b46f..e93f867 100644
277
+ --- a/core/iwasm/common/wasm_runtime_common.c
278
+ +++ b/core/iwasm/common/wasm_runtime_common.c
279
+ @@ -6867,6 +6867,32 @@ wasm_runtime_dump_call_stack_to_buf(wasm_exec_env_t exec_env, char *buf,
280
+
281
+ return 0;
282
+ }
283
+ +
284
+ +/* amxts: the frames the last trap left, innermost first, and forgets them */
285
+ +uint32
286
+ +wasm_runtime_take_trap_frames(WASMModuleInstanceCommon *module_inst,
287
+ + WASMCApiFrame *buffer, uint32 length)
288
+ +{
289
+ + Vector *frames = ((WASMModuleInstance *)module_inst)->frames;
290
+ + uint32 count = 0, i, size;
291
+ +
292
+ + if (!frames)
293
+ + return 0;
294
+ + size = (uint32)bh_vector_size(frames);
295
+ + for (i = 0; i < size; i++) {
296
+ + WASMCApiFrame frame;
297
+ + if (!bh_vector_get(frames, i, &frame))
298
+ + break;
299
+ + if (frame.lp)
300
+ + wasm_runtime_free(frame.lp);
301
+ + if (count < length) {
302
+ + frame.lp = NULL;
303
+ + buffer[count++] = frame;
304
+ + }
305
+ + }
306
+ + bh_vector_destroy(frames);
307
+ + return count;
308
+ +}
309
+ #endif /* end of WASM_ENABLE_DUMP_CALL_STACK */
310
+
311
+ #if WASM_ENABLE_STATIC_PGO != 0
312
+ diff --git a/core/iwasm/compilation/aot_compiler.c b/core/iwasm/compilation/aot_compiler.c
313
+ index 82f70ca..ea501fb 100644
314
+ --- a/core/iwasm/compilation/aot_compiler.c
315
+ +++ b/core/iwasm/compilation/aot_compiler.c
316
+ @@ -1027,6 +1027,15 @@ aot_compile_func(AOTCompContext *comp_ctx, uint32 func_index)
317
+ comp_ctx->builder,
318
+ func_ctx->block_stack.block_list_head->llvm_entry_block);
319
+
320
+ + /* amxts: the frame first - the frame's own bounds check below can trap,
321
+ + and a trap's ip (trap-ip) is read from it; before, that was the frame
322
+ + of the function compiled last, freed, and wamrc crashed */
323
+ + if (comp_ctx->aux_stack_frame_type) {
324
+ + if (!init_comp_frame(comp_ctx, func_ctx, func_index)) {
325
+ + return false;
326
+ + }
327
+ + comp_ctx->aot_frame->frame_ip = frame_ip;
328
+ + }
329
+ if (comp_ctx->aux_stack_frame_type
330
+ && comp_ctx->call_stack_features.frame_per_function) {
331
+ INT_CONST(func_index_ref,
332
+ @@ -1037,11 +1046,6 @@ aot_compile_func(AOTCompContext *comp_ctx, uint32 func_index)
333
+ return false;
334
+ }
335
+ }
336
+ - if (comp_ctx->aux_stack_frame_type) {
337
+ - if (!init_comp_frame(comp_ctx, func_ctx, func_index)) {
338
+ - return false;
339
+ - }
340
+ - }
341
+
342
+ while (frame_ip < frame_ip_end) {
343
+ opcode = *frame_ip++;
344
+ diff --git a/core/iwasm/include/wasm_export.h b/core/iwasm/include/wasm_export.h
345
+ index 81efb8f..2d80cf2 100644
346
+ --- a/core/iwasm/include/wasm_export.h
347
+ +++ b/core/iwasm/include/wasm_export.h
348
+ @@ -2075,6 +2075,20 @@ WASM_RUNTIME_API_EXTERN uint32_t
349
+ wasm_runtime_dump_call_stack_to_buf(wasm_exec_env_t exec_env, char *buf,
350
+ uint32_t len);
351
+
352
+ +/**
353
+ + * amxts: the frames of the last trap - a call that failed - innermost first,
354
+ + * and forgets them, so a later failure that left none reads none.
355
+ + *
356
+ + * @param module_inst the module instance the call failed in
357
+ + * @param buffer where the frames go
358
+ + * @param length how many fit
359
+ + *
360
+ + * @return how many were written
361
+ + */
362
+ +WASM_RUNTIME_API_EXTERN uint32_t
363
+ +wasm_runtime_take_trap_frames(wasm_module_inst_t module_inst,
364
+ + WASMCApiFrame *buffer, uint32_t length);
365
+ +
366
+ /**
367
+ * Get the size required to store the LLVM PGO profile data
368
+ *
221
369
  diff --git a/wamr-compiler/CMakeLists.txt b/wamr-compiler/CMakeLists.txt
222
370
  index 8ee61ca..5a496cd 100644
223
371
  --- a/wamr-compiler/CMakeLists.txt
@@ -360,7 +360,7 @@ export interface DocSource {
360
360
  root: string;
361
361
  }
362
362
 
363
- /** The core's hand-written API: what `~/facade` and the globals give a plugin. */
363
+ /** The core's hand-written API: what `@amxts/core` and the globals give a plugin. */
364
364
  export const CORE_API = [
365
365
  'as/facade.ts',
366
366
  'as/promise.ts',
@@ -371,7 +371,7 @@ export const CORE_API = [
371
371
  'as/os.ts',
372
372
  'as/amxts.d.ts',
373
373
  'as/lib/check.ts',
374
- 'as/modules/http.ts',
374
+ 'as/fetch.ts',
375
375
  ];
376
376
 
377
377
  export function docsFileOf(root: string, file: string): string {
@@ -70,9 +70,9 @@ export const HOOD = new Set([
70
70
  // Cells and raw handlers.
71
71
  'Call CellArray CellBuffer Handler WideHandler NoArgument arg argc argString argText cell cellFloat cells cellsToString cmd cmdWide floatCell handled hook hostIndex nativeFn noOrigin out paint playerIds publicFor putCell readText ret setArg setArgText stringToCells swapTeam text TEXT_MAX',
72
72
  // The kit's.
73
- 'caller cellArrayRows cellsText colorTags createCellArray defineModule destroyCellArray menuColors PawnCall PawnFunction pushCellArrayRow showMenu textCells',
73
+ 'caller callingPlugin cellArrayRows cellsText colorTags createCellArray defineModule destroyCellArray menuColors onPluginStop PawnCall PawnFunction pushCellArrayRow request RequestErrorKind RequestOptions RequestResult showMenu textCells',
74
74
  // The generated listener and flag tables.
75
- 'addGameListener addServerListener removeGameListener removeServerListener GameAnswerMap GameEventMap ServerEventMap HookEntry HookEvent EntvarFlags FlagFamily FlagList flagList FlagStore MemberFlags ACCESS BUTTON DAMAGE EFFECT ENTITY_FLAG HIDE_HUD PHYSICS_FLAG WEAPON_STATE',
75
+ 'addGameListener addServerListener removeGameListener removeServerListener GameAnswerMap GameEventMap ServerEventMap ServerMessageMap protocolMessageNames HookEntry HookEvent EntvarFlags FlagFamily FlagList flagList FlagStore MemberFlags ACCESS BUTTON DAMAGE EFFECT ENTITY_FLAG HIDE_HUD PHYSICS_FLAG WEAPON_STATE',
76
76
  ].flatMap(group => group.split(' ')));
77
77
 
78
78
  // ---------------------------------------------------------------- the table
@@ -0,0 +1,115 @@
1
+ // The build a module and the compiler beside it on a server are of: the
2
+ // version and the commit, as a release's manifest names them -
3
+ // `0.2.0+1bf291c0ab`, SemVer's build metadata.
4
+ //
5
+ // `bun run generate` writes it into the module (runtime/src/embedded.h), and
6
+ // `bun run serverkit` builds amxts-compile with the one read back from there,
7
+ // so the two agree whenever they come from one build. Before it compiles a
8
+ // plugin's source, the module asks the compiler for its build
9
+ // (`amxts-compile --version`) and refuses one of another: the module writes
10
+ // the API a plugin imports, and a compiler of another release reads it with
11
+ // another AssemblyScript, which fails with errors that say nothing of why.
12
+ //
13
+ // A plugin carries an identity of its own: the ABI it was compiled against,
14
+ // which the module checks before it loads the plugin (abiIdentity, below).
15
+ import { spawnSync } from 'node:child_process';
16
+ import { createHash } from 'node:crypto';
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import process from 'node:process';
20
+ import { fileURLToPath } from 'node:url';
21
+ import pkg from '../package.json';
22
+ import * as tracked from './tracked-fs';
23
+
24
+ /**
25
+ * The version the core builds as: its package.json's, or AMXTS_AS_VERSION's.
26
+ * `bun run publish:local --as 0.2.0` stages the packages as another version
27
+ * than the checkout's; the modules it compiles for them, and a module built
28
+ * for a server that runs their plugins, are of that version only with it set.
29
+ */
30
+ export function coreVersion(): string {
31
+ return process.env.AMXTS_AS_VERSION || pkg.version;
32
+ }
33
+
34
+ /** This checkout's build: the version, and the commit when git knows it. */
35
+ export function buildIdentity(): string {
36
+ const git = spawnSync('git', ['rev-parse', '--short=10', 'HEAD'], { encoding: 'utf-8' });
37
+ const commit = git.status === 0 ? git.stdout.trim() : '';
38
+ return commit ? `${coreVersion()}+${commit}` : coreVersion();
39
+ }
40
+
41
+ /** A string the generated module carries (`#define <name> "..."`), or null before `bun run generate`. */
42
+ function moduleDefine(name: string): string | null {
43
+ const header = './runtime/src/embedded.h';
44
+ if (!existsSync(header)) return null;
45
+ return new RegExp(`^#define ${name} "([^"]*)"$`, 'm').exec(readFileSync(header, 'utf-8'))?.[1] ?? null;
46
+ }
47
+
48
+ /** The build the generated module carries, or null before `bun run generate`. */
49
+ export const moduleBuild = () => moduleDefine('AMXTS_BUILD');
50
+
51
+ /** The ABI the generated module loads plugins of, or null before `bun run generate`. */
52
+ export const moduleAbi = () => moduleDefine('AMXTS_ABI');
53
+
54
+ /** The `--define`s that build amxts-compile as of `build`, stamping plugins with `abi`. */
55
+ export function buildDefines(build: string, abi: string): string[] {
56
+ return [`--define=AMXTS_BUILD=${JSON.stringify(build)}`, `--define=AMXTS_ABI=${JSON.stringify(abi)}`];
57
+ }
58
+
59
+ // ---------------------------------------------------------------- the ABI
60
+ //
61
+ // What a plugin and the module have to agree on: the imports the hood
62
+ // declares (`@external("env", ...)` in as/, by name and by the types they
63
+ // take and give) and the natives table wamrc compiles direct calls against
64
+ // (runtime/natives.txt). A plugin of another ABI calls an import the module
65
+ // does not have, or has with another signature, and the server crashes; so
66
+ // the module loads no plugin whose ABI is not its own.
67
+ //
68
+ // It is the version and a hash of those - `0.2.0+abi.1a2b3c4d`. Not the
69
+ // commit: a module from a release and the plugins built with the core from
70
+ // npm, or a module package's prebuilt plugin, are of one release but not
71
+ // built from one checkout. The version tells releases apart; the hash, builds
72
+ // of one version whose imports differ. The same release rebuilt is the same
73
+ // ABI, and a change that is only the facade's own code needs no rebuilt
74
+ // plugins.
75
+
76
+ /**
77
+ * Raised by hand when the module and the facade change how they talk without
78
+ * an import changing its name or its types: what a cell of an event means,
79
+ * an export the module calls, what names a plugin in a shared module's call
80
+ * (2: its run, not its index).
81
+ */
82
+ const ABI_REVISION = 2;
83
+
84
+ /** The custom section of a plugin that holds its ABI: wamrc copies it into the .aot. */
85
+ export const ABI_SECTION = 'amxts.abi';
86
+
87
+ const CORE = resolve(dirname(fileURLToPath(import.meta.url)), '..');
88
+
89
+ // One import: `@external("env", "ent_get") declare function _entGet(id: i32, offset: i32): i32;`.
90
+ const IMPORT = /@external\("env",\s*"([^"]+)"\)\s*(?:export\s+)?declare\s+function\s+\w+\s*\(([^)]*)\)\s*(?::([^;]+))?;/g;
91
+
92
+ /** The imports a file of the hood declares: `ent_get(i32,i32)i32`, the parameters' names left out. */
93
+ export function importsOf(code: string): string[] {
94
+ return [...code.matchAll(IMPORT)].map(([, name, params, result]) => {
95
+ const types = params.split(',').map(param => param.slice(param.indexOf(':') + 1).trim()).filter(Boolean);
96
+ return `${name}(${types.join(',')})${result?.trim() ?? 'void'}`;
97
+ });
98
+ }
99
+
100
+ declare const AMXTS_ABI: string;
101
+
102
+ /**
103
+ * The ABI this core compiles plugins for, which the module it builds loads.
104
+ * amxts-compile on a server has the module's own built in; anywhere else it
105
+ * is read off the hood's files, through the tracked reads, so a cached
106
+ * compile is made again when they change.
107
+ */
108
+ export function abiIdentity(): string {
109
+ if (typeof AMXTS_ABI === 'string') return AMXTS_ABI;
110
+ const plugins = join(CORE, 'as');
111
+ const imports = tracked.readdirSync(plugins).filter(file => file.endsWith('.ts')).flatMap(file => importsOf(tracked.readFileSync(join(plugins, file), 'utf8')));
112
+ const natives = tracked.readFileSync(join(CORE, 'runtime/natives.txt'), 'utf8').split(/\r?\n/).filter(Boolean);
113
+ const hash = createHash('sha256').update([`revision ${ABI_REVISION}`, ...imports.sort(), ...natives.sort()].join('\n')).digest('hex');
114
+ return `${coreVersion()}+abi.${hash.slice(0, 8)}`;
115
+ }
@@ -1,6 +1,5 @@
1
1
  import type { Plugin } from './compile';
2
- import type { PluginNative } from './plugin-natives';
3
- import type { ModulePackage } from './project';
2
+ import type { ModulePackage, Sources } from './project';
4
3
  import { createSocket } from 'node:dgram';
5
4
  // Builds a project's plugins into .aot files the server can load, and
6
5
  // optionally copies them there. The project is the current folder: its
@@ -32,22 +31,24 @@ import { createSocket } from 'node:dgram';
32
31
  // Two compilers, in order: asc turns AssemblyScript into wasm, wamrc turns
33
32
  // wasm into i386 machine code - in the object format of the server's system,
34
33
  // Windows or Linux, whichever this machine is (scripts/system.ts). wamrc is given runtime/natives.txt so that a
35
- // call into a host native compiles to a direct call — 2 ns instead of 28. Both
34
+ // call into a host native compiles to a direct call, far cheaper than WAMR's
35
+ // generic one. Both
36
36
  // must come from the same WAMR release as the module, or the loader reports
37
37
  // "unknown binary version" (CONTRIBUTING.md builds both from one checkout).
38
38
  import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, utimesSync, watch, writeFileSync } from 'node:fs';
39
39
  import { basename, dirname, join, relative, resolve } from 'node:path';
40
40
  import { includePath } from './compile';
41
+ import { compileAll } from './compile-pool';
41
42
  import { followConsoles, projectContainers, runCommand } from './docker-server';
42
43
  import { FileContents, sourceIn, sourcesIn } from './file-contents';
43
44
  import { serverIncludes } from './includes';
44
45
  import { pluginCache } from './plugin-cache';
45
46
  import { includeName } from './plugin-natives';
46
- import { fromRegistry, prebuiltOf } from './prebuilt';
47
+ import { fromRegistry, prebuiltOf, prebuiltSurface } from './prebuilt';
47
48
  import { CORE_DIR, CORE_PLUGINS, loadProject, modulesInUse, pluginList, projectPlugins, sourcesFor } from './project';
48
49
  import { sharedModulesBuild } from './shared-modules';
49
- import { describeSystem, serverSystem, SYSTEM_NAME, WAMRC_PACKAGE, wamrcPath } from './system';
50
- import { c, log, progress, since } from './ui';
50
+ import { describeSystem, serverFolder, serverSystem, SYSTEM_NAME, WAMRC_PACKAGE, wamrcPath } from './system';
51
+ import { c, live, log, progress, since } from './ui';
51
52
 
52
53
  function fail(message: string, hint?: string): void {
53
54
  log.error(message.trim());
@@ -78,8 +79,17 @@ const asc_ = process.env.AMXTS_ASC ?? join(CORE_DIR, 'runtime/deps/assemblyscrip
78
79
  const signatures = process.env.AMXTS_NATIVES ?? join(CORE_DIR, 'runtime/natives.txt');
79
80
  // Where --deploy copies the built plugins: the amxts folder inside a server's
80
81
  // addons. It is per-machine, so it comes from the environment rather than from
81
- // this file, and .env keeps it out of everyone else's way.
82
- const serverDir = process.env.AMXTS_SERVER ?? '';
82
+ // this file, and .env keeps it out of everyone else's way. AMXTS_SERVER may
83
+ // also name a folder above it - the server's own, cstrike, cstrike/addons
84
+ // (src/system.mjs).
85
+ const serverSetting = process.env.AMXTS_SERVER ?? '';
86
+ let serverDir = '';
87
+ try {
88
+ serverDir = serverFolder(serverSetting);
89
+ } catch (error) {
90
+ fail((error as Error).message);
91
+ process.exit(1);
92
+ }
83
93
 
84
94
  // The module's server command that starts every plugin over from disk.
85
95
  const RELOAD_COMMAND = 'amxts_reload';
@@ -159,22 +169,23 @@ for (const plugin of ownPlugins()) {
159
169
  }
160
170
  }
161
171
 
162
- /** The owners of the modules the plugins use, in load order. */
163
- function moduleOwners(): Target[] {
164
- const sources = sourcesFor(CORE_PLUGINS);
172
+ /**
173
+ * The owners of the modules the plugins use, in load order. A save's
174
+ * rebuild reads the sources once (`sources`), for this and what it affects.
175
+ */
176
+ function moduleOwners(sources = sourcesFor(CORE_PLUGINS)): Target[] {
165
177
  return modulesInUse(sources, ownPlugins().map(plugin => plugin.source)).map(pkg => ({ source: sources.ownerSource(pkg), name: pkg.short, pkg }));
166
178
  }
167
179
 
168
180
  /** The modules' owners, in load order, then the project's plugins. */
169
- function allPlugins(): Target[] {
170
- return [...moduleOwners(), ...ownPlugins()];
181
+ function allPlugins(sources: Sources): Target[] {
182
+ return [...moduleOwners(sources), ...ownPlugins()];
171
183
  }
172
184
 
173
185
  /** The plugins a change to these files has to rebuild: every one importing them. */
174
- function affected(files: string[]): Target[] {
175
- const sources = sourcesFor(CORE_PLUGINS);
186
+ function affected(files: string[], sources: Sources): Target[] {
176
187
  const changed = files.map(file => resolve(file));
177
- return allPlugins().filter((plugin) => {
188
+ return allPlugins(sources).filter((plugin) => {
178
189
  const reached = [...sources.reach(join(CORE_PLUGINS, sources.entry(plugin.source)))].map(place => sources.real(place) ?? place);
179
190
  return changed.some(file => reached.includes(file));
180
191
  });
@@ -211,22 +222,30 @@ const includes = new Map<string, string>();
211
222
  * wrote. Named before the first build as after it, or a dev started again
212
223
  * would compile everything again.
213
224
  */
214
- const cache = pluginCache(project.dir, [
225
+ const cacheIncludes = [
215
226
  'amxts',
216
227
  ...[...project.modules.map(pkg => pkg.short), ...ownPlugins().map(plugin => plugin.name)].map(includeName),
217
228
  ...project.modules.flatMap(pkg => (pkg.include ? [basename(pkg.include).replace(/\.inc$/, '')] : [])),
218
229
  ...readdirSync(outDir).filter(file => file.endsWith('.inc')).map(file => file.replace(/\.inc$/, '')),
219
- ]);
230
+ ];
231
+ const cache = pluginCache(project.dir, cacheIncludes);
220
232
 
221
233
  /**
222
234
  * Builds these plugins: a module that comes compiled for this project is
223
235
  * put in place as it came (scripts/prebuilt.ts), one an earlier build kept,
224
- * from the same files, likewise; the rest are compiled. The first build of a
225
- * run says when it has a module to compile. Returns the .aot names, or null
226
- * after printing the error.
236
+ * from the same files, likewise; the rest are compiled, several at once when
237
+ * there is room (scripts/compile-pool.ts). The first build of a run says when
238
+ * it has a module to compile. Returns the .aot names, or null after printing
239
+ * the errors.
227
240
  */
228
- async function compile(plugins: Target[], first: boolean): Promise<string[] | null> {
229
- const sources = sourcesFor(CORE_PLUGINS);
241
+ async function compile(plugins: Target[], first: boolean, sources: Sources): Promise<string[] | null> {
242
+ // Why a module is not taken as it came, said once.
243
+ const said = new Set<string>();
244
+ const say = (why: string, instead: string) => {
245
+ if (said.has(why)) return;
246
+ said.add(why);
247
+ log.info(`${why} - ${instead}`);
248
+ };
230
249
  const planned = plugins.map((each) => {
231
250
  const plugin: Plugin = {
232
251
  source: each.source,
@@ -240,31 +259,53 @@ async function compile(plugins: Target[], first: boolean): Promise<string[] | nu
240
259
  quick,
241
260
  };
242
261
  const prebuilt = each.pkg ? prebuiltOf(each.pkg, target.system, sources) : null;
243
- if (prebuilt && 'why' in prebuilt) log.info(`${prebuilt.why} - compiling it here`);
262
+ if (prebuilt && 'why' in prebuilt) say(prebuilt.why, 'compiling it here');
244
263
  if (prebuilt && 'aot' in prebuilt) return { ...each, plugin, how: 'prebuilt', natives: cache.take(plugin, prebuilt) };
245
264
  return { ...each, plugin, how: 'unchanged', natives: cache.reuse(plugin) };
246
265
  });
266
+ // The surface a plugin compiles a module's proxy from (scripts/shared-modules.ts) comes with it too.
267
+ for (const each of first ? plugins : []) {
268
+ const shipped = each.pkg ? prebuiltSurface(each.pkg, sources) : null;
269
+ if (shipped && 'why' in shipped) say(shipped.why, 'analysing it here');
270
+ }
247
271
  const fresh = planned.filter(each => !each.natives);
248
272
  if (first && fresh.some(each => each.pkg)) {
249
273
  log.info('The first build compiles the modules to machine code: it takes about a minute.');
250
274
  }
251
- // What comes prebuilt or unchanged is taken quietly: only what is compiled shows.
252
-
253
- for (const [i, each] of fresh.entries()) {
254
- const natives: PluginNative[] = [];
255
- const started = performance.now();
256
- const step = progress(`compiling ${c.bold(each.name)} ${c.dim(`${i + 1}/${fresh.length}`)}`);
257
- const problem = await cache.compile(each.plugin, natives).catch((error) => {
258
- step.end();
259
- throw error;
260
- });
261
- step.end(problem ? undefined : `${c.bold(each.name)} ${c.dim(`· ${since(started)}`)}`);
262
- if (problem) {
263
- fail(`${c.bold(each.name)} does not compile:\n${readable(problem)}\n`, keepWatching ? 'The server keeps the last good build.' : undefined);
264
- return null;
265
- }
266
- each.natives = natives;
275
+ // What comes prebuilt or unchanged is taken quietly: only what is compiled
276
+ // shows - a line for each as it is done, and as it starts: in a terminal
277
+ // one line, rewritten, says what is being compiled now.
278
+ const running = new Map<number, number>();
279
+ let step: ReturnType<typeof progress> | null = null;
280
+ const counted = (index: number) => c.dim(`${index + 1}/${fresh.length}`);
281
+ const redraw = (done?: string) => {
282
+ step?.end(done);
283
+ const names = [...running.keys()].map(index => c.bold(fresh[index].name)).join(', ');
284
+ step = running.size ? progress(`compiling ${names} ${counted(Math.max(...running.keys()))}`) : null;
285
+ };
286
+ const compiled = await compileAll(fresh.map(each => each.plugin), {
287
+ dir: project.dir,
288
+ includes: cacheIncludes,
289
+ here: (plugin, natives) => cache.compile(plugin, natives),
290
+ started(index) {
291
+ running.set(index, performance.now());
292
+ if (live) redraw();
293
+ else progress(`compiling ${c.bold(fresh[index].name)} ${counted(index)}`);
294
+ },
295
+ finished(index, { problem }) {
296
+ const done = problem ? undefined : `${c.bold(fresh[index].name)} ${c.dim(`· ${since(running.get(index)!)}`)}`;
297
+ running.delete(index);
298
+ if (live) redraw(done);
299
+ else if (done) log.success(done);
300
+ },
301
+ }).finally(() => step?.end());
302
+
303
+ const failures = compiled.flatMap((each, index) => (each?.problem ? [`${c.bold(fresh[index].name)} does not compile:\n${readable(each.problem)}\n`] : []));
304
+ if (failures.length) {
305
+ fail(failures.join('\n'), keepWatching ? 'The server keeps the last good build.' : undefined);
306
+ return null;
267
307
  }
308
+ for (const [index, each] of fresh.entries()) each.natives = compiled[index]!.natives;
268
309
 
269
310
  for (const each of planned) {
270
311
  includes.set(`${each.name}.aot`, includePath(each.plugin.output, each.natives!));
@@ -401,7 +442,10 @@ async function deployAndReload(built: string[], names: string): Promise<string>
401
442
  return `deployed ${names} - the module reloads them on its own`;
402
443
  }
403
444
 
404
- const reply = await ask(`rcon ${challenge} "${password}" ${RELOAD_COMMAND}`, 5000);
445
+ // Only what changed starts over; a new plugin needs the list read again,
446
+ // which only a reload of them all does.
447
+ const reload = added.length > 0 ? RELOAD_COMMAND : built.map(file => `${RELOAD_COMMAND} ${file.replace(/\.aot$/, '')}`).join('; ');
448
+ const reply = await ask(`rcon ${challenge} "${password}" ${reload}`, 5000);
405
449
 
406
450
  if (reply === null || /bad rcon_password/i.test(reply)) {
407
451
  // Nothing reloaded them, so let the module's watcher see the new files.
@@ -410,8 +454,12 @@ async function deployAndReload(built: string[], names: string): Promise<string>
410
454
  return `deployed ${names} - rcon ${reply === null ? 'got no answer' : 'refused the password'}, the module reloads them on its own`;
411
455
  }
412
456
 
413
- const lines = reply.split('\n').map(line => line.trimEnd()).filter(Boolean);
414
- return [`deployed, the server reloaded ${names}`, ...lines.map(line => c.dim(` ${line}`))].join('\n');
457
+ // The module's own "reloading x" and "loaded x" only repeat the line above;
458
+ // anything else it says - a refusal, an error - is shown.
459
+ const lines = reply.split('\n').map(line => line.trimEnd()).filter(line => line && !/^\[amxts\] (?:reloading|loaded) /.test(line));
460
+ const loaded = built.every(file => reply.includes(`[amxts] loaded ${file}`));
461
+ const head = loaded ? `deployed, the server reloaded ${names}` : `deployed ${names} - the server did not load it all:`;
462
+ return [head, ...lines.map(line => c.dim(` ${line}`))].join('\n');
415
463
  }
416
464
 
417
465
  /**
@@ -454,11 +502,11 @@ function writeList(owners: Target[]) {
454
502
  * the files a rebuild is for. A module a save brings into use is built with
455
503
  * the plugin that uses it.
456
504
  */
457
- async function run(plugins: Target[], verb: string, why = ''): Promise<boolean> {
505
+ async function run(plugins: Target[], verb: string, why = '', sources = sourcesFor(CORE_PLUGINS)): Promise<boolean> {
458
506
  const started = performance.now();
459
- const owners = moduleOwners();
507
+ const owners = moduleOwners(sources);
460
508
  const fresh = owners.filter(owner => !listed?.includes(owner.name) && !plugins.some(plugin => plugin.name === owner.name));
461
- const built = await compile([...fresh, ...plugins], listed === null);
509
+ const built = await compile([...fresh, ...plugins], listed === null, sources);
462
510
  if (!built) return false;
463
511
  writeList(owners);
464
512
 
@@ -478,7 +526,9 @@ async function serverLine(): Promise<string> {
478
526
  const system = SYSTEM_NAME[target.system];
479
527
  if (forDocker) return `the Docker one that mounts this project · ${system}`;
480
528
  if (!serverDir) return `${c.dim('none - AMXTS_SERVER is not set')} · the plugins are for ${system}`;
481
- const folder = serverDir.replace(/\\/g, '/');
529
+ // The folder used, and what AMXTS_SERVER holds when it named one above it.
530
+ const above = serverSetting === serverDir ? '' : c.dim(` (AMXTS_SERVER=${serverSetting.replace(/\\/g, '/')})`);
531
+ const folder = `${serverDir.replace(/\\/g, '/')}${above}`;
482
532
  if (!existsSync(serverDir)) return `${folder} ${c.yellow('is not there')} · the plugins are for ${system}`;
483
533
  const includes = serverIncludes();
484
534
  const rehlds = includes ? existsSync(join(includes, 'reapi.inc')) : project.config?.target !== 'hlds';
@@ -498,7 +548,7 @@ async function header(): Promise<void> {
498
548
  const module = (pkg: ModulePackage) => {
499
549
  const notes = [
500
550
  !fromRegistry(pkg.dir) && 'local',
501
- !used.includes(pkg) && 'no plugin uses it',
551
+ pkg.library ? 'library' : !used.includes(pkg) && 'no plugin uses it',
502
552
  ].filter(Boolean);
503
553
  return `${pkg.short} ${pkg.version}${notes.length ? c.dim(` (${notes.join(', ')})`) : ''}`;
504
554
  };
@@ -560,8 +610,9 @@ async function flush(): Promise<void> {
560
610
 
561
611
  const files = contents.changed([...heard]);
562
612
  heard.clear();
563
- const plugins = affected(files);
564
- if (plugins.length > 0) await run(plugins, 'rebuilt', `${files.map(shown).join(', ')} changed: `).catch(error => fail(String(error)));
613
+ const sources = sourcesFor(CORE_PLUGINS);
614
+ const plugins = affected(files, sources);
615
+ if (plugins.length > 0) await run(plugins, 'rebuilt', `${files.map(shown).join(', ')} changed: `, sources).catch(error => fail(String(error)));
565
616
 
566
617
  building = false;
567
618
  void flush();
package/scripts/check.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  // module's folder, as a CI step:
3
3
  //
4
4
  // - package.json's "amxts" field points at files that are there;
5
- // - the module file declares itself with defineModule, meta.name its name;
5
+ // - the module file declares itself with defineModule, meta.name its name -
6
+ // or, for a library (`"library": true`), has none and compiles;
6
7
  // - README.md and LICENSE are there;
7
8
  // - the natives compile, and include/<name>.inc is what the build writes from
8
9
  // them - not stale after an edit of natives.ts. With `"contract": true`
@@ -18,6 +19,7 @@ import { importedName } from './auto-imports';
18
19
  import { compileToWasm } from './compile';
19
20
  import { includeName, pawnInclude } from './plugin-natives';
20
21
  import { CORE_PLUGINS, loadProject, readDefinition, readPackage, sourcesFor } from './project';
22
+ import { compileAlone } from './shared-modules';
21
23
  import { c, log } from './ui';
22
24
 
23
25
  function fail(message: string): void {
@@ -44,8 +46,9 @@ if (pkg) {
44
46
  passed.push(`"amxts" points at ${[pkg.module, pkg.natives, pkg.include].filter(Boolean).map(file => relative(dir, file!).replace(/\\/g, '/')).join(', ')}`);
45
47
 
46
48
  try {
47
- const definition = readDefinition(pkg.module);
48
- if (!definition) problems.push(`${relative(dir, pkg.module)}: no \`export default defineModule({ meta: { name: "${pkg.short}" }, ... })\``);
49
+ const definition = pkg.library ? null : readDefinition(pkg.module);
50
+ if (pkg.library) passed.push('a library: compiled into each plugin that imports it');
51
+ else if (!definition) problems.push(`${relative(dir, pkg.module)}: no \`export default defineModule({ meta: { name: "${pkg.short}" }, ... })\``);
49
52
  else if (!definition.name) problems.push(`${relative(dir, pkg.module)}: defineModule has no meta.name - "${pkg.short}"`);
50
53
  else passed.push(`defineModule: ${definition.name}${definition.configKey ? `, options under "${definition.configKey}"` : ''}${definition.imports.length ? `, plugins use it as ${definition.imports.map(importedName).join(', ')}` : ''}`);
51
54
  } catch (problem) {
@@ -59,6 +62,17 @@ if (pkg) {
59
62
  const project = loadProject(dir);
60
63
  problems.push(...project.problems);
61
64
  if (pkg.natives && project.problems.length === 0) await checkNatives(pkg);
65
+ if (pkg.library && project.problems.length === 0) await checkLibrary(pkg);
66
+ }
67
+
68
+ async function checkLibrary(library: NonNullable<typeof pkg>) {
69
+ const file = relative(dir, library.module).replace(/\\/g, '/');
70
+ try {
71
+ await compileAlone(CORE_PLUGINS, library.short);
72
+ passed.push(`${file} compiles`);
73
+ } catch (problem) {
74
+ problems.push((problem as Error).message.replace(`~/modules/${library.short}`, file));
75
+ }
62
76
  }
63
77
 
64
78
  async function checkNatives(module: NonNullable<typeof pkg>) {