@tsplc/build 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/dist/cli.js +1696 -0
  4. package/dist/cli.js.map +34 -0
  5. package/dist/index.js +1683 -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 +18 -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/edge-gateway/Dockerfile +17 -0
  17. package/docker/entrypoint.sh +10 -0
  18. package/docker/install/NOTICE.md +21 -0
  19. package/docker/install/core.sh +36 -0
  20. package/docker/install/decode.c +12 -0
  21. package/docker/install/device/Runtime.ico +0 -0
  22. package/docker/install/device/Runtime.png +0 -0
  23. package/docker/install/device/codesyscontrol.devdesc.xml +1502 -0
  24. package/docker/install/extract.py +43 -0
  25. package/docker/install/helper-launcher.c +46 -0
  26. package/docker/install/install.sh +94 -0
  27. package/docker/install/prerequisites.sh +15 -0
  28. package/docker/install/verify.py +133 -0
  29. package/docker/install/verify.sh +34 -0
  30. package/docker/install/wine-ncrypt/COPYING.LIB +502 -0
  31. package/docker/install/wine-ncrypt/README.md +28 -0
  32. package/docker/install/wine-ncrypt/enable-oaep.py +34 -0
  33. package/docker/install/wine-ncrypt/main.c +619 -0
  34. package/docker/install/wine-ncrypt/oaep.patch +11 -0
  35. package/docker/install/wine-signal/README.md +22 -0
  36. package/docker/install/wine-signal/build.sh +10 -0
  37. package/docker/install/wine-signal/check.sh +34 -0
  38. package/docker/install/wine-signal/noop-i386.c +2 -0
  39. package/docker/install/wine-signal/signal-state.c +105 -0
  40. package/docker/install/wine-signal/tests/avx-registers.c +53 -0
  41. package/docker/install/wine-signal/tests/frame-and-handler.c +192 -0
  42. package/docker/install/wine-signal/tests/loader-i386.c +8 -0
  43. package/docker/install/wine-signal/tests/signal-return.c +47 -0
  44. package/docker/install/wine-signal/wine +4 -0
  45. package/docker/runner.ts +156 -0
  46. package/package.json +53 -4
  47. package/python/build.py +660 -0
  48. package/python/codesys_login.py +100 -0
  49. package/python/codesys_online.py +327 -0
  50. package/python/test_build.py +444 -0
  51. package/python/test_codesys_login.py +120 -0
  52. package/python/test_codesys_online.py +184 -0
  53. package/python/test_installer_packages.py +216 -0
  54. package/python/test_installer_verification.py +307 -0
  55. package/python/test_worker.py +142 -0
  56. package/python/worker.py +102 -0
  57. package/runtime/install.sh +74 -0
  58. package/src/__tests__/fixtures/xml/fb-simple.xml +40 -0
  59. package/src/__tests__/fixtures/xml/fb-state-machine.xml +86 -0
  60. package/src/__tests__/fixtures/xml/prg-cyclic.xml +44 -0
  61. package/src/artifacts.ts +77 -0
  62. package/src/build.ts +87 -0
  63. package/src/cli.ts +116 -0
  64. package/src/config.ts +12 -0
  65. package/src/controller-online.ts +297 -0
  66. package/src/controller-session.ts +298 -0
  67. package/src/diagnostics.ts +68 -0
  68. package/src/docker-build.ts +163 -0
  69. package/src/docker-context.ts +35 -0
  70. package/src/docker-run.ts +103 -0
  71. package/src/host/codesys-command.ts +45 -0
  72. package/src/host/codesys-host.ts +145 -0
  73. package/src/host/codesys-worker.ts +348 -0
  74. package/src/host/windows-path.ts +73 -0
  75. package/src/index.ts +43 -0
  76. package/src/io.ts +127 -0
  77. package/src/libraries.ts +166 -0
  78. package/src/package-path.ts +5 -0
  79. package/src/run.ts +133 -0
  80. package/src/runtime-status.ts +21 -0
  81. package/src/sources.ts +248 -0
  82. package/src/st/declarations.ts +229 -0
  83. package/src/st/lexer.ts +125 -0
  84. package/src/st/parse.ts +202 -0
  85. package/src/st/reader.ts +50 -0
@@ -0,0 +1,163 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { cp, mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { resolve } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { readBuild } from "./artifacts";
8
+ import { dockerImage, type ResolvedConfig } from "./config";
9
+ import { prepareDockerContext } from "./docker-context.ts";
10
+ import { buildFailureFile, buildFailureSchema, CodesysCompilerError } from "./diagnostics.ts";
11
+ import { prepareContainerRun, runDocker } from "./docker-run.ts";
12
+ import { packageDirectory } from "./package-path.ts";
13
+
14
+ const execute = promisify(execFile);
15
+
16
+ /** Install the IDE and its prerequisites in the Docker builder image. */
17
+ export const configureDocker = async () => {
18
+ const image = dockerImage;
19
+ const { stdout } = await execute(
20
+ "docker",
21
+ ["info", "--format", "{{.OSType}}/{{.Architecture}}"],
22
+ { timeout: 30_000 },
23
+ ).catch((cause: unknown) => {
24
+ throw new Error("Cannot connect to Docker. Install and start Docker Desktop or Docker Engine", {
25
+ cause,
26
+ });
27
+ });
28
+ const architecture = stdout.trim();
29
+ const arm = architecture === "linux/aarch64" || architecture === "linux/arm64";
30
+ if (!arm && architecture !== "linux/x86_64" && architecture !== "linux/amd64") {
31
+ throw new Error("The builder requires a Linux Docker server on ARM64 or AMD64");
32
+ }
33
+ const context = await mkdtemp(resolve(tmpdir(), "codesys-installer-"));
34
+ let buildContext: string | undefined;
35
+ try {
36
+ buildContext = await prepareDockerContext();
37
+ const cache = resolve(context, "winetricks");
38
+ await mkdir(cache);
39
+ if (process.env["CODESYS_WINETRICKS_CACHE"]) {
40
+ await cp(process.env["CODESYS_WINETRICKS_CACHE"], cache, {
41
+ recursive: true,
42
+ });
43
+ }
44
+ console.log(
45
+ `Preparing Docker builder (${image}). Downloading and installing CODESYS 3.5.22.30 and prerequisites.`,
46
+ );
47
+ await runDocker(
48
+ [
49
+ "build",
50
+ "--platform",
51
+ "linux/amd64",
52
+ "--build-arg",
53
+ `SETUP_BASE=${arm ? "arm-setup" : "base"}`,
54
+ "--build-arg",
55
+ `SETUP_PLATFORM=${arm ? "linux/arm64" : "linux/amd64"}`,
56
+ "--build-arg",
57
+ "INSTALLER_SOURCE=installer-download",
58
+ "--progress",
59
+ "plain",
60
+ "--build-context",
61
+ `installer=${context}`,
62
+ "-f",
63
+ resolve(buildContext, "docker/Dockerfile"),
64
+ "-t",
65
+ image,
66
+ buildContext,
67
+ ],
68
+ (status) =>
69
+ `Docker configuration failed (${status}). Review the image-build output and run tsplc configure again`,
70
+ );
71
+ return image;
72
+ } finally {
73
+ await Promise.all([
74
+ rm(context, { recursive: true, force: true }),
75
+ ...(buildContext ? [rm(buildContext, { recursive: true, force: true })] : []),
76
+ ]);
77
+ }
78
+ };
79
+
80
+ export const runtimeImage = "tsplc-control-linux-sl:4.22.0.0";
81
+
82
+ /** Build the local CODESYS Control for Linux SL runtime, the same device as a production build. */
83
+ export const configureRuntime = async () => {
84
+ const context = resolve(packageDirectory, "docker/control-linux-sl");
85
+ console.log(`Preparing the local PLC runtime (${runtimeImage}).`);
86
+ await runDocker(
87
+ ["build", "--platform", "linux/amd64", "--progress", "plain", "-t", runtimeImage, context],
88
+ (status) => `Runtime image build failed (${status}). Review the image-build output`,
89
+ );
90
+ return runtimeImage;
91
+ };
92
+
93
+ export const gatewayImage = "tsplc-edge-gateway:4.22.0.0";
94
+
95
+ /** Build the CODESYS Edge Gateway that builder containers reach a plant controller through. */
96
+ export const configureGateway = async () => {
97
+ const context = resolve(packageDirectory, "docker/edge-gateway");
98
+ console.log(`Preparing the CODESYS gateway (${gatewayImage}).`);
99
+ await runDocker(
100
+ ["build", "--platform", "linux/amd64", "--progress", "plain", "-t", gatewayImage, context],
101
+ (status) => `Gateway image build failed (${status}). Review the image-build output`,
102
+ );
103
+ return gatewayImage;
104
+ };
105
+
106
+ export const buildInDocker = async (config: ResolvedConfig, image: string) => {
107
+ const requestDirectory = await mkdtemp(resolve(tmpdir(), "codesys-docker-"));
108
+ try {
109
+ const operationId = randomUUID();
110
+ await mkdir(config.outDir, { recursive: true });
111
+ const failurePath = resolve(config.outDir, buildFailureFile);
112
+ await rm(failurePath, { force: true });
113
+ const args = [
114
+ "run",
115
+ "--rm",
116
+ ...(await prepareContainerRun(config, requestDirectory, { network: "none", sources: true })),
117
+ image,
118
+ "--request",
119
+ "/input/config.json",
120
+ "--operation-id",
121
+ operationId,
122
+ ];
123
+ console.log(`Building with Docker (${image})`);
124
+ try {
125
+ await runDocker(
126
+ args,
127
+ (status) =>
128
+ `Docker build failed (${status}). Run tsplc configure if the image is missing; inspect ${config.outDir}/.codesys/docker for compiler logs`,
129
+ );
130
+ } catch (cause) {
131
+ const message = cause instanceof Error ? cause.message : String(cause);
132
+ const failure = await readFile(failurePath, "utf8")
133
+ .then((text) => buildFailureSchema.parse(JSON.parse(text)))
134
+ .catch((error: unknown) => {
135
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
136
+ throw new AggregateError(
137
+ [cause, error],
138
+ `${message}. Invalid CODESYS failure metadata (${failurePath})`,
139
+ { cause: error },
140
+ );
141
+ });
142
+ if (!failure) throw cause;
143
+ if (failure.operationId !== operationId)
144
+ throw new Error(
145
+ `${message}. CODESYS failure metadata belongs to a different Docker build`,
146
+ {
147
+ cause,
148
+ },
149
+ );
150
+ if (cause instanceof Error) {
151
+ // The mapper keeps ordinary error messages as notes, while vendor
152
+ // diagnostics supply their own text. Preserve shutdown context there.
153
+ cause.message = `${message}\n${failure.message}`;
154
+ if (failure.diagnostics.length)
155
+ cause.cause = new CodesysCompilerError(failure.message, failure.diagnostics);
156
+ }
157
+ throw cause;
158
+ }
159
+ return await readBuild(config.outDir);
160
+ } finally {
161
+ await rm(requestDirectory, { recursive: true, force: true });
162
+ }
163
+ };
@@ -0,0 +1,35 @@
1
+ import { cp, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, resolve } from "node:path";
5
+ import { packageDirectory } from "./package-path.ts";
6
+
7
+ export async function prepareDockerContext() {
8
+ const context = await mkdtemp(resolve(tmpdir(), "tsplc-docker-"));
9
+ try {
10
+ for (const directory of ["src", "python", "docker", "runtime"])
11
+ await cp(resolve(packageDirectory, directory), resolve(context, directory), {
12
+ recursive: true,
13
+ });
14
+ const configRoot = resolve(
15
+ dirname(createRequire(import.meta.url).resolve("@tsplc/config/build")),
16
+ "..",
17
+ );
18
+ await mkdir(resolve(context, "shared-config"));
19
+ await cp(resolve(configRoot, "src"), resolve(context, "shared-config/src"), {
20
+ recursive: true,
21
+ });
22
+ await cp(resolve(configRoot, "templates"), resolve(context, "shared-config/templates"), {
23
+ recursive: true,
24
+ });
25
+ await cp(resolve(configRoot, "package.json"), resolve(context, "shared-config/package.json"));
26
+ await writeFile(
27
+ resolve(context, "package.json"),
28
+ JSON.stringify({ type: "module", dependencies: { zod: "4.3.6", jiti: "2.7.0" } }),
29
+ );
30
+ return context;
31
+ } catch (cause) {
32
+ await rm(context, { recursive: true, force: true });
33
+ throw cause;
34
+ }
35
+ }
@@ -0,0 +1,103 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { basename, posix, relative, resolve, sep } from "node:path";
5
+ import type { ResolvedConfig } from "./config.ts";
6
+
7
+ /** Run a Docker client command attached to this terminal and forward interruption to it. */
8
+ export const runDocker = (args: string[], failure: (status: string | number | null) => string) =>
9
+ new Promise<void>((accept, reject) => {
10
+ const child = spawn("docker", args, {
11
+ stdio: ["inherit", process.env["TSPLC_LOG_STDERR"] === "1" ? 2 : "inherit", "inherit"],
12
+ });
13
+ const interrupt = () => child.kill("SIGINT");
14
+ const terminate = () => child.kill("SIGTERM");
15
+ process.on("SIGINT", interrupt);
16
+ process.on("SIGTERM", terminate);
17
+ const cleanup = () => {
18
+ process.off("SIGINT", interrupt);
19
+ process.off("SIGTERM", terminate);
20
+ };
21
+ child.once("error", (cause) => {
22
+ cleanup();
23
+ reject(
24
+ new Error("Cannot start Docker. Install and start Docker Desktop or Docker Engine", {
25
+ cause,
26
+ }),
27
+ );
28
+ });
29
+ child.once("exit", (code, signal) => {
30
+ cleanup();
31
+ if (code === 0) {
32
+ accept();
33
+ } else {
34
+ reject(new Error(failure(signal ?? code)));
35
+ }
36
+ });
37
+ });
38
+
39
+ const bindMount = (source: string, target: string, readonly = false) =>
40
+ `type=bind,"source=${source.replaceAll('"', '""')}",target=${target}${readonly ? ",readonly" : ""}`;
41
+
42
+ async function mountLibraries(libraries: ResolvedConfig["libraries"], directory: string) {
43
+ if (!libraries?.length) return undefined;
44
+ await mkdir(resolve(directory, "libraries"), { recursive: true });
45
+ return Promise.all(
46
+ libraries.map(async (library) => {
47
+ const data = await readFile(library.path);
48
+ if (createHash("sha256").update(data).digest("hex") !== library.sha256)
49
+ throw new Error(`Library changed after installation: ${library.title}`);
50
+ const filename = `${library.sha256}-${basename(library.path)}`;
51
+ await writeFile(resolve(directory, "libraries", filename), data);
52
+ return { ...library, path: `/input/libraries/${filename}` };
53
+ }),
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Write the builder container's request and return the `docker run` options that precede
59
+ * the image; the caller chooses `--rm` or `--detach`. The request names only container paths;
60
+ * host paths enter through these mounts, and host-only Docker, CODESYS and runtime settings
61
+ * stay outside.
62
+ */
63
+ export const prepareContainerRun = async (
64
+ config: ResolvedConfig,
65
+ requestDirectory: string,
66
+ options: { network: string; environment?: string[]; sources: boolean },
67
+ ) => {
68
+ await writeFile(
69
+ resolve(requestDirectory, "config.json"),
70
+ JSON.stringify({
71
+ ...config,
72
+ libraries: options.sources ? await mountLibraries(config.libraries, requestDirectory) : [],
73
+ configFile: undefined,
74
+ docker: undefined,
75
+ codesys: undefined,
76
+ sourceDir: "/sources",
77
+ entry: posix.resolve(
78
+ "/sources",
79
+ relative(config.sourceDir, config.entry).split(sep).join("/"),
80
+ ),
81
+ template: "/template.project",
82
+ outDir: "/output",
83
+ runtime: undefined,
84
+ }),
85
+ );
86
+ return [
87
+ "--init",
88
+ ...(config.docker?.cpuSet ? ["--cpuset-cpus", config.docker.cpuSet] : []),
89
+ "--network",
90
+ options.network,
91
+ ...(process.getuid && process.getgid
92
+ ? ["--user", `${process.getuid()}:${process.getgid()}`]
93
+ : []),
94
+ ...(options.environment ?? []).flatMap((name) => ["--env", name]),
95
+ "--mount",
96
+ bindMount(requestDirectory, "/input", true),
97
+ ...(options.sources ? ["--mount", bindMount(config.sourceDir, "/sources", true)] : []),
98
+ "--mount",
99
+ bindMount(config.template, "/template.project", true),
100
+ "--mount",
101
+ bindMount(config.outDir, "/output"),
102
+ ];
103
+ };
@@ -0,0 +1,45 @@
1
+ export { commandArgumentSchema } from "@tsplc/config/build";
2
+ import type { ResolvedConfig } from "../config";
3
+
4
+ export const codesysCommandFilename = "run.cmd";
5
+
6
+ // Keep arguments in the Windows command line: Wine's direct launcher loses the
7
+ // quoting CODESYS needs around profile names containing spaces.
8
+ export const codesysCommand = [
9
+ "@echo off",
10
+ "setlocal DisableDelayedExpansion",
11
+ '"%CODESYS_BUILD_EXE%" --profile="%CODESYS_BUILD_PROFILE%" --noUI --runscript="%CODESYS_BUILD_SCRIPT%"',
12
+ 'set "CODESYS_BUILD_STATUS=%errorlevel%"',
13
+ 'if not "%CODESYS_BUILD_STATUS%"=="0" echo CODESYS Windows exit status: %CODESYS_BUILD_STATUS%',
14
+ "exit /b %CODESYS_BUILD_STATUS%",
15
+ "",
16
+ ].join("\r\n");
17
+
18
+ export const getHostCommand = (
19
+ config: Pick<ResolvedConfig["codesys"], "commandProcessor" | "wine">,
20
+ platform: NodeJS.Platform = process.platform,
21
+ ) => {
22
+ if (platform === "win32") {
23
+ return {
24
+ args: ["/d", "/c", codesysCommandFilename],
25
+ executable: config.commandProcessor,
26
+ };
27
+ }
28
+ const wine = config.wine?.binary ?? "wine";
29
+ const args = ["cmd", "/d", "/c", codesysCommandFilename];
30
+ // --noUI suppresses the IDE, but CODESYS can still allocate a Wine console.
31
+ // A private PTY makes Wine use its Unix console instead of a desktop window.
32
+ return {
33
+ args:
34
+ platform === "darwin"
35
+ ? ["-q", "/dev/null", wine, ...args]
36
+ : [
37
+ "-q",
38
+ "-e",
39
+ "-c",
40
+ [wine, ...args].map((argument) => `'${argument.replaceAll("'", "'\\''")}'`).join(" "),
41
+ "/dev/null",
42
+ ],
43
+ executable: "script",
44
+ };
45
+ };
@@ -0,0 +1,145 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { appendFileSync, constants, renameSync, writeFileSync } from "node:fs";
3
+ import { access, open, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { delimiter, resolve } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { z } from "zod";
7
+ import type { ResolvedConfig } from "../config";
8
+ import { codesysCommand, codesysCommandFilename, getHostCommand } from "./codesys-command";
9
+ import { toWindowsPath } from "./windows-path";
10
+
11
+ const execute = promisify(execFile);
12
+ const exitSchema = z.object({
13
+ pid: z.int().positive(),
14
+ code: z.int().nullable(),
15
+ signal: z.string().nullable(),
16
+ });
17
+
18
+ export const createCodesysHost = (config: ResolvedConfig) => {
19
+ const native = process.platform === "win32";
20
+ const { codesys, timeouts } = config;
21
+ const hostEnv = {
22
+ // Child processes inherit OS settings such as PATH and DISPLAY.
23
+ ...process.env,
24
+ WINEPREFIX: codesys.wine?.prefix,
25
+ WINEDEBUG: codesys.wine?.debug,
26
+ CODESYS_BUILD_NATIVE_EXIT: String(codesys.wine?.nativeExit ?? false),
27
+ };
28
+
29
+ /** Start the headless worker script; its launcher outlives this CLI process. */
30
+ const startWorker = async (directory: string) => {
31
+ if (!native) {
32
+ await execute(codesys.wine?.binary ?? "wine", ["--version"], {
33
+ env: hostEnv,
34
+ timeout: timeouts.hostCommand,
35
+ }).catch((cause: unknown) => {
36
+ throw new Error("Wine is missing. Install Wine; Winetricks alone cannot run CODESYS", {
37
+ cause,
38
+ });
39
+ });
40
+ const candidates = (process.env["PATH"] ?? "").split(delimiter);
41
+ const available = await Promise.all(
42
+ candidates.map((path) =>
43
+ access(resolve(path, "script"), constants.X_OK).then(
44
+ () => true,
45
+ () => false,
46
+ ),
47
+ ),
48
+ );
49
+ if (!available.some(Boolean)) {
50
+ throw new Error(
51
+ "Headless Wine builds require the script utility (install util-linux on Linux)",
52
+ );
53
+ }
54
+ }
55
+ await writeFile(resolve(directory, codesysCommandFilename), codesysCommand);
56
+ const env = {
57
+ ...hostEnv,
58
+ CODESYS_BUILD_SCRIPT: await toWindowsPath(resolve(directory, "worker.py"), codesys.wine),
59
+ CODESYS_BUILD_SESSION: await toWindowsPath(directory, codesys.wine),
60
+ CODESYS_BUILD_EXE: await toWindowsPath(codesys.executable, codesys.wine),
61
+ CODESYS_BUILD_PROFILE: codesys.profile,
62
+ };
63
+ const launcher = getHostCommand(codesys);
64
+ const logPath = resolve(directory, "codesys.log");
65
+ const exitPath = resolve(directory, "launcher-exit.json");
66
+ await rm(exitPath, { force: true });
67
+ const log = await open(logPath, "w");
68
+ try {
69
+ const child = spawn(launcher.executable, launcher.args, {
70
+ cwd: directory,
71
+ detached: true,
72
+ windowsHide: true,
73
+ env,
74
+ stdio: ["ignore", log.fd, log.fd],
75
+ });
76
+ child.once("exit", (code, signal) => {
77
+ try {
78
+ appendFileSync(logPath, `\nCODESYS launcher exit status: ${signal ?? code}\n`);
79
+ } catch {
80
+ // A failed diagnostic write must preserve the original process failure.
81
+ }
82
+ try {
83
+ const temporary = `${exitPath}.${child.pid}.tmp`;
84
+ writeFileSync(temporary, JSON.stringify({ pid: child.pid, code, signal }));
85
+ renameSync(temporary, exitPath);
86
+ } catch {
87
+ // The log still records the exit if the optional status file cannot be published.
88
+ }
89
+ });
90
+ await new Promise<void>((resolve, reject) => {
91
+ child.once("error", reject);
92
+ child.once("spawn", resolve);
93
+ });
94
+ if (!child.pid) {
95
+ throw new Error("CODESYS launcher has no process ID");
96
+ }
97
+ child.unref();
98
+ return child.pid;
99
+ } finally {
100
+ await log.close();
101
+ }
102
+ };
103
+
104
+ return {
105
+ exitStatus: async (directory: string, pid: number) => {
106
+ const path = resolve(directory, "launcher-exit.json");
107
+ const value = await readFile(path, "utf8").catch((error: unknown) => {
108
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
109
+ throw error;
110
+ });
111
+ if (value === undefined) return;
112
+ const status = exitSchema.parse(JSON.parse(value));
113
+ if (status.pid !== pid) return;
114
+ if (!native && status.code === 0 && status.signal === null) {
115
+ const logPath = resolve(directory, "codesys.log");
116
+ const log = (await readFile(logPath, "utf8"))
117
+ // Terminal color escapes must not hide a native assertion signature.
118
+ // eslint-disable-next-line no-control-regex
119
+ .replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
120
+ .replace(/\r/g, "");
121
+ // A Wine helper can abort under Rosetta while the main launcher returns 0.
122
+ // That command still failed; publishing its artifacts would hide the fault.
123
+ const assertion =
124
+ /^assertion failed \[[^\n]+\]:[^\n]*\n[ \t]*\(ThreadContext[^\n]*\)/m.exec(log);
125
+ if (assertion) {
126
+ throw new Error(
127
+ `Wine translation aborted: ${assertion[0].replace(/\s+/g, " ")}. See ${logPath}`,
128
+ );
129
+ }
130
+ }
131
+ return status;
132
+ },
133
+ startWorker,
134
+ terminate: async (pid: number) => {
135
+ if (native) {
136
+ await execute("taskkill.exe", ["/pid", String(pid), "/t", "/f"], {
137
+ env: hostEnv,
138
+ timeout: timeouts.hostCommand,
139
+ });
140
+ return;
141
+ }
142
+ process.kill(-pid, "SIGKILL");
143
+ },
144
+ };
145
+ };