@tsplc/build 0.0.0-stage → 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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/dist/cli.js +1652 -0
  4. package/dist/cli.js.map +34 -0
  5. package/dist/index.js +1552 -0
  6. package/dist/index.js.map +31 -0
  7. package/dist/libraries.js +128 -0
  8. package/dist/libraries.js.map +10 -0
  9. package/docker/Dockerfile +173 -0
  10. package/docker/Dockerfile.dockerignore +17 -0
  11. package/docker/Dockerfile.source +15 -0
  12. package/docker/build-image +38 -0
  13. package/docker/codesys-build +5 -0
  14. package/docker/config.ts +8 -0
  15. package/docker/control-linux-sl/Dockerfile +38 -0
  16. package/docker/entrypoint.sh +10 -0
  17. package/docker/install/NOTICE.md +21 -0
  18. package/docker/install/core.sh +36 -0
  19. package/docker/install/decode.c +12 -0
  20. package/docker/install/device/Runtime.ico +0 -0
  21. package/docker/install/device/Runtime.png +0 -0
  22. package/docker/install/device/codesyscontrol.devdesc.xml +1502 -0
  23. package/docker/install/extract.py +43 -0
  24. package/docker/install/helper-launcher.c +46 -0
  25. package/docker/install/install.sh +94 -0
  26. package/docker/install/prerequisites.sh +15 -0
  27. package/docker/install/verify.py +133 -0
  28. package/docker/install/verify.sh +34 -0
  29. package/docker/install/wine-ncrypt/COPYING.LIB +502 -0
  30. package/docker/install/wine-ncrypt/README.md +28 -0
  31. package/docker/install/wine-ncrypt/enable-oaep.py +34 -0
  32. package/docker/install/wine-ncrypt/main.c +619 -0
  33. package/docker/install/wine-ncrypt/oaep.patch +11 -0
  34. package/docker/install/wine-signal/README.md +22 -0
  35. package/docker/install/wine-signal/build.sh +10 -0
  36. package/docker/install/wine-signal/check.sh +34 -0
  37. package/docker/install/wine-signal/noop-i386.c +2 -0
  38. package/docker/install/wine-signal/signal-state.c +105 -0
  39. package/docker/install/wine-signal/tests/avx-registers.c +53 -0
  40. package/docker/install/wine-signal/tests/frame-and-handler.c +192 -0
  41. package/docker/install/wine-signal/tests/loader-i386.c +8 -0
  42. package/docker/install/wine-signal/tests/signal-return.c +47 -0
  43. package/docker/install/wine-signal/wine +4 -0
  44. package/docker/runner.ts +131 -0
  45. package/package.json +53 -4
  46. package/python/build.py +655 -0
  47. package/python/codesys_online.py +300 -0
  48. package/python/test_build.py +444 -0
  49. package/python/test_codesys_online.py +170 -0
  50. package/python/test_installer_packages.py +216 -0
  51. package/python/test_installer_verification.py +307 -0
  52. package/python/test_worker.py +142 -0
  53. package/python/worker.py +89 -0
  54. package/runtime/install.sh +74 -0
  55. package/src/__tests__/fixtures/xml/fb-simple.xml +40 -0
  56. package/src/__tests__/fixtures/xml/fb-state-machine.xml +86 -0
  57. package/src/__tests__/fixtures/xml/prg-cyclic.xml +44 -0
  58. package/src/artifacts.ts +77 -0
  59. package/src/build.ts +87 -0
  60. package/src/cli.ts +116 -0
  61. package/src/config.ts +12 -0
  62. package/src/controller-online.ts +143 -0
  63. package/src/controller-session.ts +229 -0
  64. package/src/diagnostics.ts +66 -0
  65. package/src/docker-build.ts +148 -0
  66. package/src/docker-context.ts +35 -0
  67. package/src/docker-run.ts +104 -0
  68. package/src/host/codesys-command.ts +45 -0
  69. package/src/host/codesys-host.ts +145 -0
  70. package/src/host/codesys-worker.ts +345 -0
  71. package/src/host/windows-path.ts +73 -0
  72. package/src/index.ts +26 -0
  73. package/src/io.ts +127 -0
  74. package/src/libraries.ts +166 -0
  75. package/src/package-path.ts +5 -0
  76. package/src/run.ts +133 -0
  77. package/src/runtime-status.ts +21 -0
  78. package/src/sources.ts +248 -0
  79. package/src/st/declarations.ts +229 -0
  80. package/src/st/lexer.ts +125 -0
  81. package/src/st/parse.ts +202 -0
  82. package/src/st/reader.ts +50 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1652 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/build/src/cli.ts
5
+ import { parseArgs } from "util";
6
+ import { deviceTree } from "@tsplc/typegen";
7
+ import { loadHardware, resolveIo } from "@tsplc/typegen/hardware";
8
+ import { controllerFor, loadTsplcConfig, rawBuildConfig } from "@tsplc/config";
9
+
10
+ // packages/build/src/artifacts.ts
11
+ import { copyFile, cp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
12
+ import { resolve } from "node:path";
13
+ import { z } from "zod";
14
+
15
+ // packages/build/src/config.ts
16
+ import {
17
+ configSchema,
18
+ controllerSchema,
19
+ cyclicTasksSchema,
20
+ dockerImage,
21
+ defineBuildConfig,
22
+ resolveConfig,
23
+ loadConfig
24
+ } from "@tsplc/config/build";
25
+
26
+ // packages/build/src/artifacts.ts
27
+ var buildFiles = {
28
+ project: "Application.project",
29
+ output: "runtime/Application.app",
30
+ checksum: "runtime/Application.crc",
31
+ receipt: "build.json",
32
+ devices: "eds",
33
+ archive: "Application.projectarchive"
34
+ };
35
+ var receiptSchema = z.object({
36
+ mode: z.literal("build"),
37
+ application: z.string().regex(/^[A-Za-z_][A-Za-z_0-9]*$/),
38
+ tasks: cyclicTasksSchema.optional()
39
+ });
40
+ var readBuild = async (directory) => {
41
+ const receipt = receiptSchema.safeParse(JSON.parse(await readFile(resolve(directory, buildFiles.receipt), "utf8")));
42
+ if (!receipt.success) {
43
+ throw new Error("CODESYS did not produce a valid build receipt", {
44
+ cause: receipt.error
45
+ });
46
+ }
47
+ for (const file of [buildFiles.project, buildFiles.output, buildFiles.checksum]) {
48
+ const artifact = await stat(resolve(directory, file));
49
+ if (!artifact.isFile() || artifact.size === 0) {
50
+ throw new Error(`CODESYS produced an empty artifact: ${file}`);
51
+ }
52
+ }
53
+ return {
54
+ ...receipt.data,
55
+ directory,
56
+ project: resolve(directory, buildFiles.project),
57
+ output: resolve(directory, buildFiles.output),
58
+ checksum: resolve(directory, buildFiles.checksum)
59
+ };
60
+ };
61
+
62
+ // packages/build/src/docker-build.ts
63
+ import { execFile } from "node:child_process";
64
+ import { randomUUID } from "node:crypto";
65
+ import { cp as cp3, mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile3, rm as rm3 } from "node:fs/promises";
66
+ import { tmpdir as tmpdir2 } from "node:os";
67
+ import { resolve as resolve5 } from "node:path";
68
+ import { promisify } from "node:util";
69
+
70
+ // packages/build/src/docker-context.ts
71
+ import { cp as cp2, mkdir, mkdtemp, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
72
+ import { createRequire } from "node:module";
73
+ import { tmpdir } from "node:os";
74
+ import { dirname, resolve as resolve3 } from "node:path";
75
+
76
+ // packages/build/src/package-path.ts
77
+ import { resolve as resolve2 } from "node:path";
78
+ import { fileURLToPath } from "node:url";
79
+ var packageDirectory = resolve2(fileURLToPath(new URL("..", import.meta.url)));
80
+
81
+ // packages/build/src/docker-context.ts
82
+ async function prepareDockerContext() {
83
+ const context = await mkdtemp(resolve3(tmpdir(), "tsplc-docker-"));
84
+ try {
85
+ for (const directory of ["src", "python", "docker", "runtime"])
86
+ await cp2(resolve3(packageDirectory, directory), resolve3(context, directory), {
87
+ recursive: true
88
+ });
89
+ const configRoot = resolve3(dirname(createRequire(import.meta.url).resolve("@tsplc/config/build")), "..");
90
+ await mkdir(resolve3(context, "shared-config"));
91
+ await cp2(resolve3(configRoot, "src"), resolve3(context, "shared-config/src"), {
92
+ recursive: true
93
+ });
94
+ await cp2(resolve3(configRoot, "templates"), resolve3(context, "shared-config/templates"), {
95
+ recursive: true
96
+ });
97
+ await cp2(resolve3(configRoot, "package.json"), resolve3(context, "shared-config/package.json"));
98
+ await writeFile2(resolve3(context, "package.json"), JSON.stringify({ type: "module", dependencies: { zod: "4.3.6", jiti: "2.7.0" } }));
99
+ return context;
100
+ } catch (cause) {
101
+ await rm2(context, { recursive: true, force: true });
102
+ throw cause;
103
+ }
104
+ }
105
+
106
+ // packages/build/src/diagnostics.ts
107
+ import { z as z2 } from "zod";
108
+ var compilerDiagnosticSchema = z2.object({
109
+ category: z2.string(),
110
+ text: z2.string(),
111
+ severity: z2.string().nullable(),
112
+ project: z2.string().nullable(),
113
+ object: z2.object({
114
+ name: z2.string(),
115
+ guid: z2.string(),
116
+ path: z2.array(z2.string()),
117
+ isApplicationChild: z2.boolean().nullable()
118
+ }).passthrough().nullable(),
119
+ position: z2.string().nullable(),
120
+ positionText: z2.string().nullable(),
121
+ positionOffset: z2.int().nullable(),
122
+ length: z2.int().nullable(),
123
+ prefix: z2.string().nullable(),
124
+ number: z2.int().nullable()
125
+ }).passthrough();
126
+
127
+ class CodesysCompilerError extends Error {
128
+ diagnostics;
129
+ constructor(message, diagnostics) {
130
+ super(message);
131
+ this.diagnostics = diagnostics;
132
+ this.name = "CodesysCompilerError";
133
+ }
134
+ }
135
+ var buildFailureFile = ".codesys/docker/failure.json";
136
+ var buildFailureSchema = z2.strictObject({
137
+ operationId: z2.uuid(),
138
+ message: z2.string(),
139
+ diagnostics: z2.array(compilerDiagnosticSchema)
140
+ });
141
+
142
+ // packages/build/src/docker-run.ts
143
+ import { spawn } from "node:child_process";
144
+ import { createHash } from "node:crypto";
145
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
146
+ import { basename, posix, relative, resolve as resolve4, sep } from "node:path";
147
+ var runDocker = (args, failure) => new Promise((accept, reject) => {
148
+ const child = spawn("docker", args, {
149
+ stdio: ["inherit", process.env["TSPLC_LOG_STDERR"] === "1" ? 2 : "inherit", "inherit"]
150
+ });
151
+ const interrupt = () => child.kill("SIGINT");
152
+ const terminate = () => child.kill("SIGTERM");
153
+ process.on("SIGINT", interrupt);
154
+ process.on("SIGTERM", terminate);
155
+ const cleanup = () => {
156
+ process.off("SIGINT", interrupt);
157
+ process.off("SIGTERM", terminate);
158
+ };
159
+ child.once("error", (cause) => {
160
+ cleanup();
161
+ reject(new Error("Cannot start Docker. Install and start Docker Desktop or Docker Engine", {
162
+ cause
163
+ }));
164
+ });
165
+ child.once("exit", (code, signal) => {
166
+ cleanup();
167
+ if (code === 0) {
168
+ accept();
169
+ } else {
170
+ reject(new Error(failure(signal ?? code)));
171
+ }
172
+ });
173
+ });
174
+ var bindMount = (source, target, readonly = false) => `type=bind,"source=${source.replaceAll('"', '""')}",target=${target}${readonly ? ",readonly" : ""}`;
175
+ async function mountLibraries(libraries, directory) {
176
+ if (!libraries?.length)
177
+ return;
178
+ await mkdir2(resolve4(directory, "libraries"), { recursive: true });
179
+ return Promise.all(libraries.map(async (library) => {
180
+ const data = await readFile2(library.path);
181
+ if (createHash("sha256").update(data).digest("hex") !== library.sha256)
182
+ throw new Error(`Library changed after installation: ${library.title}`);
183
+ const filename = `${library.sha256}-${basename(library.path)}`;
184
+ await writeFile3(resolve4(directory, "libraries", filename), data);
185
+ return { ...library, path: `/input/libraries/${filename}` };
186
+ }));
187
+ }
188
+ var prepareContainerRun = async (config, requestDirectory, options) => {
189
+ await writeFile3(resolve4(requestDirectory, "config.json"), JSON.stringify({
190
+ ...config,
191
+ libraries: options.sources ? await mountLibraries(config.libraries, requestDirectory) : [],
192
+ configFile: undefined,
193
+ docker: undefined,
194
+ codesys: undefined,
195
+ sourceDir: "/sources",
196
+ entry: posix.resolve("/sources", relative(config.sourceDir, config.entry).split(sep).join("/")),
197
+ template: "/template.project",
198
+ outDir: "/output",
199
+ runtime: undefined
200
+ }));
201
+ return [
202
+ "run",
203
+ "--rm",
204
+ "--init",
205
+ ...config.docker?.cpuSet ? ["--cpuset-cpus", config.docker.cpuSet] : [],
206
+ "--network",
207
+ options.network,
208
+ ...process.getuid && process.getgid ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
209
+ ...(options.environment ?? []).flatMap((name) => ["--env", name]),
210
+ "--mount",
211
+ bindMount(requestDirectory, "/input", true),
212
+ ...options.sources ? ["--mount", bindMount(config.sourceDir, "/sources", true)] : [],
213
+ "--mount",
214
+ bindMount(config.template, "/template.project", true),
215
+ "--mount",
216
+ bindMount(config.outDir, "/output")
217
+ ];
218
+ };
219
+
220
+ // packages/build/src/docker-build.ts
221
+ var execute = promisify(execFile);
222
+ var runtimeImage = "tsplc-control-linux-sl:4.22.0.0";
223
+ var buildInDocker = async (config, image) => {
224
+ const requestDirectory = await mkdtemp2(resolve5(tmpdir2(), "codesys-docker-"));
225
+ try {
226
+ const operationId = randomUUID();
227
+ await mkdir3(config.outDir, { recursive: true });
228
+ const failurePath = resolve5(config.outDir, buildFailureFile);
229
+ await rm3(failurePath, { force: true });
230
+ const args = [
231
+ ...await prepareContainerRun(config, requestDirectory, { network: "none", sources: true }),
232
+ image,
233
+ "--request",
234
+ "/input/config.json",
235
+ "--operation-id",
236
+ operationId
237
+ ];
238
+ console.log(`Building with Docker (${image})`);
239
+ try {
240
+ await runDocker(args, (status) => `Docker build failed (${status}). Run tsplc configure if the image is missing; inspect ${config.outDir}/.codesys/docker for compiler logs`);
241
+ } catch (cause) {
242
+ const message = cause instanceof Error ? cause.message : String(cause);
243
+ const failure = await readFile3(failurePath, "utf8").then((text) => buildFailureSchema.parse(JSON.parse(text))).catch((error) => {
244
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
245
+ return;
246
+ throw new AggregateError([cause, error], `${message}. Invalid CODESYS failure metadata (${failurePath})`, { cause: error });
247
+ });
248
+ if (!failure)
249
+ throw cause;
250
+ if (failure.operationId !== operationId)
251
+ throw new Error(`${message}. CODESYS failure metadata belongs to a different Docker build`, {
252
+ cause
253
+ });
254
+ if (cause instanceof Error) {
255
+ cause.message = `${message}
256
+ ${failure.message}`;
257
+ if (failure.diagnostics.length)
258
+ cause.cause = new CodesysCompilerError(failure.message, failure.diagnostics);
259
+ }
260
+ throw cause;
261
+ }
262
+ return await readBuild(config.outDir);
263
+ } finally {
264
+ await rm3(requestDirectory, { recursive: true, force: true });
265
+ }
266
+ };
267
+
268
+ // packages/build/src/host/codesys-worker.ts
269
+ import {
270
+ access,
271
+ copyFile as copyFile2,
272
+ mkdir as mkdir4,
273
+ open,
274
+ readFile as readFile4,
275
+ rename as rename2,
276
+ rm as rm4,
277
+ unlink,
278
+ writeFile as writeFile4
279
+ } from "node:fs/promises";
280
+ import { resolve as resolve6 } from "node:path";
281
+ import { setTimeout as delay } from "node:timers/promises";
282
+ import { z as z4 } from "zod";
283
+
284
+ // packages/build/src/host/codesys-command.ts
285
+ import { commandArgumentSchema } from "@tsplc/config/build";
286
+ var codesysCommand = [
287
+ "@echo off",
288
+ "setlocal DisableDelayedExpansion",
289
+ '"%CODESYS_BUILD_EXE%" --profile="%CODESYS_BUILD_PROFILE%" --noUI --runscript="%CODESYS_BUILD_SCRIPT%"',
290
+ 'set "CODESYS_BUILD_STATUS=%errorlevel%"',
291
+ 'if not "%CODESYS_BUILD_STATUS%"=="0" echo CODESYS Windows exit status: %CODESYS_BUILD_STATUS%',
292
+ "exit /b %CODESYS_BUILD_STATUS%",
293
+ ""
294
+ ].join(`\r
295
+ `);
296
+
297
+ // packages/build/src/host/codesys-host.ts
298
+ import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
299
+ import { promisify as promisify3 } from "node:util";
300
+ import { z as z3 } from "zod";
301
+
302
+ // packages/build/src/host/windows-path.ts
303
+ import { execFile as execFile2 } from "node:child_process";
304
+ import { promisify as promisify2 } from "node:util";
305
+ import { codesysTimeouts } from "@tsplc/config/build";
306
+ var execute2 = promisify2(execFile2);
307
+
308
+ // packages/build/src/host/codesys-host.ts
309
+ var execute3 = promisify3(execFile3);
310
+ var exitSchema = z3.object({
311
+ pid: z3.int().positive(),
312
+ code: z3.int().nullable(),
313
+ signal: z3.string().nullable()
314
+ });
315
+
316
+ // packages/build/src/host/codesys-worker.ts
317
+ var pythonDirectory = resolve6(packageDirectory, "python");
318
+ var sessionSchema = z4.object({
319
+ directory: z4.string(),
320
+ fingerprint: z4.string(),
321
+ pid: z4.int().positive()
322
+ });
323
+ var hasCode = (error, code) => error instanceof Error && ("code" in error) && error.code === code;
324
+ var isProcessAlive = (pid) => {
325
+ try {
326
+ process.kill(pid, 0);
327
+ return true;
328
+ } catch (error) {
329
+ if (hasCode(error, "ESRCH")) {
330
+ return false;
331
+ }
332
+ throw error;
333
+ }
334
+ };
335
+ var withWorkerLock = async (config, run) => {
336
+ await mkdir4(resolve6(config.outDir, ".codesys"), { recursive: true });
337
+ const path = resolve6(config.outDir, ".codesys/command.lock");
338
+ const deadline = Date.now() + config.timeouts.build + config.timeouts.startup;
339
+ while (true) {
340
+ const lock = await open(path, "wx").catch((error) => {
341
+ if (hasCode(error, "EEXIST")) {
342
+ return;
343
+ }
344
+ throw error;
345
+ });
346
+ if (lock) {
347
+ try {
348
+ await lock.writeFile(String(process.pid));
349
+ return await run();
350
+ } finally {
351
+ await lock.close();
352
+ await unlink(path);
353
+ }
354
+ }
355
+ const owner = Number(await readFile4(path, "utf8").catch((error) => {
356
+ if (hasCode(error, "ENOENT")) {
357
+ return "";
358
+ }
359
+ throw error;
360
+ }));
361
+ if (owner > 0 && !isProcessAlive(owner)) {
362
+ throw new Error(`An interrupted command left ${path}. Remove that lock file and retry.`);
363
+ }
364
+ if (Date.now() >= deadline) {
365
+ throw new Error(`Timed out waiting for another CODESYS command (${path})`);
366
+ }
367
+ await delay(200);
368
+ }
369
+ };
370
+
371
+ // packages/build/src/st/parse.ts
372
+ import { basename as basename2 } from "node:path";
373
+
374
+ // packages/build/src/st/declarations.ts
375
+ var objectKinds = {
376
+ PROGRAM: "program",
377
+ FUNCTION_BLOCK: "function_block",
378
+ FUNCTIONBLOCK: "function_block",
379
+ FUNCTION: "function",
380
+ METHOD: "method",
381
+ INTERFACE: "interface",
382
+ PROPERTY: "property"
383
+ };
384
+ var modifiers = new Set([
385
+ "PUBLIC",
386
+ "PRIVATE",
387
+ "PROTECTED",
388
+ "INTERNAL",
389
+ "FINAL",
390
+ "ABSTRACT",
391
+ "OVERRIDE"
392
+ ]);
393
+ var globalSections = new Set(["VAR_GLOBAL", "VAR_CONFIG"]);
394
+ var variableSections = new Set([
395
+ ...globalSections,
396
+ "VAR",
397
+ "VAR_INPUT",
398
+ "VAR_OUTPUT",
399
+ "VAR_IN_OUT",
400
+ "VAR_TEMP",
401
+ "VAR_STAT",
402
+ "VAR_INST",
403
+ "VAR_EXTERNAL",
404
+ "VAR_ACCESS",
405
+ "VAR_GENERIC"
406
+ ]);
407
+ var closingObjects = new Set(Object.keys(objectKinds).map((kind) => `END_${kind}`));
408
+ var declarationKeywords = new Set([
409
+ ...Object.keys(objectKinds),
410
+ ...closingObjects,
411
+ ...variableSections,
412
+ "END_VAR",
413
+ "TYPE",
414
+ "END_TYPE",
415
+ "STRUCT",
416
+ "END_STRUCT",
417
+ "UNION",
418
+ "END_UNION",
419
+ "ACTION",
420
+ "END_ACTION",
421
+ "CONFIGURATION",
422
+ "END_CONFIGURATION"
423
+ ]);
424
+ var conditionalDirective = (token) => {
425
+ if (token?.kind === "pragma") {
426
+ return /^\{\s*(IF|ELSIF|ELSE|END_IF)\b/i.exec(token.value)?.[1]?.toUpperCase();
427
+ }
428
+ };
429
+ var isDeclarationPragma = (token) => token?.kind === "pragma" && !conditionalDirective(token);
430
+ var readDeclarationPragmas = (reader) => {
431
+ while (isDeclarationPragma(reader.peek())) {
432
+ reader.take();
433
+ }
434
+ };
435
+ var readDeclarationSemicolon = (reader) => {
436
+ const token = reader.peek();
437
+ if (token?.value === ";" && !/[\r\n]/.test(reader.source.slice(reader.end, token.start))) {
438
+ reader.take();
439
+ }
440
+ };
441
+ var readIdentifier = (reader) => {
442
+ const token = reader.take();
443
+ if (token.kind !== "word" || declarationKeywords.has(token.value)) {
444
+ reader.fail("Expected an IEC identifier", token);
445
+ }
446
+ return reader.source.slice(token.start, token.end);
447
+ };
448
+ var isOverrideModifier = (reader) => {
449
+ const name = reader.peek(1);
450
+ return name?.kind === "word" && !declarationKeywords.has(name.value) && !["(", "[", ":=", ".", "^"].includes(reader.peek(2)?.value ?? "") && !/[\r\n]/.test(reader.source.slice(reader.peek()?.end, name.start));
451
+ };
452
+ var readModifiers = (reader) => {
453
+ while (modifiers.has(reader.peek()?.value ?? "")) {
454
+ if (reader.peek()?.value === "OVERRIDE" && !isOverrideModifier(reader)) {
455
+ return;
456
+ }
457
+ reader.take();
458
+ }
459
+ };
460
+ var typeDelimiters = { "(": ")", "[": "]", "<": ">" };
461
+ var readDelimitedType = (reader, open) => {
462
+ reader.expect(open);
463
+ const closings = [typeDelimiters[open]];
464
+ while (closings.length) {
465
+ const token = reader.take();
466
+ if (token.kind === "string" || token.kind === "pragma") {
467
+ continue;
468
+ }
469
+ if (token.value === "(" || token.value === "[") {
470
+ closings.push(typeDelimiters[token.value]);
471
+ } else if (token.value === ")" || token.value === "]") {
472
+ if (closings.pop() !== token.value) {
473
+ reader.fail(`Mismatched delimiter ${token.value}`, token);
474
+ }
475
+ } else if (open === "<" && closings.at(-1) === ">") {
476
+ if (token.value === "<") {
477
+ closings.push(">");
478
+ }
479
+ if (token.value === ">") {
480
+ closings.pop();
481
+ }
482
+ }
483
+ if (declarationKeywords.has(token.value) || token.value === ";") {
484
+ reader.fail(`Unclosed ${open}`, token);
485
+ }
486
+ }
487
+ };
488
+ var readType = (reader) => {
489
+ const start = reader.peek()?.start ?? reader.source.length;
490
+ while (true) {
491
+ if (["POINTER", "REFERENCE", "REF_TO"].includes(reader.peek()?.value ?? "")) {
492
+ if (reader.take().value !== "REF_TO") {
493
+ reader.expect("TO");
494
+ }
495
+ continue;
496
+ }
497
+ if (reader.consume("ARRAY")) {
498
+ readDelimitedType(reader, "[");
499
+ reader.expect("OF");
500
+ continue;
501
+ }
502
+ break;
503
+ }
504
+ readIdentifier(reader);
505
+ while (reader.consume(".")) {
506
+ readIdentifier(reader);
507
+ }
508
+ for (const open of ["(", "[", "<"]) {
509
+ if (reader.peek()?.value === open) {
510
+ readDelimitedType(reader, open);
511
+ }
512
+ }
513
+ return reader.source.slice(start, reader.end);
514
+ };
515
+ var readVariableSection = (reader) => {
516
+ reader.take();
517
+ const conditionals = [];
518
+ while (reader.peek()?.value !== "END_VAR") {
519
+ const token = reader.take();
520
+ if (variableSections.has(token.value) || closingObjects.has(token.value)) {
521
+ reader.fail("Missing END_VAR", token);
522
+ }
523
+ const directive = conditionalDirective(token);
524
+ if (!directive) {
525
+ continue;
526
+ }
527
+ if (directive === "IF") {
528
+ conditionals.push({ hasElse: false });
529
+ continue;
530
+ }
531
+ const current = conditionals.at(-1);
532
+ if (!current) {
533
+ return reader.fail("Unmatched conditional pragma", token);
534
+ }
535
+ if (directive === "END_IF") {
536
+ conditionals.pop();
537
+ continue;
538
+ }
539
+ if (current.hasElse) {
540
+ reader.fail("Conditional branch after ELSE", token);
541
+ }
542
+ current.hasElse = directive === "ELSE";
543
+ }
544
+ if (conditionals.length) {
545
+ reader.fail("Conditional pragmas must remain inside one variable section");
546
+ }
547
+ reader.take();
548
+ readDeclarationSemicolon(reader);
549
+ };
550
+ var readDeclarationSections = (reader) => {
551
+ while (reader.peek()) {
552
+ let ahead = 0;
553
+ while (isDeclarationPragma(reader.peek(ahead))) {
554
+ ahead++;
555
+ }
556
+ const next = reader.peek(ahead)?.value ?? "";
557
+ if (variableSections.has(next)) {
558
+ readDeclarationPragmas(reader);
559
+ readVariableSection(reader);
560
+ continue;
561
+ }
562
+ if (next !== "EXTENDS" && next !== "IMPLEMENTS") {
563
+ return;
564
+ }
565
+ readDeclarationPragmas(reader);
566
+ reader.take();
567
+ readType(reader);
568
+ while (reader.consume(",")) {
569
+ readType(reader);
570
+ }
571
+ }
572
+ };
573
+
574
+ // packages/build/src/st/lexer.ts
575
+ class StSyntaxError extends Error {
576
+ constructor(source, offset, filename, message) {
577
+ const lines = source.slice(0, offset).split(/\r\n|\r|\n/);
578
+ super(`${filename}:${lines.length}:${(lines.at(-1)?.length ?? 0) + 1}: ${message}`);
579
+ this.name = "StSyntaxError";
580
+ }
581
+ }
582
+ var tokenize = (source, filename = "<source>") => {
583
+ const tokens = [];
584
+ let offset = 0;
585
+ const fail = (start, message) => {
586
+ throw new StSyntaxError(source, start, filename, message);
587
+ };
588
+ const quoted = () => {
589
+ const start = offset;
590
+ const quote = source[offset++];
591
+ while (offset < source.length) {
592
+ if (source[offset] === "$") {
593
+ offset += 2;
594
+ } else if (source[offset++] === quote) {
595
+ return;
596
+ }
597
+ }
598
+ fail(start, "Unterminated string");
599
+ };
600
+ while (offset < source.length) {
601
+ const start = offset;
602
+ const character = source[offset] ?? "";
603
+ if (/\s|\uFEFF/.test(character)) {
604
+ offset++;
605
+ continue;
606
+ }
607
+ if (source.startsWith("//", offset)) {
608
+ while (offset < source.length && !/[\r\n]/.test(source[offset] ?? "")) {
609
+ offset++;
610
+ }
611
+ continue;
612
+ }
613
+ if (source.startsWith("(*", offset)) {
614
+ offset += 2;
615
+ let depth = 1;
616
+ while (offset < source.length && depth > 0) {
617
+ if (source.startsWith("(*", offset)) {
618
+ depth++;
619
+ offset += 2;
620
+ } else if (source.startsWith("*)", offset)) {
621
+ depth--;
622
+ offset += 2;
623
+ } else {
624
+ offset++;
625
+ }
626
+ }
627
+ if (depth) {
628
+ fail(start, "Unterminated block comment");
629
+ }
630
+ continue;
631
+ }
632
+ if (character === "'" || character === '"') {
633
+ quoted();
634
+ tokens.push({
635
+ kind: "string",
636
+ value: source.slice(start, offset),
637
+ start,
638
+ end: offset
639
+ });
640
+ continue;
641
+ }
642
+ if (character === "{") {
643
+ offset++;
644
+ while (offset < source.length && source[offset] !== "}") {
645
+ if (source[offset] === "'" || source[offset] === '"') {
646
+ quoted();
647
+ } else {
648
+ offset++;
649
+ }
650
+ }
651
+ if (offset === source.length) {
652
+ fail(start, "Unterminated pragma");
653
+ }
654
+ offset++;
655
+ tokens.push({
656
+ kind: "pragma",
657
+ value: source.slice(start, offset),
658
+ start,
659
+ end: offset
660
+ });
661
+ continue;
662
+ }
663
+ if (/[A-Za-z_]/.test(character)) {
664
+ offset++;
665
+ while (/[A-Za-z_0-9]/.test(source[offset] ?? "")) {
666
+ offset++;
667
+ }
668
+ tokens.push({
669
+ kind: "word",
670
+ value: source.slice(start, offset).toUpperCase(),
671
+ start,
672
+ end: offset
673
+ });
674
+ continue;
675
+ }
676
+ offset++;
677
+ const pair = source.slice(start, offset + 1);
678
+ if ([":=", "=>", "..", "<=", ">=", "<>", "**"].includes(pair)) {
679
+ offset++;
680
+ }
681
+ tokens.push({
682
+ kind: "symbol",
683
+ value: source.slice(start, offset),
684
+ start,
685
+ end: offset
686
+ });
687
+ }
688
+ return tokens;
689
+ };
690
+
691
+ // packages/build/src/st/reader.ts
692
+ var createTokenReader = (source, filename) => {
693
+ const tokens = tokenize(source, filename);
694
+ let index = 0;
695
+ const peek = (ahead = 0) => tokens[index + ahead];
696
+ const fail = (message, token = peek()) => {
697
+ throw new StSyntaxError(source, token?.start ?? source.length, filename, message);
698
+ };
699
+ const take = () => {
700
+ const token = peek();
701
+ if (!token) {
702
+ return fail("Unexpected end of declaration");
703
+ }
704
+ index++;
705
+ return token;
706
+ };
707
+ const expect = (value) => {
708
+ if (peek()?.value !== value) {
709
+ fail(`Expected ${value}`);
710
+ }
711
+ return take();
712
+ };
713
+ const consume = (value) => {
714
+ if (peek()?.value === value) {
715
+ return take();
716
+ }
717
+ };
718
+ return {
719
+ source,
720
+ filename,
721
+ tokens,
722
+ peek,
723
+ take,
724
+ expect,
725
+ consume,
726
+ fail,
727
+ get index() {
728
+ return index;
729
+ },
730
+ get end() {
731
+ return tokens[index - 1]?.end ?? 0;
732
+ }
733
+ };
734
+ };
735
+
736
+ // packages/build/src/st/parse.ts
737
+ var dutTypes = {
738
+ STRUCT: "structure",
739
+ UNION: "union",
740
+ "(": "enumeration"
741
+ };
742
+ var parseDut = (reader) => {
743
+ reader.expect("TYPE");
744
+ readModifiers(reader);
745
+ const name = readIdentifier(reader);
746
+ if (reader.consume("EXTENDS")) {
747
+ readType(reader);
748
+ }
749
+ reader.expect(":");
750
+ readDeclarationPragmas(reader);
751
+ const dutType = dutTypes[reader.peek()?.value ?? ""] ?? "alias";
752
+ while (reader.peek()?.value !== "END_TYPE") {
753
+ if (reader.peek()?.value === "TYPE") {
754
+ reader.fail("Only one TYPE declaration is allowed per file");
755
+ }
756
+ reader.take();
757
+ }
758
+ reader.take();
759
+ reader.consume(";");
760
+ if (reader.peek()) {
761
+ reader.fail("Unexpected content after END_TYPE");
762
+ }
763
+ return { kind: "dut", name, declaration: reader.source, dutType };
764
+ };
765
+ var parseGlobals = (reader, options) => {
766
+ const header = reader.peek();
767
+ let persistent = false;
768
+ while (reader.peek()) {
769
+ readDeclarationPragmas(reader);
770
+ if (!reader.peek()) {
771
+ break;
772
+ }
773
+ if (!globalSections.has(reader.peek()?.value ?? "")) {
774
+ reader.fail("Expected a global variable section");
775
+ }
776
+ const start = reader.index;
777
+ readVariableSection(reader);
778
+ for (const token of reader.tokens.slice(start + 1, reader.index)) {
779
+ if (token.kind !== "word" || !["CONSTANT", "RETAIN", "PERSISTENT", "NON_RETAIN"].includes(token.value)) {
780
+ break;
781
+ }
782
+ persistent ||= token.value === "PERSISTENT";
783
+ }
784
+ }
785
+ const name = options.name ?? basename2(reader.filename, ".st");
786
+ if (!/^[A-Za-z_][A-Za-z_0-9]*$/.test(name)) {
787
+ reader.fail("Global list requires an IEC filename or an explicit name", header);
788
+ }
789
+ return {
790
+ kind: "gvl",
791
+ name,
792
+ declaration: reader.source,
793
+ ...persistent && { persistent: true }
794
+ };
795
+ };
796
+ var validateImplementation = (reader, tokens) => {
797
+ for (const token of tokens) {
798
+ if (token.kind !== "word") {
799
+ continue;
800
+ }
801
+ if (variableSections.has(token.value) || token.value === "END_VAR") {
802
+ reader.fail("Variable declaration outside the declaration section; conditional declaration envelopes are not supported", token);
803
+ }
804
+ if (declarationKeywords.has(token.value)) {
805
+ reader.fail("Nested objects must be separate child files, or the object terminator is mismatched", token);
806
+ }
807
+ }
808
+ };
809
+ var splitObject = (reader, header, kind, options) => {
810
+ const terminator = header.value === "FUNCTIONBLOCK" ? "END_FUNCTION_BLOCK" : `END_${header.value}`;
811
+ const remaining = reader.tokens.slice(reader.index);
812
+ const terminatorIndex = remaining.findIndex((token) => token.kind === "word" && token.value === terminator);
813
+ if (terminatorIndex < 0 && !options.declarationOnly) {
814
+ reader.fail(`Missing ${terminator}`);
815
+ }
816
+ const bodyTokens = terminatorIndex < 0 ? remaining : remaining.slice(0, terminatorIndex);
817
+ const [endToken, next, afterSemicolon] = terminatorIndex < 0 ? [] : remaining.slice(terminatorIndex);
818
+ const semicolon = next?.value === ";" ? next : undefined;
819
+ const unexpected = semicolon ? afterSemicolon : next;
820
+ if (unexpected) {
821
+ reader.fail(`Unexpected content after ${terminator}`, unexpected);
822
+ }
823
+ validateImplementation(reader, bodyTokens);
824
+ const declarationOnly = options.declarationOnly || kind === "interface" || kind === "property";
825
+ if (declarationOnly && bodyTokens.length) {
826
+ reader.fail("Unexpected implementation in a declaration-only object", bodyTokens[0]);
827
+ }
828
+ const declaration = reader.source.slice(0, reader.end);
829
+ const body = reader.source.slice(reader.end, endToken?.start) + (endToken ? reader.source.slice((semicolon ?? endToken).end) : "");
830
+ if (declarationOnly) {
831
+ return { declaration: declaration + body };
832
+ }
833
+ return { declaration, implementation: body };
834
+ };
835
+ var parsePou = (reader, options) => {
836
+ const header = reader.take();
837
+ if (!(header.value in objectKinds)) {
838
+ reader.fail(`Unsupported object header ${header.value}`, header);
839
+ }
840
+ const kind = objectKinds[header.value];
841
+ readModifiers(reader);
842
+ const name = readIdentifier(reader);
843
+ const returnType = reader.consume(":") ? readType(reader) : undefined;
844
+ if (returnType === undefined && kind === "property") {
845
+ reader.fail(`${header.value} requires a return type`);
846
+ }
847
+ readDeclarationSemicolon(reader);
848
+ readDeclarationSections(reader);
849
+ return {
850
+ kind,
851
+ name,
852
+ ...returnType === undefined ? {} : { returnType },
853
+ ...splitObject(reader, header, kind, options)
854
+ };
855
+ };
856
+ var parseSource = (source, options = {}) => {
857
+ const reader = createTokenReader(source, options.filename ?? "<source>");
858
+ readDeclarationPragmas(reader);
859
+ const header = reader.peek();
860
+ if (header?.value === "TYPE") {
861
+ return parseDut(reader);
862
+ }
863
+ if (header && globalSections.has(header.value)) {
864
+ return parseGlobals(reader, options);
865
+ }
866
+ return parsePou(reader, options);
867
+ };
868
+
869
+ // packages/build/src/io.ts
870
+ var arrayType = /^ARRAY\s*\[\s*(-?\d+)\s*\.\.\s*(-?\d+)\s*\]\s*OF\s+(\w+)$/;
871
+ var layout = (type) => {
872
+ const array = arrayType.exec(type);
873
+ return array ? { element: array[3], length: Number(array[2]) - Number(array[1]) + 1 } : { element: type, length: 1 };
874
+ };
875
+ var widths = {
876
+ BOOL: "X",
877
+ BYTE: "B",
878
+ SINT: "B",
879
+ USINT: "B",
880
+ WORD: "W",
881
+ INT: "W",
882
+ UINT: "W",
883
+ DWORD: "D",
884
+ DINT: "D",
885
+ UDINT: "D",
886
+ REAL: "D"
887
+ };
888
+ function bindStructuredTextIo(objects, bindings) {
889
+ const declarations = [];
890
+ const visit = (object) => {
891
+ const tokens = tokenize(object.declaration, object.source);
892
+ let section = false, start = 0;
893
+ for (let i = 0;i < tokens.length; i++) {
894
+ const token = tokens[i];
895
+ if (variableSections.has(token.value)) {
896
+ section = true;
897
+ start = i + 1;
898
+ continue;
899
+ }
900
+ if (token.value === "END_VAR") {
901
+ section = false;
902
+ continue;
903
+ }
904
+ if (!section || token.value !== ";")
905
+ continue;
906
+ const part = tokens.slice(start, i).filter((t) => t.kind !== "pragma");
907
+ start = i + 1;
908
+ const colonIndex = part.findIndex((t) => t.value === ":");
909
+ if (colonIndex < 1)
910
+ continue;
911
+ const names = part.slice(0, colonIndex).filter((t) => t.kind === "word" && !["CONSTANT", "RETAIN", "PERSISTENT", "AT"].includes(t.value));
912
+ const at = part.findIndex((t) => t.value === "AT");
913
+ const variables = at < 0 ? names : names.filter((t) => t.start < part[at].start);
914
+ const initializer = part.findIndex((t, index) => index > colonIndex && t.value === ":=");
915
+ const typeTokens = part.slice(colonIndex + 1, initializer < 0 ? undefined : initializer);
916
+ const type = typeTokens.length ? object.declaration.slice(typeTokens[0].start, typeTokens.at(-1).end).replace(/\s+/g, " ").toUpperCase() : "";
917
+ for (const name of variables)
918
+ declarations.push({
919
+ object,
920
+ name: name.value,
921
+ names: variables,
922
+ colon: part[colonIndex],
923
+ type
924
+ });
925
+ }
926
+ object.children.forEach(visit);
927
+ };
928
+ objects.forEach(visit);
929
+ const edits = new Map;
930
+ for (const [symbol, binding] of Object.entries(bindings)) {
931
+ const parts = symbol.toUpperCase().split(".");
932
+ if (parts.length > 2)
933
+ throw new Error(`${symbol}: direct IO maps a scalar declaration, not a nested field.`);
934
+ const matches = declarations.filter((d) => d.name === parts.at(-1) && (parts.length === 1 || d.object.name.toUpperCase() === parts[0]));
935
+ if (matches.length !== 1)
936
+ throw new Error(`${symbol}: expected one ST variable declaration, found ${matches.length}. Use Object.Variable to disambiguate.`);
937
+ const declaration = matches[0];
938
+ if (declaration.names.length !== 1)
939
+ throw new Error(`${symbol}: declare mapped IO variables separately.`);
940
+ const declared = layout(declaration.type);
941
+ if (!widths[declared.element] || declared.length > 1 && declared.element === "BOOL" || binding.address[2] !== widths[declared.element])
942
+ throw new Error(`${symbol}: ${declaration.type} is incompatible with ${binding.address}.`);
943
+ const expected = binding.type && layout(binding.type.replace(/\s+/g, " ").toUpperCase());
944
+ if (expected && (expected.element !== declared.element || expected.length !== declared.length))
945
+ throw new Error(`${symbol}: expected ${binding.type}, found ${declaration.type}.`);
946
+ const list = edits.get(declaration.object) ?? [];
947
+ const start = declaration.names[0].end;
948
+ if (list.some((e) => e.start === start))
949
+ throw new Error(`${symbol}: the variable has more than one IO binding.`);
950
+ list.push({ start, end: declaration.colon.start, text: ` AT ${binding.address} ` });
951
+ edits.set(declaration.object, list);
952
+ }
953
+ for (const [object, list] of edits)
954
+ for (const edit of list.sort((a, b) => b.start - a.start))
955
+ object.declaration = object.declaration.slice(0, edit.start) + edit.text + object.declaration.slice(edit.end);
956
+ }
957
+
958
+ // packages/build/src/build.ts
959
+ var buildProject = async (config) => {
960
+ if (!config.docker)
961
+ throw new Error("Builds require a Docker builder image.");
962
+ return buildInDocker(config, dockerImage);
963
+ };
964
+
965
+ // packages/build/src/config.ts
966
+ import {
967
+ configSchema as configSchema2,
968
+ controllerSchema as controllerSchema2,
969
+ cyclicTasksSchema as cyclicTasksSchema2,
970
+ dockerImage as dockerImage2,
971
+ defineBuildConfig as defineBuildConfig2,
972
+ resolveConfig as resolveConfig2,
973
+ loadConfig as loadConfig2
974
+ } from "@tsplc/config/build";
975
+
976
+ // packages/build/src/controller-online.ts
977
+ import { execFile as execFile4 } from "node:child_process";
978
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
979
+ import { mkdir as mkdir5, mkdtemp as mkdtemp3, readFile as readFile5, rm as rm5 } from "node:fs/promises";
980
+ import { tmpdir as tmpdir3 } from "node:os";
981
+ import { resolve as resolve7 } from "node:path";
982
+ import { promisify as promisify4 } from "node:util";
983
+ import { z as z5 } from "zod";
984
+ var execute4 = promisify4(execFile4);
985
+ var imageIdSchema = z5.string().regex(/^sha256:[a-f0-9]{64}$/);
986
+ var endpointSchema = z5.strictObject({
987
+ host: z5.string().nonempty(),
988
+ port: z5.number().int().min(1).max(65535)
989
+ });
990
+ var controllerResultSchema = z5.strictObject({
991
+ schemaVersion: z5.literal(1),
992
+ operationId: z5.uuid(),
993
+ operation: z5.enum(["download", "online-change", "inspect"]),
994
+ imageId: imageIdSchema,
995
+ templateHash: z5.string().regex(/^[a-f0-9]{64}$/),
996
+ target: z5.strictObject({
997
+ network: z5.string().nonempty(),
998
+ gateway: endpointSchema,
999
+ device: endpointSchema,
1000
+ controller: controllerSchema.optional()
1001
+ }),
1002
+ generation: z5.string().nonempty(),
1003
+ application: z5.string().nonempty(),
1004
+ applicationGuid: z5.string().nonempty(),
1005
+ controllerIds: z5.strictObject({
1006
+ codeId: z5.string().nonempty(),
1007
+ dataId: z5.string().nonempty()
1008
+ }),
1009
+ state: z5.string().nonempty(),
1010
+ values: z5.record(z5.string(), z5.string())
1011
+ });
1012
+ var controllerOperation = async (config, operation, options) => {
1013
+ if (!config.docker || !config.online) {
1014
+ throw new Error("Docker image and online gateway/device configuration are required for controller operations");
1015
+ }
1016
+ const environmentNames = config.online.credentials ? [config.online.credentials.usernameEnv, config.online.credentials.passwordEnv] : [];
1017
+ for (const name of environmentNames) {
1018
+ if (process.env[name] === undefined) {
1019
+ throw new Error(`Controller credential environment variable ${name} is not set`);
1020
+ }
1021
+ }
1022
+ const { stdout } = await execute4("docker", ["image", "inspect", "--format", "{{.Id}}", dockerImage], { timeout: config.timeouts.hostCommand }).catch((cause) => {
1023
+ throw new Error(`Cannot inspect Docker builder image ${dockerImage}. Install and start Docker, then run tsplc-build configure if the image is missing`, { cause });
1024
+ });
1025
+ const imageId = imageIdSchema.parse(stdout.trim());
1026
+ const templateHash = createHash2("sha256").update(await readFile5(config.template)).digest("hex");
1027
+ const operationId = randomUUID2();
1028
+ const target = {
1029
+ network: config.online.network,
1030
+ gateway: config.online.gateway,
1031
+ device: config.online.device,
1032
+ controller: config.controller
1033
+ };
1034
+ const requestDirectory = await mkdtemp3(resolve7(tmpdir3(), "codesys-controller-"));
1035
+ const diagnostics = resolve7(config.outDir, ".codesys/controller");
1036
+ try {
1037
+ await mkdir5(config.outDir, { recursive: true });
1038
+ await runDocker([
1039
+ ...await prepareContainerRun(config, requestDirectory, {
1040
+ network: target.network,
1041
+ environment: environmentNames,
1042
+ sources: operation !== "inspect"
1043
+ }),
1044
+ imageId,
1045
+ operation,
1046
+ "--request",
1047
+ "/input/config.json",
1048
+ "--operation-id",
1049
+ operationId,
1050
+ "--image-id",
1051
+ imageId,
1052
+ ...options.initializeUser ? ["--initialize-user"] : [],
1053
+ ...options.read ? ["--read", JSON.stringify(options.read)] : []
1054
+ ], (status) => `Docker controller operation failed (${status}). Inspect ${diagnostics} for diagnostics`);
1055
+ const result = await readFile5(resolve7(diagnostics, "result.json"), "utf8").then((content) => controllerResultSchema.parse(JSON.parse(content))).catch((cause) => {
1056
+ throw new Error(`Docker did not produce a valid controller receipt. Inspect ${diagnostics} for diagnostics`, { cause });
1057
+ });
1058
+ if (result.operationId !== operationId || result.operation !== operation || result.imageId !== imageId || result.templateHash !== templateHash || JSON.stringify(result.target) !== JSON.stringify(target)) {
1059
+ throw new Error(`Controller receipt does not match this operation's image, template or target. Inspect ${diagnostics} for diagnostics`);
1060
+ }
1061
+ return result;
1062
+ } finally {
1063
+ await rm5(requestDirectory, { recursive: true, force: true });
1064
+ }
1065
+ };
1066
+ var updateController = (config, operation, options = {}) => controllerOperation(config, operation, options);
1067
+ var inspectController = (config, read) => controllerOperation(config, "inspect", { read });
1068
+
1069
+ // packages/build/src/docker-build.ts
1070
+ import { execFile as execFile5 } from "node:child_process";
1071
+ import { cp as cp4, mkdir as mkdir6, mkdtemp as mkdtemp4, readFile as readFile6, rm as rm6 } from "node:fs/promises";
1072
+ import { tmpdir as tmpdir4 } from "node:os";
1073
+ import { resolve as resolve8 } from "node:path";
1074
+ import { promisify as promisify5 } from "node:util";
1075
+ var execute5 = promisify5(execFile5);
1076
+ var configureDocker = async () => {
1077
+ const image = dockerImage;
1078
+ const { stdout } = await execute5("docker", ["info", "--format", "{{.OSType}}/{{.Architecture}}"], { timeout: 30000 }).catch((cause) => {
1079
+ throw new Error("Cannot connect to Docker. Install and start Docker Desktop or Docker Engine", {
1080
+ cause
1081
+ });
1082
+ });
1083
+ const architecture = stdout.trim();
1084
+ const arm = architecture === "linux/aarch64" || architecture === "linux/arm64";
1085
+ if (!arm && architecture !== "linux/x86_64" && architecture !== "linux/amd64") {
1086
+ throw new Error("The builder requires a Linux Docker server on ARM64 or AMD64");
1087
+ }
1088
+ const context = await mkdtemp4(resolve8(tmpdir4(), "codesys-installer-"));
1089
+ let buildContext;
1090
+ try {
1091
+ buildContext = await prepareDockerContext();
1092
+ const cache = resolve8(context, "winetricks");
1093
+ await mkdir6(cache);
1094
+ if (process.env["CODESYS_WINETRICKS_CACHE"]) {
1095
+ await cp4(process.env["CODESYS_WINETRICKS_CACHE"], cache, {
1096
+ recursive: true
1097
+ });
1098
+ }
1099
+ console.log(`Preparing Docker builder (${image}). Downloading and installing CODESYS 3.5.22.30 and prerequisites.`);
1100
+ await runDocker([
1101
+ "build",
1102
+ "--platform",
1103
+ "linux/amd64",
1104
+ "--build-arg",
1105
+ `SETUP_BASE=${arm ? "arm-setup" : "base"}`,
1106
+ "--build-arg",
1107
+ `SETUP_PLATFORM=${arm ? "linux/arm64" : "linux/amd64"}`,
1108
+ "--build-arg",
1109
+ "INSTALLER_SOURCE=installer-download",
1110
+ "--progress",
1111
+ "plain",
1112
+ "--build-context",
1113
+ `installer=${context}`,
1114
+ "-f",
1115
+ resolve8(buildContext, "docker/Dockerfile"),
1116
+ "-t",
1117
+ image,
1118
+ buildContext
1119
+ ], (status) => `Docker configuration failed (${status}). Review the image-build output and run tsplc configure again`);
1120
+ return image;
1121
+ } finally {
1122
+ await Promise.all([
1123
+ rm6(context, { recursive: true, force: true }),
1124
+ ...buildContext ? [rm6(buildContext, { recursive: true, force: true })] : []
1125
+ ]);
1126
+ }
1127
+ };
1128
+ var runtimeImage2 = "tsplc-control-linux-sl:4.22.0.0";
1129
+ var configureRuntime = async () => {
1130
+ const context = resolve8(packageDirectory, "docker/control-linux-sl");
1131
+ console.log(`Preparing the local PLC runtime (${runtimeImage2}).`);
1132
+ await runDocker(["build", "--platform", "linux/amd64", "--progress", "plain", "-t", runtimeImage2, context], (status) => `Runtime image build failed (${status}). Review the image-build output`);
1133
+ return runtimeImage2;
1134
+ };
1135
+
1136
+ // packages/build/src/libraries.ts
1137
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
1138
+ import { mkdir as mkdir7, readFile as readFile7, rename as rename3, writeFile as writeFile5 } from "node:fs/promises";
1139
+ import { basename as basename3, relative as relative2, resolve as resolve9 } from "node:path";
1140
+ var libraryRegistry = "https://store-archive.codesys.com/CODESYSLibs";
1141
+ var libraryFile = /\.(?:compiled-library(?:-v\d+)?|library)$/i;
1142
+ var hash = (data) => createHash3("sha256").update(data).digest("hex");
1143
+ var component = (name) => {
1144
+ if (!name.trim() || /[/\\\0\r\n]/.test(name) || name === "." || name === "..")
1145
+ throw new Error(`Invalid library registry component: ${name}.`);
1146
+ return encodeURIComponent(name);
1147
+ };
1148
+ async function response(url, fetcher) {
1149
+ const result = await fetcher(url, { signal: AbortSignal.timeout(30000) });
1150
+ if (!result.ok)
1151
+ throw new Error(`Library registry ${result.status}: ${url}`);
1152
+ return result;
1153
+ }
1154
+ async function libraryIndex(path = ["System"], fetcher = fetch) {
1155
+ const url = `${libraryRegistry}/${path.map(component).join("/")}/index`;
1156
+ const result = await response(url, fetcher);
1157
+ const text = await result.text();
1158
+ if (text.length > 4000000 || /<html/i.test(text))
1159
+ throw new Error(`Invalid library registry index: ${url}`);
1160
+ return text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
1161
+ }
1162
+ async function validateLibrary(title, version, company = "System", fetcher = fetch) {
1163
+ if (!(await libraryIndex([company], fetcher)).includes(title))
1164
+ throw new Error(`Unknown CODESYS library ${company}/${title}. Names are case sensitive.`);
1165
+ const versions = await libraryIndex([company, title], fetcher);
1166
+ if (!versions.includes(version))
1167
+ throw new Error(`Unknown version ${version} for ${title}. Available: ${versions.join(", ")}`);
1168
+ const filenames = (await libraryIndex([company, title, version], fetcher)).filter((name) => libraryFile.test(name));
1169
+ if (filenames.length !== 1)
1170
+ throw new Error(`Expected one library payload for ${title} ${version}, received ${filenames.length}.`);
1171
+ return {
1172
+ filename: filenames[0],
1173
+ url: `${libraryRegistry}/${[company, title, version, filenames[0]].map(component).join("/")}`
1174
+ };
1175
+ }
1176
+ async function installLibraries(dependencies, root, options = {}) {
1177
+ const file = resolve9(root, "tsplc.lock.json"), store = resolve9(root, ".tsplc/libraries");
1178
+ let previous = { version: 1, dependencies: {} };
1179
+ try {
1180
+ previous = JSON.parse(await readFile7(file, "utf8"));
1181
+ } catch (cause) {
1182
+ if (!(cause && typeof cause === "object" && ("code" in cause) && cause.code === "ENOENT"))
1183
+ throw cause;
1184
+ }
1185
+ if (previous.version !== 1 || !previous.dependencies || typeof previous.dependencies !== "object")
1186
+ throw new Error("Invalid tsplc.lock.json.");
1187
+ const installed = {};
1188
+ const fetcher = options.fetcher ?? fetch;
1189
+ await mkdir7(store, { recursive: true });
1190
+ for (const [title, dependency] of Object.entries(dependencies)) {
1191
+ component(title);
1192
+ const settings = typeof dependency === "string" ? { version: dependency } : dependency;
1193
+ const locked = previous.dependencies[title];
1194
+ const prior = locked ? { ...locked, path: resolve9(root, locked.path) } : undefined;
1195
+ const same = prior !== undefined && prior.namespace === settings.namespace && ("path" in settings ? prior.url === undefined && prior.path === resolve9(root, settings.path) : prior.version === settings.version && prior.company === (settings.company ?? "System"));
1196
+ if (same) {
1197
+ try {
1198
+ if (hash(await readFile7(prior.path)) === prior.sha256) {
1199
+ installed[title] = prior;
1200
+ continue;
1201
+ }
1202
+ } catch {}
1203
+ if (options.frozen)
1204
+ throw new Error(`Locked library ${title} is missing or changed.`);
1205
+ } else if (options.frozen)
1206
+ throw new Error(`Dependency ${title} differs from tsplc.lock.json.`);
1207
+ if ("path" in settings) {
1208
+ const path = resolve9(root, settings.path);
1209
+ if (!libraryFile.test(path))
1210
+ throw new Error(`${title}: expected a .library or .compiled-library file.`);
1211
+ installed[title] = {
1212
+ title,
1213
+ path,
1214
+ sha256: hash(await readFile7(path)),
1215
+ ...settings.namespace ? { namespace: settings.namespace } : {}
1216
+ };
1217
+ } else {
1218
+ const company = settings.company ?? "System";
1219
+ const payload = await validateLibrary(title, settings.version, company, fetcher);
1220
+ const result = await response(payload.url, fetcher);
1221
+ const data = new Uint8Array(await result.arrayBuffer());
1222
+ if (!data.length || data.length > 256 * 1024 * 1024)
1223
+ throw new Error(`${title}: invalid library payload size.`);
1224
+ const sha256 = hash(data);
1225
+ if (same && prior.sha256 !== sha256)
1226
+ throw new Error(`Registry payload changed for locked library ${title} ${settings.version}.`);
1227
+ const path = resolve9(store, `${sha256}-${basename3(payload.filename)}`), temp = `${path}.${randomUUID3()}.tmp`;
1228
+ await writeFile5(temp, data, { flag: "wx" });
1229
+ await rename3(temp, path);
1230
+ installed[title] = {
1231
+ title,
1232
+ company,
1233
+ version: settings.version,
1234
+ path,
1235
+ sha256,
1236
+ url: payload.url,
1237
+ ...settings.namespace ? { namespace: settings.namespace } : {}
1238
+ };
1239
+ }
1240
+ }
1241
+ if (options.frozen && Object.keys(previous.dependencies).some((name) => !Object.hasOwn(dependencies, name)))
1242
+ throw new Error("Removed dependencies differ from tsplc.lock.json.");
1243
+ if (!options.frozen) {
1244
+ const temp = `${file}.${randomUUID3()}.tmp`;
1245
+ const portable = Object.fromEntries(Object.entries(installed).map(([title, library]) => [
1246
+ title,
1247
+ { ...library, path: relative2(root, library.path).replaceAll("\\", "/") }
1248
+ ]));
1249
+ await writeFile5(temp, JSON.stringify({ version: 1, dependencies: portable }, null, 2) + `
1250
+ `);
1251
+ await rename3(temp, file);
1252
+ }
1253
+ return Object.values(installed);
1254
+ }
1255
+
1256
+ // packages/build/src/run.ts
1257
+ import { execFile as execFile6 } from "node:child_process";
1258
+ import { mkdtemp as mkdtemp5, readFile as readFile8, rm as rm7, writeFile as writeFile6 } from "node:fs/promises";
1259
+ import { tmpdir as tmpdir5 } from "node:os";
1260
+ import { basename as basename4, dirname as dirname2, join } from "node:path";
1261
+ import { setTimeout as delay2 } from "node:timers/promises";
1262
+ import { promisify as promisify6 } from "node:util";
1263
+ import { z as z6 } from "zod";
1264
+
1265
+ // packages/build/src/runtime-status.ts
1266
+ var getApplicationStatus = (log, application, startedAt) => {
1267
+ const event = log.split(/\r?\n/).reverse().find((line) => {
1268
+ const timestamp = Date.parse(line.split(",")[0] ?? "");
1269
+ if (!Number.isFinite(timestamp) || timestamp < startedAt) {
1270
+ return false;
1271
+ }
1272
+ return line.includes(`<app>${application}</app>`) && /\b(started|stopped|exception|corrupt|failed|denied)\b/i.test(line) && !line.includes("load started");
1273
+ });
1274
+ if (!event) {
1275
+ return "pending";
1276
+ }
1277
+ return event.includes(`Application [<app>${application}</app>] started`) ? "running" : "failed";
1278
+ };
1279
+
1280
+ // packages/build/src/run.ts
1281
+ var execute6 = promisify6(execFile6);
1282
+ var stateSchema = z6.object({
1283
+ Running: z6.boolean(),
1284
+ StartedAt: z6.iso.datetime({ offset: true })
1285
+ });
1286
+ var service = "plc";
1287
+ var runtimeProject = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1288
+ var runtimeDefinition = (name) => ({
1289
+ services: {
1290
+ [service]: {
1291
+ image: runtimeImage,
1292
+ platform: "linux/amd64",
1293
+ hostname: `${runtimeProject(name)}-${service}`,
1294
+ init: true,
1295
+ ports: ["127.0.0.1:4840:4840", "127.0.0.1:11740:11740"],
1296
+ cap_add: ["SYS_NICE", "IPC_LOCK"],
1297
+ volumes: ["control-conf:/conf/codesyscontrol", "control-data:/data/codesyscontrol"],
1298
+ restart: "no"
1299
+ }
1300
+ },
1301
+ volumes: { "control-conf": {}, "control-data": {} }
1302
+ });
1303
+ var runProject = async (config, name) => withWorkerLock(config, async () => {
1304
+ const build = await readBuild(config.outDir).catch((cause) => {
1305
+ throw new Error(`No complete build in ${config.outDir}. Run tsplc-build build first.`, {
1306
+ cause
1307
+ });
1308
+ });
1309
+ const definitionDirectory = await mkdtemp5(join(tmpdir5(), "tsplc-runtime-"));
1310
+ try {
1311
+ const composeFile = join(definitionDirectory, "compose.yaml");
1312
+ await writeFile6(composeFile, JSON.stringify(runtimeDefinition(name)));
1313
+ const docker = (...args) => execute6("docker", args, {
1314
+ timeout: config.timeouts.runtime,
1315
+ maxBuffer: 8 * 1024 * 1024
1316
+ });
1317
+ const compose = (...args) => docker("compose", "--project-name", runtimeProject(name), "--file", composeFile, ...args);
1318
+ const installer = await readFile8(new URL("../runtime/install.sh", import.meta.url), "utf8");
1319
+ console.log(`Installing ${build.application} in Docker service ${service}`);
1320
+ await compose("stop", service);
1321
+ await compose("run", "--rm", "--no-deps", "-T", "--volume", `${dirname2(build.output)}:/codesys-build:ro`, "--entrypoint", "/bin/sh", service, "-c", installer, "codesys-build-install", build.application, basename4(build.output, ".app"));
1322
+ await compose("up", "--detach", "--no-deps", service);
1323
+ const containers = (await compose("ps", "--all", "--quiet", service)).stdout.trim().split(/\s+/).filter(Boolean);
1324
+ if (containers.length !== 1 || !containers[0]) {
1325
+ throw new Error(`Expected one container for Compose service ${service}`);
1326
+ }
1327
+ const container = containers[0];
1328
+ const deadline = Date.now() + config.timeouts.runtime;
1329
+ while (true) {
1330
+ const state = stateSchema.parse(JSON.parse((await docker("inspect", "--format", "{{json .State}}", container)).stdout));
1331
+ if (!state.Running) {
1332
+ throw new Error(`Docker service ${service} exited before the application started. Check docker logs ${container}.`);
1333
+ }
1334
+ const { stdout: log } = await docker("exec", container, "/bin/sh", "-c", "if [ -f /data/codesyscontrol/codesyscontrol.log ]; then cat /data/codesyscontrol/codesyscontrol.log; fi");
1335
+ const status = getApplicationStatus(log, build.application, Date.parse(state.StartedAt));
1336
+ if (status === "running") {
1337
+ console.log(`${build.application} is running in ${service}`);
1338
+ return { ...build, container };
1339
+ }
1340
+ if (status === "failed" || Date.now() >= deadline) {
1341
+ throw new Error(`CODESYS did not start ${build.application}. Runtime log:
1342
+ ${log.split(`
1343
+ `).slice(-25).join(`
1344
+ `)}`);
1345
+ }
1346
+ await delay2(250);
1347
+ }
1348
+ } finally {
1349
+ await rm7(definitionDirectory, { force: true, recursive: true });
1350
+ }
1351
+ });
1352
+
1353
+ // packages/build/src/sources.ts
1354
+ import { readdir, readFile as readFile9 } from "node:fs/promises";
1355
+ import { basename as basename5, resolve as resolve10 } from "node:path";
1356
+ var prefixes = {
1357
+ FB: "function_block",
1358
+ PRG: "program",
1359
+ FUN: "function",
1360
+ F: "function",
1361
+ I: "interface",
1362
+ ST: "dut",
1363
+ E: "dut",
1364
+ T: "dut",
1365
+ U: "dut",
1366
+ GVL: "gvl"
1367
+ };
1368
+ var childFolders = {
1369
+ methods: "method",
1370
+ properties: "property",
1371
+ actions: "action"
1372
+ };
1373
+ var canOwn = (kind) => ["program", "function_block", "interface", "property"].includes(kind);
1374
+ var assertUnique = (objects, scope) => {
1375
+ const seen = new Map;
1376
+ for (const object of objects) {
1377
+ const key = object.name.toLowerCase();
1378
+ const previous = seen.get(key);
1379
+ if (previous) {
1380
+ throw new Error(`Duplicate IEC name ${object.name} in ${scope}: ${previous} and ${object.source}`);
1381
+ }
1382
+ seen.set(key, object.source);
1383
+ assertUnique(object.children, `${scope}.${object.name}`);
1384
+ }
1385
+ };
1386
+ var readUnit = async (name, files, role, interfaceMember = false) => {
1387
+ if (!/^[A-Za-z_][A-Za-z_0-9]*$/.test(name)) {
1388
+ throw new Error(`Invalid IEC object name: ${name}`);
1389
+ }
1390
+ for (const [part, candidates] of Object.entries(files)) {
1391
+ if (candidates.length > 1) {
1392
+ throw new Error(`Ambiguous ${part} for ${name}: ${candidates.join(", ")}`);
1393
+ }
1394
+ }
1395
+ const combined = files.combined[0];
1396
+ const declaration = files.declaration[0];
1397
+ const implementation = files.implementation[0];
1398
+ if (combined && (declaration || implementation)) {
1399
+ throw new Error(`Both combined and split sources exist for ${name}`);
1400
+ }
1401
+ const path = combined ?? declaration ?? implementation;
1402
+ if (!path) {
1403
+ throw new Error(`Missing source for ${name}`);
1404
+ }
1405
+ if (role === "action") {
1406
+ if (!combined) {
1407
+ throw new Error(`Actions use one implementation-only .st file: ${path}`);
1408
+ }
1409
+ return {
1410
+ kind: "action",
1411
+ name,
1412
+ declaration: "",
1413
+ implementation: await readFile9(combined, "utf8"),
1414
+ source: combined
1415
+ };
1416
+ }
1417
+ if (role === "get" || role === "set") {
1418
+ if (!combined) {
1419
+ throw new Error(`Property accessors use one .st file: ${path}`);
1420
+ }
1421
+ const body = await readFile9(combined, "utf8");
1422
+ const header = `METHOD ${name}
1423
+ `;
1424
+ const parsed = parseSource(`${header}${body}
1425
+ END_METHOD`, {
1426
+ filename: combined
1427
+ });
1428
+ return {
1429
+ ...parsed,
1430
+ kind: role,
1431
+ name,
1432
+ declaration: parsed.declaration.slice(header.length),
1433
+ source: combined
1434
+ };
1435
+ }
1436
+ if (!combined && !declaration) {
1437
+ throw new Error(`Missing declaration for ${path}`);
1438
+ }
1439
+ const sourcePath = combined ?? declaration ?? path;
1440
+ const parsed = parseSource(await readFile9(sourcePath, "utf8"), {
1441
+ filename: sourcePath,
1442
+ name,
1443
+ declarationOnly: !combined || interfaceMember
1444
+ });
1445
+ if (parsed.name.toLowerCase() !== name.toLowerCase()) {
1446
+ throw new Error(`Source name ${name} does not match declaration ${parsed.name}: ${sourcePath}`);
1447
+ }
1448
+ if (role && parsed.kind !== role) {
1449
+ throw new Error(`Expected ${role}, found ${parsed.kind}: ${sourcePath}`);
1450
+ }
1451
+ if (!role && ["method", "property"].includes(parsed.kind)) {
1452
+ throw new Error(`Child object has no enclosing POU: ${sourcePath}`);
1453
+ }
1454
+ const prefix = /^([A-Z]+)_/.exec(name.toUpperCase())?.[1];
1455
+ if (!role && prefix && prefixes[prefix] && prefixes[prefix] !== parsed.kind) {
1456
+ throw new Error(`Prefix ${prefix}_ conflicts with ${parsed.kind}: ${sourcePath}`);
1457
+ }
1458
+ const declarationOnly = ["dut", "gvl", "interface", "property"].includes(parsed.kind) || interfaceMember;
1459
+ if (declarationOnly && implementation) {
1460
+ throw new Error(`This object has no implementation: ${implementation}`);
1461
+ }
1462
+ if (!combined && !declarationOnly && !implementation) {
1463
+ throw new Error(`Missing implementation for ${sourcePath}`);
1464
+ }
1465
+ return {
1466
+ ...parsed,
1467
+ ...implementation ? { implementation: await readFile9(implementation, "utf8") } : {},
1468
+ source: sourcePath
1469
+ };
1470
+ };
1471
+ var loadSources2 = async (config) => {
1472
+ const objects = [];
1473
+ const visit = async (directory, folder, parent, role) => {
1474
+ const entries = (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name, "en"));
1475
+ const groups = new Map;
1476
+ for (const entry of entries) {
1477
+ if (entry.isSymbolicLink()) {
1478
+ throw new Error(`Source symlinks are not supported: ${resolve10(directory, entry.name)}`);
1479
+ }
1480
+ if (!entry.isFile() || !/\.st$/i.test(entry.name)) {
1481
+ continue;
1482
+ }
1483
+ const stem = entry.name.slice(0, -3);
1484
+ const suffix = /^(.*)\.(declaration|implementation)$/.exec(stem);
1485
+ const generic = ["source", "declaration", "implementation"].includes(stem);
1486
+ const name = generic ? basename5(directory) : suffix?.[1] ?? stem;
1487
+ const section = suffix?.[2] ?? stem;
1488
+ const part = section === "declaration" || section === "implementation" ? section : "combined";
1489
+ const key = name.toLowerCase();
1490
+ const group = groups.get(key) ?? {
1491
+ name,
1492
+ files: { combined: [], declaration: [], implementation: [] }
1493
+ };
1494
+ group.files[part].push(resolve10(directory, entry.name));
1495
+ groups.set(key, group);
1496
+ }
1497
+ const units = [];
1498
+ const ownKey = basename5(directory).toLowerCase();
1499
+ const ordered = [...groups].sort(([a], [b]) => Number(b === ownKey) - Number(a === ownKey));
1500
+ for (const [key, { name, files }] of ordered) {
1501
+ const ownProperty = units.find((unit) => unit.kind === "property" && unit.name.toLowerCase() === ownKey);
1502
+ const accessor = ownProperty && (key === "get" || key === "set") ? key : undefined;
1503
+ const unit = {
1504
+ ...await readUnit(accessor ? `${accessor[0]?.toUpperCase()}${accessor.slice(1)}` : name, files, accessor ?? role, parent?.kind === "interface"),
1505
+ folder: parent ? [] : folder,
1506
+ children: []
1507
+ };
1508
+ if (accessor && ownProperty) {
1509
+ if (parent?.kind === "interface") {
1510
+ throw new Error(`Interface properties cannot implement accessors: ${unit.source}`);
1511
+ }
1512
+ ownProperty.children.push(unit);
1513
+ } else {
1514
+ units.push(unit);
1515
+ if (parent) {
1516
+ parent.children.push(unit);
1517
+ } else {
1518
+ objects.push(unit);
1519
+ }
1520
+ }
1521
+ }
1522
+ const own = units.find((unit) => unit.name.toLowerCase() === ownKey && canOwn(unit.kind));
1523
+ if (own && !parent) {
1524
+ own.folder = folder.slice(0, -1);
1525
+ }
1526
+ for (const entry of entries.filter((entry) => entry.isDirectory())) {
1527
+ const childRole = childFolders[entry.name];
1528
+ if (own && childRole) {
1529
+ if (own.kind === "property" || own.kind === "interface" && childRole === "action") {
1530
+ throw new Error(`Unsupported ${entry.name} under ${own.name}`);
1531
+ }
1532
+ await visit(resolve10(directory, entry.name), [], own, childRole);
1533
+ } else if (parent) {
1534
+ await visit(resolve10(directory, entry.name), [], parent, role);
1535
+ } else {
1536
+ await visit(resolve10(directory, entry.name), [...folder, entry.name]);
1537
+ }
1538
+ }
1539
+ };
1540
+ await visit(resolve10(config.sourceDir), []);
1541
+ assertUnique(objects, "Application");
1542
+ if (config.io)
1543
+ bindStructuredTextIo(objects, config.io);
1544
+ const entry = objects.find((object) => object.source === resolve10(config.entry));
1545
+ if (!entry) {
1546
+ throw new Error(`Entry is not a discovered source: ${config.entry}`);
1547
+ }
1548
+ if (entry.kind !== "program") {
1549
+ throw new Error(`Entry must be a PROGRAM: ${config.entry}`);
1550
+ }
1551
+ if (config.tasks) {
1552
+ const programs = new Set(objects.filter((object) => object.kind === "program").map((object) => object.name));
1553
+ for (const task of config.tasks)
1554
+ for (const program of task.programs)
1555
+ if (!programs.has(program))
1556
+ throw new Error(`Task ${task.name} references an undiscovered PROGRAM ${program}`);
1557
+ }
1558
+ return { entry: entry.name, objects };
1559
+ };
1560
+
1561
+ // packages/build/src/cli.ts
1562
+ async function main() {
1563
+ const { values, positionals } = parseArgs({
1564
+ allowPositionals: true,
1565
+ options: {
1566
+ config: { type: "string" },
1567
+ help: { type: "boolean", short: "h" },
1568
+ read: { type: "string", multiple: true },
1569
+ "initialize-user": { type: "boolean" },
1570
+ "frozen-lockfile": { type: "boolean" },
1571
+ runtime: { type: "boolean" }
1572
+ }
1573
+ });
1574
+ const command = positionals[0];
1575
+ const commands = new Set([
1576
+ "configure",
1577
+ "install",
1578
+ "check",
1579
+ "build",
1580
+ "run",
1581
+ "download",
1582
+ "online-change",
1583
+ "inspect"
1584
+ ]);
1585
+ if (positionals.length > 1)
1586
+ throw new Error("Specify one command.");
1587
+ if (!command || values.help) {
1588
+ console.log("tsplc-build configure|install|check|build|run|download|online-change|inspect [--config tsplc.config.ts] [--read symbol] [--runtime]");
1589
+ return;
1590
+ }
1591
+ if (!commands.has(command))
1592
+ throw new Error(`Unknown command ${command}.`);
1593
+ if (values.runtime && command !== "configure")
1594
+ throw new Error("--runtime applies only to configure.");
1595
+ if (command === "configure") {
1596
+ await (values.runtime ? configureRuntime() : configureDocker());
1597
+ return;
1598
+ }
1599
+ const { config, configFile, root } = await loadTsplcConfig(values.config);
1600
+ if (command === "install") {
1601
+ const libraries = await installLibraries(config.dependencies, root, {
1602
+ frozen: values["frozen-lockfile"]
1603
+ });
1604
+ console.log(`Installed ${libraries.length} CODESYS libraries.`);
1605
+ return;
1606
+ }
1607
+ if (!config.build)
1608
+ throw new Error("Configure build options in tsplc.config.ts.");
1609
+ if (command === "inspect") {
1610
+ console.log(JSON.stringify(await inspectController(resolveConfig2({ ...config.build, controller: controllerFor(config, command) }, configFile), values.read), null, 2));
1611
+ return;
1612
+ }
1613
+ if (config.compiler)
1614
+ throw new Error("Use tsplc for TypeScript projects. tsplc-build accepts raw Structured Text.");
1615
+ const build = rawBuildConfig(config);
1616
+ const hardware = await loadHardware(config, root);
1617
+ const io = resolveIo(config, hardware);
1618
+ if (command === "check") {
1619
+ const sources = await loadSources2(resolveConfig2({ ...build, io }, configFile));
1620
+ console.log(`Validated ${sources.objects.length} Structured Text objects.`);
1621
+ return;
1622
+ }
1623
+ const libraries = await installLibraries(config.dependencies, root, {
1624
+ frozen: values["frozen-lockfile"]
1625
+ });
1626
+ const settings = resolveConfig2({
1627
+ ...build,
1628
+ libraries,
1629
+ io,
1630
+ deviceTree: await deviceTree(config, hardware),
1631
+ controller: controllerFor(config, command)
1632
+ }, configFile);
1633
+ if (command === "build")
1634
+ await buildProject(settings);
1635
+ else if (command === "run") {
1636
+ await buildProject(settings);
1637
+ await runProject(settings, config.name);
1638
+ } else if (command === "download" || command === "online-change")
1639
+ await updateController(settings, command, {
1640
+ initializeUser: values["initialize-user"],
1641
+ read: values.read
1642
+ });
1643
+ }
1644
+ try {
1645
+ await main();
1646
+ } catch (cause) {
1647
+ console.error(cause instanceof Error ? cause.message : String(cause));
1648
+ process.exitCode = 1;
1649
+ }
1650
+
1651
+ //# debugId=B5CDCE065D7E468A64756E2164756E21
1652
+ //# sourceMappingURL=cli.js.map