@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,34 @@
1
+ #!/bin/sh
2
+ set -eu
3
+ sources=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
4
+ work=$(mktemp -d /tmp/tsplc-wine-signal.XXXXXX)
5
+ echo "Native check artifacts: $work"
6
+ "$sources/build.sh" "$work/compat"
7
+ gcc -Wall -Wextra -Werror -O2 -fno-tree-vectorize \
8
+ "$sources/tests/frame-and-handler.c" -ldl -o "$work/frame-and-handler"
9
+ gcc -Wall -Wextra -Werror -O2 -mavx \
10
+ "$sources/tests/avx-registers.c" -o "$work/avx-registers"
11
+ gcc -Wall -Wextra -Werror -O2 \
12
+ "$sources/tests/signal-return.c" -o "$work/signal-return"
13
+ gcc -m32 -nostdlib -Wall -Wextra -Werror -O2 -Wl,-e,_start \
14
+ -Wl,--dynamic-linker=/lib/ld-linux.so.2 \
15
+ "$sources/tests/loader-i386.c" -o "$work/loader-i386"
16
+ "$work/frame-and-handler"
17
+ # Leave the calling shell's environment untouched, just as the Wine launcher does.
18
+ preload="$work/compat/\$LIB/signal-state.so"
19
+ LD_PRELOAD="$preload" "$work/signal-return"
20
+ LD_PRELOAD="$preload" "$work/avx-registers"
21
+ LD_PRELOAD="$preload" "$work/avx-registers" nonzero
22
+ LD_DEBUG=libs,files LD_PRELOAD="$preload" "$work/signal-return" > "$work/amd64.log" 2>&1
23
+ LD_DEBUG=libs,files LD_PRELOAD="$preload" "$work/loader-i386" > "$work/i386.log" 2>&1
24
+ python3 - "$work" <<'PY_CHECK'
25
+ import pathlib, sys
26
+ work = pathlib.Path(sys.argv[1])
27
+ for architecture, library in [('amd64', 'x86_64-linux-gnu'), ('i386', 'i386-linux-gnu')]:
28
+ log = (work / (architecture + '.log')).read_text()
29
+ expected = str(work / 'compat' / 'lib' / library / 'signal-state.so')
30
+ if 'calling init: ' + expected not in log or any(message in log for message in
31
+ ['ERROR:', 'wrong ELF class', 'cannot be preloaded']):
32
+ raise SystemExit(log)
33
+ print(architecture + ' loader: ' + expected + ' PASS (no preload warnings)')
34
+ PY_CHECK
@@ -0,0 +1,2 @@
1
+ /* The 32-bit Wine loader needs no x86-64 signal-state adapter. */
2
+ void tsplc_wine_compat_noop(void) {}
@@ -0,0 +1,105 @@
1
+ #define _GNU_SOURCE
2
+ #include <dlfcn.h>
3
+ #include <errno.h>
4
+ #include <signal.h>
5
+ #include <stdatomic.h>
6
+ #include <stddef.h>
7
+ #include <stdint.h>
8
+ #include <ucontext.h>
9
+ #include <unistd.h>
10
+
11
+ /* Linux's standard, uncompacted XSAVE layout for x87, SSE and AVX only.
12
+ Restrict the adapter to the exact frame produced by the tested Rosetta ABI. */
13
+ struct __attribute__((may_alias)) signal_xstate {
14
+ unsigned char legacy[464];
15
+ uint32_t magic;
16
+ uint32_t extended_size;
17
+ uint64_t supported_features;
18
+ uint32_t xstate_size;
19
+ uint32_t software_reserved[7];
20
+ uint64_t features;
21
+ uint64_t header_reserved[7];
22
+ unsigned char ymm_high[256];
23
+ uint32_t end_magic;
24
+ };
25
+ _Static_assert(offsetof(struct signal_xstate, features) == 512, "XSAVE header offset");
26
+ _Static_assert(offsetof(struct signal_xstate, ymm_high) == 576, "AVX component offset");
27
+ _Static_assert(offsetof(struct signal_xstate, end_magic) == 832, "Linux XSAVE tail offset");
28
+
29
+ typedef void (*signal_handler)(int, siginfo_t *, void *);
30
+ typedef int (*sigaction_function)(int, const struct sigaction *, struct sigaction *);
31
+ static sigaction_function next_sigaction;
32
+ static _Atomic(signal_handler) usr1_handler;
33
+ _Static_assert(ATOMIC_POINTER_LOCK_FREE == 2, "Handler loads must be signal-safe");
34
+
35
+ static struct signal_xstate *valid_state(ucontext_t *context)
36
+ {
37
+ struct signal_xstate *state = (void *)context->uc_mcontext.fpregs;
38
+ if (!state || state->magic != UINT32_C(0x46505853)) return NULL;
39
+ /* Check lengths before accessing the extended header, component or tail. */
40
+ if (state->extended_size != 836 || state->xstate_size != 832 ||
41
+ state->supported_features != 7) return NULL;
42
+ /* The Linux public signal ABI reserves all other header words. Rosetta
43
+ leaves some uninitialized; preserve them rather than infer a format. */
44
+ if (state->end_magic != UINT32_C(0x46505845)) return NULL;
45
+ return state;
46
+ }
47
+
48
+ static void dispatch_usr1(int signal, siginfo_t *information, void *pointer)
49
+ {
50
+ ucontext_t *context = pointer;
51
+ struct signal_xstate *entry = context ? valid_state(context) : NULL;
52
+ int full_state_on_entry = entry && entry->features == 7;
53
+ signal_handler handler = atomic_load_explicit(&usr1_handler, memory_order_acquire);
54
+ handler(signal, information, pointer);
55
+ /* Wine may replace fpregs; revalidate the returned frame rather than retain
56
+ the old pointer. An absent AVX component means all YMM-high bits are zero.
57
+ Store that same architectural state explicitly for Rosetta's sigreturn. */
58
+ struct signal_xstate *returned = context && full_state_on_entry ? valid_state(context) : NULL;
59
+ if (returned && returned->features == 3) {
60
+ volatile unsigned char *ymm = returned->ymm_high;
61
+ for (size_t index = 0; index < sizeof(returned->ymm_high); ++index) ymm[index] = 0;
62
+ returned->features = 7;
63
+ }
64
+ }
65
+
66
+ __attribute__((constructor)) static void resolve_sigaction(void)
67
+ {
68
+ next_sigaction = (sigaction_function)dlsym(RTLD_NEXT, "sigaction");
69
+ if (!next_sigaction) {
70
+ const char message[] = "Wine signal adapter: cannot resolve libc sigaction\n";
71
+ ssize_t ignored = write(STDERR_FILENO, message, sizeof(message) - 1);
72
+ (void)ignored;
73
+ _exit(127);
74
+ }
75
+ }
76
+
77
+ int sigaction(int signal, const struct sigaction *action, struct sigaction *old_action)
78
+ {
79
+ if (signal != SIGUSR1) return next_sigaction(signal, action, old_action);
80
+ struct sigaction wrapped;
81
+ const struct sigaction *installed = action;
82
+ int siginfo_handler = action && (action->sa_flags & SA_SIGINFO) &&
83
+ action->sa_handler != SIG_DFL && action->sa_handler != SIG_IGN;
84
+ if (siginfo_handler) {
85
+ /* Wine installs one process-wide usr1_handler. Bind it once, so a
86
+ concurrently replaced disposition cannot redirect an active wrapper.
87
+ A different application's replacement handler goes directly to libc. */
88
+ signal_handler expected = NULL;
89
+ atomic_compare_exchange_strong_explicit(&usr1_handler, &expected, action->sa_sigaction,
90
+ memory_order_acq_rel, memory_order_acquire);
91
+ signal_handler bound = atomic_load_explicit(&usr1_handler, memory_order_acquire);
92
+ if (bound == action->sa_sigaction) {
93
+ wrapped = *action;
94
+ wrapped.sa_sigaction = dispatch_usr1;
95
+ installed = &wrapped;
96
+ }
97
+ }
98
+ int result = next_sigaction(signal, installed, old_action);
99
+ int saved_errno = errno;
100
+ if (!result && old_action && (old_action->sa_flags & SA_SIGINFO) &&
101
+ old_action->sa_sigaction == dispatch_usr1)
102
+ old_action->sa_sigaction = atomic_load_explicit(&usr1_handler, memory_order_acquire);
103
+ errno = saved_errno;
104
+ return result;
105
+ }
@@ -0,0 +1,53 @@
1
+ #define _GNU_SOURCE
2
+ #include <assert.h>
3
+ #include <signal.h>
4
+ #include <stdint.h>
5
+ #include <stdio.h>
6
+ #include <string.h>
7
+ #include <sys/syscall.h>
8
+ #include <ucontext.h>
9
+ #include <unistd.h>
10
+
11
+ static int preserve_nonzero;
12
+
13
+ static void handler(int signal, siginfo_t *information, void *pointer)
14
+ {
15
+ (void)signal;
16
+ (void)information;
17
+ unsigned char *state = (void *)((ucontext_t *)pointer)->uc_mcontext.fpregs;
18
+ uint64_t features;
19
+ memcpy(&features, state + 512, sizeof(features));
20
+ if ((features & 7) != 7) _exit(78);
21
+ if (preserve_nonzero) {
22
+ memset(state + 576, 0x3c, 256);
23
+ } else {
24
+ features &= ~UINT64_C(4);
25
+ memcpy(state + 512, &features, sizeof(features));
26
+ }
27
+ }
28
+
29
+ int main(int argc, char **argv)
30
+ {
31
+ (void)argv;
32
+ preserve_nonzero = argc > 1;
33
+ struct sigaction action = {.sa_sigaction = handler, .sa_flags = SA_SIGINFO};
34
+ sigemptyset(&action.sa_mask);
35
+ assert(!sigaction(SIGUSR1, &action, NULL));
36
+ unsigned char seed[32], returned[32];
37
+ memset(seed, 0x19, 16);
38
+ memset(seed + 16, 0x29, 16);
39
+ long process = getpid(), thread = syscall(SYS_gettid), number = SYS_tgkill;
40
+ /* Do not call a libc function between loading and reading YMM0: the SysV
41
+ calling convention would allow it to destroy the vector register. */
42
+ __asm__ volatile("vmovdqu %2, %%ymm0\n\tsyscall\n\tvmovdqu %%ymm0, %0"
43
+ : "=m"(returned), "+a"(number)
44
+ : "m"(seed), "D"(process), "S"(thread), "d"((long)SIGUSR1)
45
+ : "rcx", "r11", "memory", "ymm0");
46
+ assert(number == 0);
47
+ for (int index = 0; index < 16; ++index) assert(returned[index] == 0x19);
48
+ for (int index = 16; index < 32; ++index)
49
+ assert(returned[index] == (preserve_nonzero ? 0x3c : 0));
50
+ printf("Real AVX register return: %s PASS\n",
51
+ preserve_nonzero ? "nonzero preserved" : "absent component initialized");
52
+ return 0;
53
+ }
@@ -0,0 +1,192 @@
1
+ #include "../signal-state.c"
2
+ #include <assert.h>
3
+ #include <stdio.h>
4
+ #include <string.h>
5
+ #include <sys/mman.h>
6
+
7
+ static int calls;
8
+ static uint64_t desired_features;
9
+ static void *replacement;
10
+ static void original(int signal, siginfo_t *information, void *pointer)
11
+ {
12
+ (void)information;
13
+ assert(signal == SIGUSR1);
14
+ ++calls;
15
+ ucontext_t *context = pointer;
16
+ if (replacement) context->uc_mcontext.fpregs = replacement;
17
+ if (context->uc_mcontext.fpregs)
18
+ ((struct signal_xstate *)context->uc_mcontext.fpregs)->features = desired_features;
19
+ }
20
+ static void different(int signal, siginfo_t *information, void *pointer)
21
+ {
22
+ (void)signal;
23
+ (void)information;
24
+ (void)pointer;
25
+ }
26
+ static void simple(int signal)
27
+ {
28
+ (void)signal;
29
+ }
30
+
31
+ static struct signal_xstate frame(void)
32
+ {
33
+ struct signal_xstate value;
34
+ memset(&value, 0xa5, sizeof(value));
35
+ value.magic = 0x46505853;
36
+ value.extended_size = 836;
37
+ value.supported_features = 7;
38
+ value.xstate_size = 832;
39
+ value.features = 7;
40
+ value.header_reserved[0] = 0x6e6769732d656e69;
41
+ /* Observed unused Rosetta header data. */
42
+ value.end_magic = 0x46505845;
43
+ return value;
44
+ }
45
+ static void invoke(struct signal_xstate *value)
46
+ {
47
+ ucontext_t context = {0};
48
+ context.uc_mcontext.fpregs = (void *)value;
49
+ struct sigaction kernel;
50
+ assert(!next_sigaction(SIGUSR1, NULL, &kernel));
51
+ kernel.sa_sigaction(SIGUSR1, NULL, &context);
52
+ }
53
+ static int cases;
54
+ static void expect_unchanged(struct signal_xstate value)
55
+ {
56
+ struct signal_xstate expected = value;
57
+ expected.features = desired_features;
58
+ invoke(&value);
59
+ assert(!memcmp(&value, &expected, sizeof(value)));
60
+ ++cases;
61
+ }
62
+
63
+ int main(void)
64
+ {
65
+ struct sigaction action = {
66
+ .sa_sigaction = original,
67
+ .sa_flags = SA_SIGINFO | SA_RESTART | SA_ONSTACK
68
+ };
69
+ sigemptyset(&action.sa_mask);
70
+ sigaddset(&action.sa_mask, SIGUSR2);
71
+ struct sigaction before, query;
72
+ assert(!sigaction(SIGUSR1, &action, &before));
73
+ assert(!sigaction(SIGUSR1, NULL, &query));
74
+ assert(query.sa_sigaction == original);
75
+ assert((query.sa_flags & (SA_SIGINFO | SA_RESTART | SA_ONSTACK)) == action.sa_flags);
76
+ assert(sigismember(&query.sa_mask, SIGUSR2));
77
+ ++cases;
78
+
79
+ desired_features = 3;
80
+ struct signal_xstate value = frame(), expected = value;
81
+ memset(expected.ymm_high, 0, sizeof(expected.ymm_high));
82
+ invoke(&value);
83
+ assert(!memcmp(&value, &expected, sizeof(value)));
84
+ ++cases;
85
+
86
+ desired_features = 7;
87
+ expect_unchanged(frame());
88
+ /* Nonzero AVX state remains intact. */
89
+ desired_features = 3;
90
+ value = frame();
91
+ value.features = 3;
92
+ expect_unchanged(value);
93
+ value = frame();
94
+ value.magic = 0;
95
+ expect_unchanged(value);
96
+ value = frame();
97
+ value.extended_size = 835;
98
+ expect_unchanged(value);
99
+ value = frame();
100
+ value.extended_size = 840;
101
+ expect_unchanged(value);
102
+ value = frame();
103
+ value.xstate_size = 831;
104
+ expect_unchanged(value);
105
+ value = frame();
106
+ value.supported_features = 15;
107
+ expect_unchanged(value);
108
+ value = frame();
109
+ value.header_reserved[0] = UINT64_C(1) << 63;
110
+ expected = value;
111
+ memset(expected.ymm_high,0, sizeof(expected.ymm_high));
112
+ invoke(&value);
113
+ assert(!memcmp(&value, &expected, sizeof(value)));
114
+ ++cases;
115
+ value = frame();
116
+ value.end_magic = 0;
117
+ expect_unchanged(value);
118
+ desired_features = 1;
119
+ expect_unchanged(frame());
120
+ desired_features = 5;
121
+ expect_unchanged(frame());
122
+ desired_features = 15;
123
+ expect_unchanged(frame());
124
+ desired_features = 3;
125
+ invoke(NULL);
126
+ ++cases;
127
+
128
+ value = frame();
129
+ struct signal_xstate second = frame();
130
+ expected = second;
131
+ memset(expected.ymm_high, 0, sizeof(expected.ymm_high));
132
+ replacement = &second;
133
+ invoke(&value);
134
+ struct signal_xstate unchanged = frame();
135
+ assert(!memcmp(&value, &unchanged, sizeof(value)));
136
+ assert(!memcmp(&second, &expected, sizeof(second)));
137
+ replacement = NULL;
138
+ ++cases;
139
+
140
+ /* The legacy header can end at a protected page boundary. No extended
141
+ access is permitted after missing magic. */
142
+ long page = sysconf(_SC_PAGESIZE);
143
+ void *memory = mmap(NULL, (size_t)page * 2, PROT_READ | PROT_WRITE,
144
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
145
+ assert(memory != MAP_FAILED);
146
+ assert(!mprotect((char *)memory + page,(size_t)page, PROT_NONE));
147
+ unsigned char *legacy = (unsigned char *)memory + page - 512;
148
+ memset(legacy, 0, 512);
149
+ ucontext_t context = {0};
150
+ context.uc_mcontext.fpregs = (void *)legacy;
151
+ assert(valid_state(&context) == NULL);
152
+ munmap(memory,(size_t)page * 2);
153
+ ++cases;
154
+
155
+ struct sigaction changed = {.sa_sigaction = different, .sa_flags = SA_SIGINFO};
156
+ sigemptyset(&changed.sa_mask);
157
+ assert(!sigaction(SIGUSR1, &changed, &query));
158
+ assert(query.sa_sigaction == original);
159
+ assert(!next_sigaction(SIGUSR1, NULL, &query));
160
+ assert(query.sa_sigaction == different);
161
+ ++cases;
162
+ assert(!sigaction(SIGUSR1, &action, &query));
163
+ assert(query.sa_sigaction == different);
164
+ ++cases;
165
+ struct sigaction ignored = {.sa_handler = SIG_IGN};
166
+ sigemptyset(&ignored.sa_mask);
167
+ assert(!sigaction(SIGUSR1, &ignored, &query));
168
+ assert(query.sa_sigaction == original);
169
+ assert(!sigaction(SIGUSR1, NULL, &query));
170
+ assert(query.sa_handler == SIG_IGN);
171
+ ++cases;
172
+ struct sigaction defaults = {.sa_handler = SIG_DFL};
173
+ sigemptyset(&defaults.sa_mask);
174
+ assert(!sigaction(SIGUSR1, &defaults, &query));
175
+ assert(query.sa_handler == SIG_IGN);
176
+ assert(!sigaction(SIGUSR1, NULL, &query));
177
+ assert(query.sa_handler == SIG_DFL);
178
+ ++cases;
179
+ struct sigaction one = {.sa_handler = simple};
180
+ sigemptyset(&one.sa_mask);
181
+ assert(!sigaction(SIGUSR1, &one, &query));
182
+ assert(query.sa_handler == SIG_DFL);
183
+ assert(!next_sigaction(SIGUSR1, NULL, &query));
184
+ assert(query.sa_handler == simple);
185
+ ++cases;
186
+ assert(!sigaction(SIGUSR2, &action, NULL));
187
+ assert(!next_sigaction(SIGUSR2, NULL, &query));
188
+ assert(query.sa_sigaction == original);
189
+ ++cases;
190
+ printf("Signal adapter semantics: %d cases PASS (%d original callbacks)\n", cases, calls);
191
+ return 0;
192
+ }
@@ -0,0 +1,8 @@
1
+ /* A minimal dynamic i386 executable: its loader must expand LD_PRELOAD
2
+ correctly, then this entry point exits without depending on 32-bit libc. */
3
+ __attribute__((noreturn)) void _start(void)
4
+ {
5
+ __asm__ volatile("movl $1, %%eax\n\txorl %%ebx, %%ebx\n\tint $0x80"
6
+ : : : "eax", "ebx", "memory");
7
+ __builtin_unreachable();
8
+ }
@@ -0,0 +1,47 @@
1
+ #define _GNU_SOURCE
2
+ #include <signal.h>
3
+ #include <stdint.h>
4
+ #include <stdio.h>
5
+ #include <stdlib.h>
6
+ #include <string.h>
7
+ #include <ucontext.h>
8
+ #include <unistd.h>
9
+
10
+ static int normalize;
11
+
12
+ static void handler(int signal, siginfo_t *information, void *context)
13
+ {
14
+ (void)signal;
15
+ (void)information;
16
+ unsigned char *state = (unsigned char *)((ucontext_t *)context)->uc_mcontext.fpregs;
17
+ uint32_t magic, extended_size;
18
+ uint64_t features;
19
+ memcpy(&magic, state + 464, sizeof(magic));
20
+ memcpy(&extended_size, state + 468, sizeof(extended_size));
21
+ if (magic != 0x46505853 || extended_size < 832) _exit(77);
22
+ memcpy(&features, state + 512, sizeof(features));
23
+ if ((features & 7) != 7) _exit(78);
24
+ features &= ~UINT64_C(4);
25
+ memcpy(state + 512, &features, sizeof(features));
26
+ if (normalize) {
27
+ /* An absent AVX component requests its initial, all-zero YMM-high state.
28
+ Store that state explicitly before marking the component present. */
29
+ memset(state + 576, 0, 256);
30
+ features |= 4;
31
+ memcpy(state + 512, &features, sizeof(features));
32
+ }
33
+ ssize_t ignored = write(STDOUT_FILENO,
34
+ normalize ? "Returning features 7\n" : "Returning features 3\n", 21);
35
+ (void)ignored;
36
+ }
37
+
38
+ int main(int argc, char **argv)
39
+ {
40
+ (void)argv;
41
+ normalize = argc > 1;
42
+ struct sigaction action = { .sa_sigaction = handler, .sa_flags = SA_SIGINFO };
43
+ sigemptyset(&action.sa_mask);
44
+ if (sigaction(SIGUSR1, &action, NULL) || raise(SIGUSR1)) return 1;
45
+ puts("Signal returned: PASS");
46
+ return 0;
47
+ }
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # The dynamic loader selects the matching library for each Wine process.
3
+ export LD_PRELOAD='/opt/tsplc-wine-compat/$LIB/signal-state.so'"${LD_PRELOAD:+:$LD_PRELOAD}"
4
+ exec /opt/wine-devel/bin/wine "$@"
@@ -0,0 +1,131 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { parseArgs } from "node:util";
6
+ import { z } from "zod";
7
+ import { buildFiles, publishBuild } from "../src/artifacts";
8
+ import { buildInContainer } from "../src/build";
9
+ import { resolveContainerConfig } from "./config.ts";
10
+ import { runControllerSession } from "../src/controller-session";
11
+ import { createCodesysWorker, withWorkerShutdown } from "../src/host/codesys-worker";
12
+ import { buildFailureFile, describeBuildFailure } from "../src/diagnostics.ts";
13
+
14
+ const { values, positionals } = parseArgs({
15
+ args: process.argv.slice(2),
16
+ allowPositionals: true,
17
+ options: {
18
+ config: { type: "string" },
19
+ request: { type: "string" },
20
+ job: { type: "string" },
21
+ help: { type: "boolean", short: "h" },
22
+ "image-id": { type: "string" },
23
+ "operation-id": { type: "string" },
24
+ "initialize-user": { type: "boolean" },
25
+ read: { type: "string" },
26
+ },
27
+ });
28
+ if (values.help || (!positionals.length && !values.request && !values.job)) {
29
+ console.log(
30
+ "codesys-build build|download|online-change|inspect [--config codesys-build.config.ts]\n\nDocker contains Bun, Wine and CODESYS 3.5.22.30. Outputs are written to the configured outDir.",
31
+ );
32
+ process.exit(0);
33
+ }
34
+ if (
35
+ positionals.length &&
36
+ (positionals.length !== 1 ||
37
+ !["build", "download", "online-change", "inspect"].includes(positionals[0] ?? ""))
38
+ ) {
39
+ throw new Error("The builder container supports build, download, online-change and inspect.");
40
+ }
41
+ const configFile = resolve(values.config ?? "codesys-build.config.ts");
42
+ const inputFile = values.job ?? values.request;
43
+ const input = inputFile
44
+ ? JSON.parse(await readFile(inputFile, "utf8"))
45
+ : z
46
+ .object({ config: z.record(z.string(), z.unknown()) })
47
+ .parse(await import(pathToFileURL(configFile).href)).config;
48
+ // Host installation paths never enter Wine inside this container.
49
+ const config = resolveContainerConfig(input, configFile);
50
+ const directory = resolve(config.outDir, ".codesys/docker");
51
+ const operation = positionals[0] ?? "build";
52
+ await mkdir(directory, { recursive: true });
53
+
54
+ if (!values.job) {
55
+ const request = "/tmp/codesys-job-config.json";
56
+ await writeFile(
57
+ request,
58
+ JSON.stringify({ ...config, configFile: undefined, codesys: undefined }),
59
+ );
60
+ const child = Bun.spawn(
61
+ [
62
+ "flock",
63
+ "--nonblock",
64
+ "--conflict-exit-code",
65
+ "73",
66
+ resolve(directory, "command.lock"),
67
+ process.execPath,
68
+ import.meta.path,
69
+ "--job",
70
+ request,
71
+ operation,
72
+ ...(values["image-id"] ? ["--image-id", values["image-id"]] : []),
73
+ ...(values["operation-id"] ? ["--operation-id", values["operation-id"]] : []),
74
+ ...(values["initialize-user"] ? ["--initialize-user"] : []),
75
+ ...(values.read ? ["--read", values.read] : []),
76
+ ],
77
+ { stdin: "inherit", stdout: "inherit", stderr: "inherit" },
78
+ );
79
+ const code = await child.exited;
80
+ if (code === 73) {
81
+ console.error(`Another Docker command is using ${config.outDir}`);
82
+ }
83
+ process.exit(code);
84
+ }
85
+
86
+ if (operation !== "build") {
87
+ const imageId = z
88
+ .string()
89
+ .regex(/^sha256:[a-f0-9]{64}$/)
90
+ .parse(values["image-id"]);
91
+ const operationId = z.string().uuid().parse(values["operation-id"]);
92
+ const read = z.array(z.string().nonempty()).parse(JSON.parse(values.read ?? "[]"));
93
+ await runControllerSession(
94
+ config,
95
+ z.enum(["download", "online-change", "inspect"]).parse(operation),
96
+ {
97
+ imageId,
98
+ operationId,
99
+ read,
100
+ initializeUser: values["initialize-user"],
101
+ },
102
+ );
103
+ console.log(`Completed ${operation}. Controller state: ${config.outDir}/.codesys/controller`);
104
+ process.exit(0);
105
+ }
106
+
107
+ await rm(resolve(config.outDir, buildFiles.receipt), { force: true });
108
+ const jobConfig = { ...config, outDir: resolve(directory, "job") };
109
+ // PIDs from a previous container cannot identify processes in this job.
110
+ await rm(resolve(jobConfig.outDir, ".codesys/session.json"), { force: true });
111
+ await rm(resolve(jobConfig.outDir, ".codesys/command.lock"), { force: true });
112
+ const operationId = z.uuid().parse(values["operation-id"] ?? randomUUID());
113
+ // A worker crash must not publish a receipt that another command can deploy.
114
+ try {
115
+ const result = await withWorkerShutdown(jobConfig, createCodesysWorker(jobConfig), () =>
116
+ buildInContainer(jobConfig),
117
+ );
118
+ await publishBuild(result, config.outDir);
119
+ console.log(`Completed build. Artifacts: ${config.outDir}`);
120
+ } catch (cause) {
121
+ try {
122
+ await writeFile(
123
+ resolve(config.outDir, buildFailureFile),
124
+ JSON.stringify({ operationId, ...describeBuildFailure(cause) }, null, 2),
125
+ );
126
+ } catch (metadataError) {
127
+ // The original compiler/worker failure still determines the container exit status.
128
+ console.error("Could not save CODESYS failure metadata", metadataError);
129
+ }
130
+ throw cause;
131
+ }
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@tsplc/build",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "license": "MIT",
4
+ "description": "Build tsPLC and Structured Text projects with CODESYS in Docker: library installs, compilation, runtime download and online changes.",
5
+ "keywords": [
6
+ "plc",
7
+ "codesys",
8
+ "docker",
9
+ "structured-text"
10
+ ],
11
+ "homepage": "https://github.com/livingforjesus/tsplc#readme",
12
+ "bugs": "https://github.com/livingforjesus/tsplc/issues",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/livingforjesus/tsplc.git",
16
+ "directory": "packages/build"
17
+ },
18
+ "version": "0.1.0",
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./src/index.ts",
23
+ "bun": "./src/index.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./libraries": {
27
+ "types": "./src/libraries.ts",
28
+ "bun": "./src/libraries.ts",
29
+ "default": "./dist/libraries.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src",
35
+ "python",
36
+ "docker",
37
+ "runtime",
38
+ "!**/*.test.ts",
39
+ "!**/__tests__/**"
40
+ ],
41
+ "engines": {
42
+ "bun": ">=1.4.3"
43
+ },
44
+ "dependencies": {
45
+ "zod": "4.3.6",
46
+ "@tsplc/config": "0.1.0",
47
+ "@tsplc/typegen": "0.1.0"
48
+ },
49
+ "bin": {
50
+ "tsplc-build": "./dist/cli.js"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }