@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
@@ -0,0 +1,89 @@
1
+ """One long-lived script inside CODESYS; all engine calls stay on its script thread."""
2
+ from __future__ import print_function
3
+
4
+ import io
5
+ import json
6
+ import os
7
+ import traceback
8
+
9
+ if __name__ == "__main__":
10
+ import sys
11
+
12
+ sys.path.insert(0, os.environ["CODESYS_BUILD_SESSION"])
13
+ from build import CompilerError, build
14
+
15
+
16
+ def write_json(path, value):
17
+ # The client reads only complete replies. Each request has a unique filename.
18
+ with io.open(path + ".tmp", "w", encoding="utf-8") as output:
19
+ output.write(u"{0}".format(json.dumps(value, ensure_ascii=False)))
20
+ os.rename(path + ".tmp", path)
21
+
22
+
23
+ def run_build(engine, directory, loaded):
24
+ root = os.path.join(os.path.dirname(directory), "build")
25
+ with io.open(os.path.join(root, "request.json"), "r", encoding="utf-8") as source:
26
+ request = json.load(source)
27
+ for key in ("template", "project", "output", "receipt"):
28
+ request[key] = os.path.join(root, request[key])
29
+ try:
30
+ build(engine, request, loaded, directory)
31
+ except Exception:
32
+ with io.open(os.path.join(root, "error.txt"), "w", encoding="utf-8") as output:
33
+ output.write(u"{0}".format(traceback.format_exc()))
34
+ raise
35
+
36
+
37
+ def serve(engine, directory):
38
+ request_path = os.path.join(directory, "request.json")
39
+ loaded = {}
40
+ while True:
41
+ if not os.path.isfile(request_path):
42
+ # Unlike time.sleep, this keeps CODESYS's message loop responsive.
43
+ engine.system.delay(200)
44
+ continue
45
+ with io.open(request_path, "r", encoding="utf-8") as source:
46
+ command = json.load(source)
47
+ os.remove(request_path)
48
+ response = {"id": command["id"], "ok": True}
49
+ try:
50
+ if command["action"] == "build":
51
+ run_build(engine, directory, loaded)
52
+ elif command["action"] == "online":
53
+ from codesys_online import run_online
54
+ if loaded.get("project") is not None:
55
+ loaded["project"].close()
56
+ loaded.clear()
57
+ root = os.path.join(os.path.dirname(directory), "controller")
58
+ with io.open(os.path.join(root, "request.json"), encoding="utf-8") as source:
59
+ request = json.load(source)
60
+ write_json(os.path.join(root, "result.json"), run_online(engine, request))
61
+ elif command["action"] not in ("ping", "stop"):
62
+ raise ValueError("Unknown worker action")
63
+ except Exception as error:
64
+ response = {"id": command["id"], "ok": False, "error": traceback.format_exc()}
65
+ if isinstance(error, CompilerError):
66
+ response["diagnostics"] = error.diagnostics
67
+ write_json(os.path.join(directory, command["id"] + ".json"), response)
68
+ if command["action"] == "stop":
69
+ return
70
+
71
+
72
+ if __name__ == "__main__":
73
+ import scriptengine
74
+ scriptengine.system.script_prompt_handling = scriptengine.ScriptPromptHandling.LogPrompts
75
+ native_exit = None
76
+ if os.environ.get("CODESYS_BUILD_NATIVE_EXIT") == "true":
77
+ # Import and bind while the engine is starting. A late IronPython import
78
+ # after serving builds can fault in the runtime's debug-info rewriter.
79
+ import ctypes
80
+ native_exit = ctypes.windll.kernel32.ExitProcess
81
+ exit_code = 0
82
+ try:
83
+ serve(scriptengine, os.environ["CODESYS_BUILD_SESSION"])
84
+ except Exception:
85
+ traceback.print_exc()
86
+ exit_code = 1
87
+ if native_exit is not None:
88
+ native_exit(exit_code)
89
+ scriptengine.system.exit(exit_code)
@@ -0,0 +1,74 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ application_name=$1
5
+ source_application=${2:-Application}
6
+ config_dir=/conf/codesyscontrol
7
+ data_dir=/data/codesyscontrol
8
+ config_file=$config_dir/CODESYSControl_User.cfg
9
+
10
+ test -d "$config_dir"
11
+ test -d "$data_dir"
12
+ test -s "/codesys-build/$source_application.app"
13
+ test -s "/codesys-build/$source_application.crc"
14
+
15
+ # Match the Virtual Control image's first-start initialization before installing
16
+ # the application. Existing configuration, device users, licenses and data survive.
17
+ if [ ! -f "$config_dir/.docker_initialized" ]; then
18
+ for file in CODESYSControl.cfg CODESYSControl_User.cfg; do
19
+ if [ ! -f "$config_dir/$file" ]; then
20
+ cp "/etc/codesyscontrol/$file" "$config_dir/$file"
21
+ fi
22
+ done
23
+ touch "$config_dir/.docker_initialized"
24
+ fi
25
+ if [ ! -f "$data_dir/.docker_initialized" ]; then
26
+ cp -a /var/opt/codesys/. "$data_dir/"
27
+ touch "$data_dir/.docker_initialized"
28
+ fi
29
+
30
+ # Register the boot application once, retaining other applications and settings.
31
+ # Read the full section first so a new application index cannot overwrite one.
32
+ cp -p "$config_file" "$config_file.codesys-build.tmp"
33
+ awk -v application="$application_name" '
34
+ {
35
+ if (FNR == 1) in_app = 0
36
+ if (FILENAME == ARGV[2]) lines[++count] = $0
37
+ line = $0
38
+ sub(/\r$/, "", line)
39
+ gsub(/^[ \t]+|[ \t]+$/, "", line)
40
+ if (line ~ /^\[/) in_app = (line == "[CmpApp]")
41
+ if (in_app && line ~ /^Application\.[0-9]+[ \t]*=/) {
42
+ split(line, parts, "=")
43
+ index_text = parts[1]
44
+ sub(/^Application\./, "", index_text)
45
+ if (index_text + 0 > maximum) maximum = index_text + 0
46
+ value = parts[2]
47
+ gsub(/^[ \t]+|[ \t]+$/, "", value)
48
+ applications[index_text + 0] = value
49
+ }
50
+ }
51
+ END {
52
+ # User configuration overrides the base file for the same Application.N key.
53
+ for (application_index in applications) {
54
+ if (applications[application_index] == application) registered = 1
55
+ }
56
+ for (i = 1; i <= count; i++) {
57
+ print lines[i]
58
+ if (!registered && lines[i] ~ /^[ \t]*\[CmpApp\][ \t]*\r?$/) {
59
+ print "Application." (maximum + 1) "=" application
60
+ registered = 1
61
+ }
62
+ }
63
+ if (!registered) print "\n[CmpApp]\nApplication." (maximum + 1) "=" application
64
+ }' "$config_dir/CODESYSControl.cfg" "$config_file" > "$config_file.codesys-build.tmp"
65
+ mv "$config_file.codesys-build.tmp" "$config_file"
66
+
67
+ # The runtime stays stopped until both boot files are installed. Keep retain files
68
+ # and application-owned data; this is a cold deployment, not an online change.
69
+ application_dir=$data_dir/PlcLogic/$application_name
70
+ mkdir -p "$application_dir"
71
+ for extension in app crc; do
72
+ cp "/codesys-build/$source_application.$extension" "$application_dir/$application_name.$extension.tmp"
73
+ mv "$application_dir/$application_name.$extension.tmp" "$application_dir/$application_name.$extension"
74
+ done
@@ -0,0 +1,40 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <project xmlns="http://www.plcopen.org/xml/tc6_0200" xmlns:xhtml="http://www.w3.org/1999/xhtml">
3
+ <fileHeader companyName="" productName="CODESYS" productVersion="CODESYS V3.5 SP21" creationDateTime="<NORMALIZED>" />
4
+ <contentHeader name="FB_Simple" modificationDateTime="<NORMALIZED>">
5
+ <coordinateInfo>
6
+ <fbd><scaling x="1" y="1" /></fbd>
7
+ <ld><scaling x="1" y="1" /></ld>
8
+ <sfc><scaling x="1" y="1" /></sfc>
9
+ </coordinateInfo>
10
+ </contentHeader>
11
+ <types>
12
+ <dataTypes />
13
+ <pous>
14
+ <pou name="FB_Simple" pouType="functionBlock">
15
+ <interface>
16
+ <inputVars>
17
+ <variable name="bEnable">
18
+ <type><BOOL /></type>
19
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Enable command</xhtml></documentation>
20
+ </variable>
21
+ </inputVars>
22
+ <outputVars>
23
+ <variable name="bDone">
24
+ <type><BOOL /></type>
25
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Done flag</xhtml></documentation>
26
+ </variable>
27
+ </outputVars>
28
+ </interface>
29
+ <body>
30
+ <ST>
31
+ <xhtml xmlns="http://www.w3.org/1999/xhtml"><![CDATA[bDone := bEnable;]]></xhtml>
32
+ </ST>
33
+ </body>
34
+ </pou>
35
+ </pous>
36
+ </types>
37
+ <instances>
38
+ <configurations />
39
+ </instances>
40
+ </project>
@@ -0,0 +1,86 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <project xmlns="http://www.plcopen.org/xml/tc6_0200" xmlns:xhtml="http://www.w3.org/1999/xhtml">
3
+ <fileHeader companyName="" productName="CODESYS" productVersion="CODESYS V3.5 SP21" creationDateTime="<NORMALIZED>" />
4
+ <contentHeader name="FB_StateMachine" modificationDateTime="<NORMALIZED>">
5
+ <coordinateInfo>
6
+ <fbd><scaling x="1" y="1" /></fbd>
7
+ <ld><scaling x="1" y="1" /></ld>
8
+ <sfc><scaling x="1" y="1" /></sfc>
9
+ </coordinateInfo>
10
+ </contentHeader>
11
+ <types>
12
+ <dataTypes />
13
+ <pous>
14
+ <pou name="FB_StateMachine" pouType="functionBlock">
15
+ <interface>
16
+ <inputVars>
17
+ <variable name="bExecute">
18
+ <type><BOOL /></type>
19
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Rising edge starts cycle</xhtml></documentation>
20
+ </variable>
21
+ <variable name="tTimeout">
22
+ <type><TIME /></type>
23
+ <initialValue><simpleValue value="T#5s" /></initialValue>
24
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Operation timeout</xhtml></documentation>
25
+ </variable>
26
+ </inputVars>
27
+ <outputVars>
28
+ <variable name="bDone">
29
+ <type><BOOL /></type>
30
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Cycle complete</xhtml></documentation>
31
+ </variable>
32
+ <variable name="bBusy">
33
+ <type><BOOL /></type>
34
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Cycle in progress</xhtml></documentation>
35
+ </variable>
36
+ <variable name="bErr">
37
+ <type><BOOL /></type>
38
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Cycle errored</xhtml></documentation>
39
+ </variable>
40
+ <variable name="nErrId">
41
+ <type><INT /></type>
42
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Error ID</xhtml></documentation>
43
+ </variable>
44
+ </outputVars>
45
+ <localVars>
46
+ <variable name="nState">
47
+ <type><INT /></type>
48
+ <initialValue><simpleValue value="0" /></initialValue>
49
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">State machine pointer</xhtml></documentation>
50
+ </variable>
51
+ <variable name="fbTimer">
52
+ <type><derived name="TON" /></type>
53
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Timeout watchdog</xhtml></documentation>
54
+ </variable>
55
+ </localVars>
56
+ </interface>
57
+ <body>
58
+ <ST>
59
+ <xhtml xmlns="http://www.w3.org/1999/xhtml"><![CDATA[CASE nState OF
60
+ 0: (* IDLE *)
61
+ IF bExecute THEN
62
+ nState := 10;
63
+ END_IF
64
+ 10: (* WORKING *)
65
+ bBusy := TRUE;
66
+ fbTimer(IN := TRUE, PT := tTimeout);
67
+ IF fbTimer.Q THEN
68
+ nState := 99;
69
+ nErrId := 100;
70
+ END_IF
71
+ 90: (* DONE *)
72
+ bDone := TRUE;
73
+ bBusy := FALSE;
74
+ 99: (* ERROR *)
75
+ bErr := TRUE;
76
+ bBusy := FALSE;
77
+ END_CASE]]></xhtml>
78
+ </ST>
79
+ </body>
80
+ </pou>
81
+ </pous>
82
+ </types>
83
+ <instances>
84
+ <configurations />
85
+ </instances>
86
+ </project>
@@ -0,0 +1,44 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <project xmlns="http://www.plcopen.org/xml/tc6_0200" xmlns:xhtml="http://www.w3.org/1999/xhtml">
3
+ <fileHeader companyName="" productName="CODESYS" productVersion="CODESYS V3.5 SP21" creationDateTime="<NORMALIZED>" />
4
+ <contentHeader name="PRG_Main" modificationDateTime="<NORMALIZED>">
5
+ <coordinateInfo>
6
+ <fbd><scaling x="1" y="1" /></fbd>
7
+ <ld><scaling x="1" y="1" /></ld>
8
+ <sfc><scaling x="1" y="1" /></sfc>
9
+ </coordinateInfo>
10
+ </contentHeader>
11
+ <types>
12
+ <dataTypes />
13
+ <pous>
14
+ <pou name="PRG_Main" pouType="program">
15
+ <interface>
16
+ <localVars>
17
+ <variable name="fbWorker">
18
+ <type><derived name="FB_StateMachine" /></type>
19
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Cycle worker</xhtml></documentation>
20
+ </variable>
21
+ <variable name="bStart">
22
+ <type><BOOL /></type>
23
+ <documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Operator start button</xhtml></documentation>
24
+ </variable>
25
+ </localVars>
26
+ </interface>
27
+ <body>
28
+ <ST>
29
+ <xhtml xmlns="http://www.w3.org/1999/xhtml"><![CDATA[fbWorker(
30
+ bExecute := bStart,
31
+ );
32
+
33
+ IF fbWorker.bDone THEN
34
+ bStart := FALSE;
35
+ END_IF]]></xhtml>
36
+ </ST>
37
+ </body>
38
+ </pou>
39
+ </pous>
40
+ </types>
41
+ <instances>
42
+ <configurations />
43
+ </instances>
44
+ </project>
@@ -0,0 +1,77 @@
1
+ import { copyFile, cp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { z } from "zod";
4
+ import { cyclicTasksSchema } from "./config";
5
+
6
+ export const buildFiles = {
7
+ project: "Application.project",
8
+ output: "runtime/Application.app",
9
+ checksum: "runtime/Application.crc",
10
+ receipt: "build.json",
11
+ // Normalized EDS files of a generated device tree, imported by the worker.
12
+ devices: "eds",
13
+ // With a generated device tree: the project plus the descriptions an IDE needs.
14
+ archive: "Application.projectarchive",
15
+ } as const;
16
+
17
+ const receiptSchema = z.object({
18
+ mode: z.literal("build"),
19
+ application: z.string().regex(/^[A-Za-z_][A-Za-z_0-9]*$/),
20
+ tasks: cyclicTasksSchema.optional(),
21
+ });
22
+
23
+ /** A receipt is published last, after every artifact has been checked and replaced. */
24
+ export const readBuild = async (directory: string) => {
25
+ const receipt = receiptSchema.safeParse(
26
+ JSON.parse(await readFile(resolve(directory, buildFiles.receipt), "utf8")),
27
+ );
28
+ if (!receipt.success) {
29
+ throw new Error("CODESYS did not produce a valid build receipt", {
30
+ cause: receipt.error,
31
+ });
32
+ }
33
+ for (const file of [buildFiles.project, buildFiles.output, buildFiles.checksum]) {
34
+ const artifact = await stat(resolve(directory, file));
35
+ if (!artifact.isFile() || artifact.size === 0) {
36
+ throw new Error(`CODESYS produced an empty artifact: ${file}`);
37
+ }
38
+ }
39
+ return {
40
+ ...receipt.data,
41
+ directory,
42
+ project: resolve(directory, buildFiles.project),
43
+ output: resolve(directory, buildFiles.output),
44
+ checksum: resolve(directory, buildFiles.checksum),
45
+ };
46
+ };
47
+
48
+ /** Publish a verified IDE build, with the completion receipt written last. */
49
+ export const publishBuild = async (
50
+ compiled: Awaited<ReturnType<typeof readBuild>>,
51
+ outDir: string,
52
+ ) => {
53
+ await copyFile(compiled.project, resolve(outDir, buildFiles.project));
54
+ await rm(resolve(outDir, "runtime"), { recursive: true, force: true });
55
+ await rename(resolve(compiled.directory, "runtime"), resolve(outDir, "runtime"));
56
+ await rm(resolve(outDir, buildFiles.archive), { force: true });
57
+ await cp(
58
+ resolve(compiled.directory, buildFiles.archive),
59
+ resolve(outDir, buildFiles.archive),
60
+ ).catch((cause: unknown) => {
61
+ if (!(cause && typeof cause === "object" && "code" in cause && cause.code === "ENOENT"))
62
+ throw cause;
63
+ });
64
+ await writeFile(
65
+ resolve(outDir, buildFiles.receipt),
66
+ JSON.stringify(
67
+ {
68
+ mode: "build",
69
+ application: compiled.application,
70
+ ...(compiled.tasks && { tasks: compiled.tasks }),
71
+ },
72
+ null,
73
+ 2,
74
+ ),
75
+ );
76
+ return readBuild(outDir);
77
+ };
package/src/build.ts ADDED
@@ -0,0 +1,87 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { buildFiles, publishBuild, readBuild } from "./artifacts";
5
+ import { dockerImage, type ResolvedConfig } from "./config";
6
+ import { buildInDocker } from "./docker-build";
7
+ import { createCodesysWorker, withWorkerLock } from "./host/codesys-worker";
8
+ import { loadSources } from "./sources";
9
+ import { toWindowsPath } from "./host/windows-path.ts";
10
+
11
+ export const buildProject = async (config: ResolvedConfig) => {
12
+ if (!config.docker) throw new Error("Builds require a Docker builder image.");
13
+ return buildInDocker(config, dockerImage);
14
+ };
15
+
16
+ /** The container runner owns Wine; this operation is not a public host build. */
17
+ export const buildInContainer = async (config: ResolvedConfig) => {
18
+ return withWorkerLock(config, async () => {
19
+ // Invalidate the previous build before validation, so run cannot deploy stale
20
+ // output after a failed rebuild. Keep the last project available for inspection.
21
+ await rm(resolve(config.outDir, buildFiles.receipt), { force: true });
22
+ const sources = await loadSources(config);
23
+ await access(config.template);
24
+ const worker = createCodesysWorker(config);
25
+ const session = await worker.launch();
26
+ const directory = resolve(config.outDir, ".codesys/build");
27
+ await rm(directory, { recursive: true, force: true });
28
+ await mkdir(resolve(directory, "runtime"), { recursive: true });
29
+ const template = resolve(directory, "Template.project");
30
+ await copyFile(config.template, template);
31
+ // CODESYS imports each adapter's EDS from the build directory.
32
+ const deviceTree = config.deviceTree && {
33
+ ...config.deviceTree,
34
+ devices: await Promise.all(
35
+ config.deviceTree.devices.map(async (device) => {
36
+ const eds = `${buildFiles.devices}/${device.name}.eds`;
37
+ await mkdir(resolve(directory, buildFiles.devices), { recursive: true });
38
+ await writeFile(resolve(directory, eds), device.eds);
39
+ // The digest keys project reuse on the description's content.
40
+ return { ...device, eds, sha256: createHash("sha256").update(device.eds).digest("hex") };
41
+ }),
42
+ ),
43
+ };
44
+ const request = {
45
+ libraryReferences: config.libraryReferences,
46
+ libraries: await Promise.all(
47
+ (config.libraries ?? []).map(async (library) => ({
48
+ ...library,
49
+ path: await toWindowsPath(library.path, config.codesys.wine),
50
+ })),
51
+ ),
52
+ ...sources,
53
+ task: config.task,
54
+ ...(config.tasks && { tasks: config.tasks }),
55
+ removeObjects: config.removeObjects,
56
+ symbols: config.symbols,
57
+ ...(deviceTree && { deviceTree }),
58
+ ...(config.controller && { controller: config.controller }),
59
+ mode: "build",
60
+ output: buildFiles.output,
61
+ project: buildFiles.project,
62
+ archive: buildFiles.archive,
63
+ receipt: buildFiles.receipt,
64
+ template: "Template.project",
65
+ templateHash: createHash("sha256")
66
+ .update(await readFile(template))
67
+ .digest("hex"),
68
+ };
69
+ await writeFile(resolve(directory, "request.json"), JSON.stringify(request, null, 2));
70
+ console.log(`CODESYS build: ${config.outDir}`);
71
+ try {
72
+ await worker.request(session, { action: "build" }, config.timeouts.build);
73
+ const compiled = await readBuild(directory);
74
+ if (config.tasks && JSON.stringify(compiled.tasks) !== JSON.stringify(config.tasks)) {
75
+ throw new Error("CODESYS build receipt does not match the requested task plan");
76
+ }
77
+ // Builds and runs share the lock. Removing the receipt above keeps a failed
78
+ // publication from looking complete even if only some files were replaced.
79
+ return await publishBuild(compiled, config.outDir);
80
+ } catch (cause) {
81
+ throw new Error(
82
+ `CODESYS build failed. Inspect ${directory}/progress.log, error.txt and ${session.directory}/codesys.log`,
83
+ { cause },
84
+ );
85
+ }
86
+ });
87
+ };
package/src/cli.ts ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env bun
2
+ import { parseArgs } from "node:util";
3
+ import { deviceTree } from "@tsplc/typegen";
4
+ import { loadHardware, resolveIo } from "@tsplc/typegen/hardware";
5
+ import { controllerFor, loadTsplcConfig, rawBuildConfig } from "@tsplc/config";
6
+ import { buildProject } from "./build.ts";
7
+ import { resolveConfig } from "./config.ts";
8
+ import { inspectController, updateController } from "./controller-online.ts";
9
+ import { configureDocker, configureRuntime } from "./docker-build.ts";
10
+ import { installLibraries } from "./libraries.ts";
11
+ import { runProject } from "./run.ts";
12
+ import { loadSources } from "./sources.ts";
13
+
14
+ async function main() {
15
+ const { values, positionals } = parseArgs({
16
+ allowPositionals: true,
17
+ options: {
18
+ config: { type: "string" },
19
+ help: { type: "boolean", short: "h" },
20
+ read: { type: "string", multiple: true },
21
+ "initialize-user": { type: "boolean" },
22
+ "frozen-lockfile": { type: "boolean" },
23
+ runtime: { type: "boolean" },
24
+ },
25
+ });
26
+ const command = positionals[0];
27
+ const commands = new Set([
28
+ "configure",
29
+ "install",
30
+ "check",
31
+ "build",
32
+ "run",
33
+ "download",
34
+ "online-change",
35
+ "inspect",
36
+ ]);
37
+ if (positionals.length > 1) throw new Error("Specify one command.");
38
+ if (!command || values.help) {
39
+ console.log(
40
+ "tsplc-build configure|install|check|build|run|download|online-change|inspect [--config tsplc.config.ts] [--read symbol] [--runtime]",
41
+ );
42
+ return;
43
+ }
44
+ if (!commands.has(command)) throw new Error(`Unknown command ${command}.`);
45
+ if (values.runtime && command !== "configure")
46
+ throw new Error("--runtime applies only to configure.");
47
+ if (command === "configure") {
48
+ await (values.runtime ? configureRuntime() : configureDocker());
49
+ return;
50
+ }
51
+ const { config, configFile, root } = await loadTsplcConfig(values.config);
52
+ if (command === "install") {
53
+ const libraries = await installLibraries(config.dependencies, root, {
54
+ frozen: values["frozen-lockfile"],
55
+ });
56
+ console.log(`Installed ${libraries.length} CODESYS libraries.`);
57
+ return;
58
+ }
59
+ if (!config.build) throw new Error("Configure build options in tsplc.config.ts.");
60
+ if (command === "inspect") {
61
+ // Inspection only needs connection/baseline settings; it remains usable
62
+ // without sources, EDS descriptors or installed library payloads.
63
+ console.log(
64
+ JSON.stringify(
65
+ await inspectController(
66
+ resolveConfig(
67
+ { ...config.build, controller: controllerFor(config, command) },
68
+ configFile,
69
+ ),
70
+ values.read,
71
+ ),
72
+ null,
73
+ 2,
74
+ ),
75
+ );
76
+ return;
77
+ }
78
+ if (config.compiler)
79
+ throw new Error("Use tsplc for TypeScript projects. tsplc-build accepts raw Structured Text.");
80
+ const build = rawBuildConfig(config);
81
+ const hardware = await loadHardware(config, root);
82
+ const io = resolveIo(config, hardware);
83
+ if (command === "check") {
84
+ const sources = await loadSources(resolveConfig({ ...build, io }, configFile));
85
+ console.log(`Validated ${sources.objects.length} Structured Text objects.`);
86
+ return;
87
+ }
88
+ const libraries = await installLibraries(config.dependencies, root, {
89
+ frozen: values["frozen-lockfile"],
90
+ });
91
+ const settings = resolveConfig(
92
+ {
93
+ ...build,
94
+ libraries,
95
+ io,
96
+ deviceTree: await deviceTree(config, hardware),
97
+ controller: controllerFor(config, command),
98
+ },
99
+ configFile,
100
+ );
101
+ if (command === "build") await buildProject(settings);
102
+ else if (command === "run") {
103
+ await buildProject(settings);
104
+ await runProject(settings, config.name);
105
+ } else if (command === "download" || command === "online-change")
106
+ await updateController(settings, command, {
107
+ initializeUser: values["initialize-user"],
108
+ read: values.read,
109
+ });
110
+ }
111
+ try {
112
+ await main();
113
+ } catch (cause) {
114
+ console.error(cause instanceof Error ? cause.message : String(cause));
115
+ process.exitCode = 1;
116
+ }
package/src/config.ts ADDED
@@ -0,0 +1,12 @@
1
+ export {
2
+ configSchema,
3
+ controllerSchema,
4
+ cyclicTasksSchema,
5
+ dockerImage,
6
+ defineBuildConfig as defineConfig,
7
+ resolveConfig,
8
+ loadConfig,
9
+ type CodesysBuildConfig,
10
+ type ResolvedConfig,
11
+ type CyclicTask,
12
+ } from "@tsplc/config/build";