Repository navigation
Expand file tree
/
Copy pathbuild.py
More file actions
1370 lines (1249 loc) · 46.3 KB
/
Copy pathbuild.py
File metadata and controls
1370 lines (1249 loc) · 46.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""OpenVMM, Linux-native, and Docker-backed artifact build workflows."""
from __future__ import annotations
import hashlib
import json
import os
import shlex
import shutil
import ssl
import subprocess
import sys
from pathlib import Path
from typing import TypedDict
from . import ubuntu
from .build_config import (
BuildConfig,
DistroLayerBuildConfig,
DockerBuildConfig,
InitramfsBuildConfig,
KernelBuildConfig,
OpenVmmBackend,
OpenVmmBuildConfig,
OpenVmmPlatform,
)
from .build_constants import (
AlpineBuildConstants,
AzureLinuxBuildConstants,
BuildConstants,
DockerBuildConstants,
InitramfsBuildConstants,
KernelBuildConstants,
OpenVMMBuildConstants,
UbuntuBuildConstants,
)
from .common import (
ScriptError,
artifact_path,
download_verified,
format_size,
openvmm_git_state,
require_file,
require_success,
require_tool,
run_capture,
run_checked,
sha256_file,
)
from .guests import GuestDescriptor, guest_descriptor
class ApkPackage(TypedDict):
name: str
version: str | None
architecture: str | None
license: str | None
origin: str | None
url: str | None
description: str | None
aports_commit: str | None
build_time: str | None
def _run_openvmm_command(
args: list[str | os.PathLike[str]],
*,
cwd: Path | None = None,
env: dict[str, str] | None = None,
) -> None:
command = [os.fspath(arg) for arg in args]
print(f">> {shlex.join(command)}")
if cwd is None and env is None:
run_checked(command)
elif env is None:
run_checked(command, cwd=cwd)
else:
run_checked(command, cwd=cwd, env=env)
def _assert_kernel_config(
path: Path, required: tuple[str, ...], error_prefix: str
) -> None:
configured = set(path.read_text(encoding="utf-8").splitlines())
missing = [setting for setting in required if setting not in configured]
if missing:
raise ScriptError(error_prefix + ", ".join(missing))
def _assert_direct_boot_kernel_config(path: Path) -> None:
_assert_kernel_config(
path,
KernelBuildConstants.REQUIRED_DIRECT_BOOT_CONFIG,
"kernel configuration cannot boot the ACPI-free MP-table microVM: ",
)
def _assert_virtio_console_kernel_config(path: Path) -> None:
_assert_kernel_config(
path,
KernelBuildConstants.REQUIRED_VIRTIO_CONSOLE_CONFIG,
"kernel configuration cannot provide /dev/hvc1: ",
)
def _assert_sandbox_kernel_config(path: Path) -> None:
_assert_kernel_config(
path,
KernelBuildConstants.REQUIRED_SANDBOX_CONFIG,
"kernel configuration cannot support sandbox workloads: ",
)
def _assert_shared_status_kernel_config(path: Path) -> None:
_assert_kernel_config(
path,
KernelBuildConstants.REQUIRED_SHARED_STATUS_CONFIG,
"kernel configuration cannot consume shared virtio interrupt status: ",
)
def _assert_time_abi_kernel_config(path: Path) -> None:
_assert_kernel_config(
path,
KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG,
"kernel configuration does not meet the NVX time ABI: ",
)
def _assert_hardening_kernel_config(path: Path) -> None:
_assert_kernel_config(
path,
KernelBuildConstants.REQUIRED_HARDENING_CONFIG,
"kernel configuration does not keep runtime code read-only: ",
)
modules = [
line
for line in path.read_text(encoding="utf-8").splitlines()
if line.startswith("CONFIG_") and line.endswith("=m")
]
if modules:
raise ScriptError(
"kernel configuration builds loadable modules, which are never "
"shipped: " + ", ".join(modules)
)
def _assert_watchdog_kernel_config(path: Path, *, debug: bool) -> None:
if debug:
_assert_kernel_config(
path,
KernelBuildConstants.REQUIRED_DEBUG_CONFIG,
"debug kernel configuration lacks the CI watchdogs: ",
)
return
configured = set(path.read_text(encoding="utf-8").splitlines())
enabled = [
setting
for setting in KernelBuildConstants.DEBUG_WATCHDOG_CONFIG
if setting in configured
]
if enabled:
raise ScriptError(
"production kernel configuration enables debug-only watchdogs: "
+ ", ".join(enabled)
)
def assert_required_kernel_config(path: Path, *, debug: bool = False) -> None:
"""Validate the generated configuration required by the NVX platform."""
_assert_direct_boot_kernel_config(path)
_assert_virtio_console_kernel_config(path)
_assert_sandbox_kernel_config(path)
_assert_shared_status_kernel_config(path)
_assert_time_abi_kernel_config(path)
_assert_hardening_kernel_config(path)
_assert_watchdog_kernel_config(path, debug=debug)
def _require_linux(workflow: str) -> None:
if sys.platform != "linux":
raise ScriptError(
f"{workflow} requires Linux; use build-guest without --native for Docker"
)
def _alpine_tarball(config: InitramfsBuildConfig) -> Path:
return config.work / AlpineBuildConstants.MINIROOTFS_NAME
def _kernel_patch_files() -> tuple[Path, ...]:
patches = tuple(
sorted(
(BuildConstants.REPO_ROOT / KernelBuildConstants.PATCH_DIRECTORY).glob(
"*.patch"
)
)
)
if not patches:
raise ScriptError("no kernel patches were found")
return patches
def materialize_kernel_provenance_inputs() -> None:
"""Write kernel provenance inputs from immutable run-head blobs."""
tracked = run_capture(
[
"git",
"ls-tree",
"-r",
"-z",
"--name-only",
"HEAD",
"--",
KernelBuildConstants.INPUT_CONFIG.as_posix(),
KernelBuildConstants.PATCH_DIRECTORY.as_posix(),
],
cwd=BuildConstants.REPO_ROOT,
)
require_success(tracked, "run-head kernel provenance input query")
tree_paths = tuple(
path for path in tracked.stdout.decode("utf-8").split("\0") if path
)
config_path = KernelBuildConstants.INPUT_CONFIG.as_posix()
patch_paths = tuple(
path
for path in tree_paths
if path.startswith(f"{KernelBuildConstants.PATCH_DIRECTORY.as_posix()}/")
and path.endswith(".patch")
)
if config_path not in tree_paths:
raise ScriptError("kernel config is missing from the run head")
if not patch_paths:
raise ScriptError("kernel patches are missing from the run head")
head_patch_paths = set(patch_paths)
worktree_patch_paths = {
path.relative_to(BuildConstants.REPO_ROOT).as_posix(): path
for path in (
BuildConstants.REPO_ROOT / KernelBuildConstants.PATCH_DIRECTORY
).glob("*.patch")
}
for relative in sorted(worktree_patch_paths.keys() - head_patch_paths):
worktree_patch_paths[relative].unlink()
print(f">> removed stale {relative} absent from the run head")
for relative in (config_path, *patch_paths):
blob = run_capture(
["git", "cat-file", "blob", f"HEAD:{relative}"],
cwd=BuildConstants.REPO_ROOT,
)
require_success(blob, f"run-head kernel provenance input read for {relative}")
(BuildConstants.REPO_ROOT / relative).write_bytes(blob.stdout)
print(f">> materialized {relative} from the run head")
def _kernel_source_fingerprint() -> str:
return json.dumps(
{
"version": KernelBuildConstants.VERSION,
"upstream_url": KernelBuildConstants.URL,
"upstream_archive_sha256": KernelBuildConstants.SHA256,
"patches": [
{
"path": patch.relative_to(BuildConstants.REPO_ROOT).as_posix(),
"sha256": sha256_file(patch),
}
for patch in _kernel_patch_files()
],
},
sort_keys=True,
)
def _kernel_provenance_inputs(
source_fingerprint: str,
input_config_sha256: str,
debug_config_fragment_sha256: str | None = None,
) -> dict[str, object]:
inputs: dict[str, object] = {
"source": json.loads(source_fingerprint),
"input_config": {
"path": KernelBuildConstants.INPUT_CONFIG.as_posix(),
"sha256": input_config_sha256,
},
}
if debug_config_fragment_sha256 is not None:
inputs["debug_config_fragment"] = {
"path": KernelBuildConstants.DEBUG_CONFIG_FRAGMENT.as_posix(),
"sha256": debug_config_fragment_sha256,
}
return inputs
def kernel_provenance_inputs() -> dict[str, object]:
"""Return the current source and input-config identity for a kernel build."""
return _kernel_provenance_inputs(
_kernel_source_fingerprint(),
sha256_file(BuildConstants.REPO_ROOT / KernelBuildConstants.INPUT_CONFIG),
)
def _kernel_config_symbol(line: str) -> str | None:
if line.startswith("CONFIG_") and "=" in line:
return line.split("=", 1)[0]
if line.startswith("# CONFIG_") and line.endswith(" is not set"):
return line[2 : -len(" is not set")]
return None
def merge_kernel_config_fragment(base: str, fragment: str) -> str:
"""Apply a Kconfig fragment the way ``merge_config.sh`` does.
Every symbol the fragment assigns replaces the base assignment, so
``olddefconfig`` sees one value per symbol.
"""
assignments: list[str] = []
for number, line in enumerate(fragment.splitlines(), start=1):
symbol = _kernel_config_symbol(line)
if symbol is not None:
assignments.append(line)
elif line and not line.startswith("#"):
raise ScriptError(
f"kernel config fragment line {number} is invalid: {line}"
)
if not assignments:
raise ScriptError("kernel config fragment assigns no symbols")
symbols = [_kernel_config_symbol(line) for line in assignments]
if len(set(symbols)) != len(symbols):
raise ScriptError("kernel config fragment assigns a symbol more than once")
replaced = set(symbols)
kept = [
line
for line in base.splitlines()
if _kernel_config_symbol(line) not in replaced
]
return "\n".join((*kept, *assignments)) + "\n"
def _initramfs_source_files() -> tuple[Path, ...]:
sources = [
BuildConstants.REPO_ROOT / DockerBuildConstants.DOCKERFILE,
BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "build.py",
BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "build_config.py",
BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "build_constants.py",
BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "common.py",
BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "guests.py",
*(
path
for directory in AlpineBuildConstants.GUEST_SOURCE_DIRECTORIES
for path in (BuildConstants.REPO_ROOT / directory).rglob("*")
if path.is_file()
),
]
return tuple(
sorted(
(require_file(path, "initramfs provenance input") for path in sources),
key=lambda path: path.relative_to(BuildConstants.REPO_ROOT).as_posix(),
)
)
def initramfs_provenance_inputs() -> dict[str, object]:
"""Return the current source identity for an initramfs build."""
return {
"alpine": {
"version": AlpineBuildConstants.VERSION,
"branch": AlpineBuildConstants.BRANCH,
"minirootfs_sha256": AlpineBuildConstants.MINIROOTFS_SHA256,
},
"source_files": [
{
"path": path.relative_to(BuildConstants.REPO_ROOT).as_posix(),
"sha256": sha256_file(path),
}
for path in _initramfs_source_files()
],
}
def record_openvmm_provenance(config: OpenVmmBuildConfig) -> None:
"""Bind an OpenVMM executable to the checked-out submodule revision."""
source_revision, source_clean = openvmm_git_state(config.directory)
executable = require_file(config.output, "OpenVMM release binary")
provenance = {
"format": OpenVMMBuildConstants.PROVENANCE_FORMAT,
"source_revision": source_revision,
"source_clean": source_clean,
"executable_sha256": sha256_file(executable),
}
path = config.build_directory / OpenVMMBuildConstants.PROVENANCE_NAME
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(provenance, indent=2) + "\n", encoding="utf-8")
def detect_openvmm_platform(backend: OpenVmmBackend | None = None) -> OpenVmmPlatform:
"""Select a build target without requiring runtime hypervisor access."""
if sys.platform == "win32":
if backend in (None, "whp"):
return "windows-msvc"
elif sys.platform == "linux":
if backend in (None, "kvm"):
return "linux-gnu"
if backend == "mshv":
return "linux-musl"
else:
raise ScriptError(f"OpenVMM builds are unsupported on {sys.platform}")
raise ScriptError(f"OpenVMM backend {backend!r} is unsupported on {sys.platform}")
def _restore_openvmm_packages(config: OpenVmmBuildConfig) -> None:
if config.skip_restore:
return
_run_openvmm_command(
["cargo", "xflowey", "restore-packages", "--no-compat-igvm"],
cwd=config.directory,
)
def _build_openvmm_musl(
config: OpenVmmBuildConfig,
platform: OpenVmmPlatform,
) -> None:
target = config.openvmm_target(platform)
_run_openvmm_command(["rustup", "target", "add", target])
sysroot = config.directory.resolve() / OpenVMMBuildConstants.MUSL_SYSROOT
require_file(
sysroot / "lib" / "libsymcrypt.a",
"restored OpenVMM musl SymCrypt library",
)
environment = os.environ.copy()
environment.update(
{
"X86_64_UNKNOWN_LINUX_MUSL_OPENSSL_DIR": os.fspath(sysroot),
"X86_64_UNKNOWN_LINUX_MUSL_OPENSSL_NO_VENDOR": "1",
"X86_64_UNKNOWN_LINUX_MUSL_OPENSSL_STATIC": "1",
"X86_64_UNKNOWN_LINUX_MUSL_SYMCRYPT_LIB_PATH": os.fspath(sysroot / "lib"),
"X86_64_UNKNOWN_LINUX_MUSL_SYMCRYPT_STATIC": "1",
}
)
_run_openvmm_command(
[
"cargo",
"build",
"--release",
"--target",
target,
"-p",
OpenVMMBuildConstants.PACKAGE_NAME,
"--bin",
OpenVMMBuildConstants.BINARY_NAME,
],
cwd=config.directory,
env=environment,
)
def build_openvmm(
config: OpenVmmBuildConfig,
*,
platform: OpenVmmPlatform | None = None,
) -> None:
require_file(
config.directory / "Cargo.toml",
"initialized OpenVMM submodule",
)
selected = platform or detect_openvmm_platform(config.backend)
mode = config.openvmm_build_mode(selected)
_restore_openvmm_packages(config)
if mode == "musl":
_build_openvmm_musl(config, selected)
else:
_run_openvmm_command(
[
"cargo",
"build",
"--release",
"-p",
OpenVMMBuildConstants.PACKAGE_NAME,
"--bin",
OpenVMMBuildConstants.BINARY_NAME,
],
cwd=config.directory,
)
source = require_file(
config.openvmm_target_output(selected),
f"OpenVMM {config.openvmm_target(selected)} release binary",
)
if not config.output.exists() or not source.samefile(config.output):
config.output.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, config.output)
if mode == "musl":
config.output.chmod(config.output.stat().st_mode | 0o111)
record_openvmm_provenance(config)
def build_guest(config: BuildConfig) -> None:
if config.native_guest:
unsupported = [
guest
for guest in config.selected_guests()
if not guest_descriptor(guest).native_build_supported
]
if unsupported:
raise ScriptError(
f"{guest_descriptor(unsupported[0]).distribution} initramfs builds "
"require Docker"
)
build_kernel(config.kernel)
if config.debug_kernel:
build_kernel(KernelBuildConfig.debug_variant())
for guest in config.selected_guests():
build_initramfs(config.initramfs_config(guest))
if config.guest == "all":
build_distro_layer(config.distro_layer_config())
return
build_docker_artifacts(config.docker, config.guest)
if config.debug_kernel:
build_docker_debug_kernel(config.docker)
def build_all(config: BuildConfig) -> None:
platform = detect_openvmm_platform(config.openvmm.backend)
build_guest(config)
build_openvmm(config.openvmm, platform=platform)
def prepare_kernel_source(config: KernelBuildConfig) -> tuple[Path, str]:
"""Download, verify, extract, and patch the pinned Linux source."""
version = KernelBuildConstants.VERSION
for tool in ("patch", "tar"):
require_tool(tool)
cache = config.cache_directory
downloads = cache / BuildConstants.DOWNLOAD_DIRECTORY_NAME
source_parent = cache / KernelBuildConstants.SOURCE_DIRECTORY_NAME
tarball = downloads / KernelBuildConstants.UPSTREAM_ARCHIVE_NAME
source = source_parent / KernelBuildConstants.SOURCE_NAME
stamp = source_parent / KernelBuildConstants.SOURCE_STAMP_NAME
fingerprint = _kernel_source_fingerprint()
downloads.mkdir(parents=True, exist_ok=True)
source_parent.mkdir(parents=True, exist_ok=True)
download_verified(KernelBuildConstants.URL, tarball, KernelBuildConstants.SHA256)
cached_fingerprint = stamp.read_text(encoding="utf-8") if stamp.is_file() else None
if source.is_dir() and cached_fingerprint != fingerprint:
shutil.rmtree(source)
if not source.is_dir():
stamp.unlink(missing_ok=True)
print(f">> extracting and patching Linux {version}")
run_checked(["tar", "-xf", tarball, "-C", source_parent])
if not (source / "Makefile").is_file():
raise ScriptError(f"Linux archive did not produce {source}")
for patch in _kernel_patch_files():
print(f">> applying {patch.name}")
run_checked(
["patch", "--batch", "--forward", "-p1", "-i", patch],
cwd=source,
)
stamp.write_text(fingerprint, encoding="utf-8")
return source, fingerprint
def _prepare_alpine_root(config: InitramfsBuildConfig) -> Path:
config.work.mkdir(parents=True, exist_ok=True)
tarball = _alpine_tarball(config)
download_verified(
AlpineBuildConstants.MINIROOTFS_URL,
tarball,
AlpineBuildConstants.MINIROOTFS_SHA256,
)
root = config.work / InitramfsBuildConstants.ROOT_DIRECTORY_NAME
shutil.rmtree(root, ignore_errors=True)
root.mkdir(parents=True)
require_tool("tar")
run_checked(["tar", "-xzf", tarball, "-C", root])
print(">> installing sandbox utilities into the Alpine rootfs")
_apk_add(root, *AlpineBuildConstants.PACKAGES)
resolver = root / "etc" / "resolv.conf"
resolver.unlink(missing_ok=True)
resolver.touch()
return root
def _install(source: Path, destination: Path) -> dict[str, str]:
destination.write_bytes(source.read_bytes().replace(b"\r\n", b"\n"))
destination.chmod(0o755)
return {
"source_sha256": sha256_file(source),
"binary_sha256": sha256_file(destination),
}
def _build_static_helper(
work: Path,
source: Path,
destination: Path,
cflags: tuple[str, ...] = InitramfsBuildConstants.STATIC_HELPER_CFLAGS,
compiler_name: str = "cc",
) -> dict[str, str]:
compiler = require_tool(compiler_name)
output = work / source.stem
run_checked(
[
compiler,
*cflags,
"-o",
output,
source,
]
)
shutil.copyfile(output, destination)
destination.chmod(0o755)
return {
"source_sha256": sha256_file(source),
"binary_sha256": sha256_file(output),
}
def _build_device_io_helper(work: Path, destination: Path) -> dict[str, str]:
compiler = require_tool("cc")
source = BuildConstants.REPO_ROOT / "guest" / "common" / "nvx-device-io.c"
output = work / "nvx-device-io"
run_checked(
[
compiler,
*InitramfsBuildConstants.DEVICE_IO_CFLAGS,
"-o",
output,
source,
]
)
shutil.copyfile(output, destination)
destination.chmod(0o755)
return {
"source_sha256": sha256_file(source),
"binary_sha256": sha256_file(output),
}
def _apk_add(root: Path, *packages: str) -> None:
loader = root / "lib" / "ld-musl-x86_64.so.1"
environment = os.environ.copy()
environment["LD_LIBRARY_PATH"] = f"{root / 'lib'}:{root / 'usr' / 'lib'}"
host_ca_file = ssl.get_default_verify_paths().cafile
if host_ca_file:
environment.setdefault("SSL_CERT_FILE", host_ca_file)
run_checked(
[
loader,
root / "sbin" / "apk",
"--root",
root,
"--no-cache",
"--no-interactive",
"add",
*packages,
],
env=environment,
)
def _normalize_initramfs_metadata(root: Path) -> None:
(root / "var" / "log" / "apk.log").unlink(missing_ok=True)
for path in (*root.rglob("*"), root):
try:
os.utime(
path,
(InitramfsBuildConstants.TIMESTAMP, InitramfsBuildConstants.TIMESTAMP),
follow_symlinks=False,
)
except (NotImplementedError, OSError) as error:
if not path.is_symlink():
raise ScriptError(
f"failed to normalize initramfs timestamp for {path}: {error}"
) from error
def _pack_initramfs(root: Path, output: Path) -> None:
require_tool("find")
require_tool("sort")
require_tool("cpio")
require_tool("gzip")
_normalize_initramfs_metadata(root)
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("wb") as archive:
finder = subprocess.Popen(
["find", ".", "-print0"], cwd=root, stdout=subprocess.PIPE
)
assert finder.stdout is not None
sort_environment = os.environ.copy()
sort_environment["LC_ALL"] = "C"
sorter = subprocess.Popen(
["sort", "-z"],
cwd=root,
stdin=finder.stdout,
stdout=subprocess.PIPE,
env=sort_environment,
)
finder.stdout.close()
assert sorter.stdout is not None
cpio = subprocess.Popen(
[
"cpio",
"--null",
"--quiet",
"--reproducible",
"--owner=0:0",
"-o",
"-H",
InitramfsBuildConstants.CPIO_FORMAT,
],
cwd=root,
stdin=sorter.stdout,
stdout=subprocess.PIPE,
)
sorter.stdout.close()
assert cpio.stdout is not None
gzip = subprocess.Popen(
["gzip", "-n", f"-{InitramfsBuildConstants.GZIP_COMPRESSION_LEVEL}"],
stdin=cpio.stdout,
stdout=archive,
)
cpio.stdout.close()
gzip_code = gzip.wait()
cpio_code = cpio.wait()
sorter_code = sorter.wait()
finder_code = finder.wait()
if finder_code or sorter_code or cpio_code or gzip_code:
output.unlink(missing_ok=True)
raise ScriptError(
"failed to pack initramfs "
f"(find={finder_code}, sort={sorter_code}, cpio={cpio_code}, "
f"gzip={gzip_code})"
)
def _write_apk_manifest(
root: Path,
output: Path,
helpers: dict[str, dict[str, str]],
) -> None:
installed = root / "lib" / "apk" / "db" / "installed"
packages: list[ApkPackage] = []
for record in installed.read_text(encoding="utf-8").split("\n\n"):
fields: dict[str, str] = {}
for line in record.splitlines():
if len(line) >= 2 and line[1] == ":":
fields[line[0]] = line[2:]
if "P" not in fields:
continue
packages.append(
{
"name": fields["P"],
"version": fields.get("V"),
"architecture": fields.get("A"),
"license": fields.get("L"),
"origin": fields.get("o"),
"url": fields.get("U"),
"description": fields.get("T"),
"aports_commit": fields.get("c"),
"build_time": fields.get("t"),
}
)
packages.sort(key=lambda package: package["name"])
manifest = output.with_name(
f"{output.name}{BuildConstants.PACKAGE_MANIFEST_SUFFIX}"
)
manifest.write_text(
json.dumps(
{
"format": AlpineBuildConstants.PACKAGE_MANIFEST_VERSION,
"alpine_version": AlpineBuildConstants.VERSION,
"alpine_branch": AlpineBuildConstants.BRANCH,
"architecture": AlpineBuildConstants.ARCHITECTURE,
"packages": packages,
"helpers": helpers,
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
def _install_guest_files(
config: InitramfsBuildConfig,
root: Path,
descriptor: GuestDescriptor,
) -> dict[str, dict[str, str]]:
common = BuildConstants.REPO_ROOT / BuildConstants.COMMON_GUEST_DIRECTORY
helpers: dict[str, dict[str, str]] = {}
scripts = [
("init", common / "init", root / "init"),
(
"nvx-hostmount",
common / "nvx-hostmount",
root / "sbin" / "nvx-hostmount",
),
(
"nvx-identity-probe",
common / "nvx-identity-probe",
root / "sbin" / "nvx-identity-probe",
),
(
"nvx-snapshot",
common / "nvx-snapshot",
root / "sbin" / "nvx-snapshot",
),
(
"nvx-sandbox-smoke",
common / "nvx-sandbox-smoke",
root / "sbin" / "nvx-sandbox-smoke",
),
(
"nvx-virtio-restore-probe",
common / "nvx-virtio-restore-probe",
root / "sbin" / "nvx-virtio-restore-probe",
),
]
if descriptor.sandbox_control:
alpine = BuildConstants.REPO_ROOT / AlpineBuildConstants.GUEST_DIRECTORY
scripts.extend(
[
(
"nvx-init-agent",
common / "nvx-init-agent",
root / "sbin" / "nvx-init-agent",
),
(
"nvx-container-enter",
alpine / "nvx-container-enter",
root / "sbin" / "nvx-container-enter",
),
(
"nvx-container-launch",
alpine / "nvx-container-launch",
root / "sbin" / "nvx-container-launch",
),
]
)
else:
scripts.append(
(
"nvx-bashrc",
BuildConstants.REPO_ROOT
/ UbuntuBuildConstants.GUEST_DIRECTORY
/ "nvx-bashrc",
root / "etc" / "nvx-bashrc",
)
)
for name, source, destination in scripts:
helpers[name] = _install(source, destination)
if name == "nvx-bashrc":
destination.chmod(0o644)
for name in InitramfsBuildConstants.STATIC_HELPERS:
helpers[name] = _build_static_helper(
config.work,
common / f"{name}.c",
root / "sbin" / name,
)
for name in InitramfsBuildConstants.MUSL_STATIC_HELPERS:
helpers[name] = _build_static_helper(
config.work,
common / f"{name}.c",
root / "sbin" / name,
compiler_name=InitramfsBuildConstants.MUSL_COMPILER,
)
probe = InitramfsBuildConstants.TIME_PROBE_NAME
helpers[probe] = _build_static_helper(
config.work,
common / f"{probe}.c",
root / "sbin" / probe,
InitramfsBuildConstants.TIME_PROBE_CFLAGS,
compiler_name=InitramfsBuildConstants.MUSL_COMPILER,
)
helpers["nvx-device-io"] = _build_device_io_helper(
config.work,
root / "sbin" / "nvx-device-io",
)
return helpers
def _prepare_guest_root(
config: InitramfsBuildConfig,
descriptor: GuestDescriptor,
) -> Path:
if descriptor.name == "alpine":
return _prepare_alpine_root(config)
if descriptor.name == "ubuntu":
return ubuntu.prepare_root(config.work)
raise AssertionError(f"missing rootfs preparer for {descriptor.name}")
def _write_ubuntu_manifest(
root: Path,
output: Path,
helpers: dict[str, dict[str, str]],
input_sha256: str,
) -> None:
manifest = output.with_name(
f"{output.name}{BuildConstants.PACKAGE_MANIFEST_SUFFIX}"
)
document = ubuntu.package_manifest(root, helpers)
document.update(
{
"artifact": output.name,
"artifact_sha256": sha256_file(output),
"input_sha256": input_sha256,
}
)
manifest.write_text(
json.dumps(document, indent=2) + "\n",
encoding="utf-8",
)
def _guest_customization_files(descriptor: GuestDescriptor) -> tuple[Path, ...]:
if descriptor.name == "ubuntu":
return ubuntu.customization_files()
common = tuple(
path
for path in sorted(
(BuildConstants.REPO_ROOT / BuildConstants.COMMON_GUEST_DIRECTORY).iterdir()
)
if path.is_file()
)
alpine = tuple(
path
for path in sorted(
(BuildConstants.REPO_ROOT / AlpineBuildConstants.GUEST_DIRECTORY).iterdir()
)
if path.is_file()
)
return (*common, *alpine)
def build_initramfs(config: InitramfsBuildConfig) -> None:
_require_linux("build-initramfs")
descriptor = guest_descriptor(config.guest)
output = config.output or artifact_path(descriptor.initramfs_name)
package_manifest = output.with_name(
f"{output.name}{BuildConstants.PACKAGE_MANIFEST_SUFFIX}"
)
provenance_inputs = (
initramfs_provenance_inputs() if descriptor.name == "alpine" else None
)
provenance_path = output.with_name(InitramfsBuildConstants.PROVENANCE_NAME)
if provenance_inputs is not None:
provenance_path.unlink(missing_ok=True)
root = _prepare_guest_root(config, descriptor)
helpers = _install_guest_files(config, root, descriptor)
if descriptor.name == "ubuntu":
ubuntu.apply_metadata_policy(root)
else:
_write_apk_manifest(root, output, helpers)
_pack_initramfs(root, output)
if descriptor.name == "ubuntu":
_write_ubuntu_manifest(
root,
output,
helpers,
ubuntu.converter_input_sha256(ubuntu.customization_files()),
)
if (
provenance_inputs is not None
and initramfs_provenance_inputs() != provenance_inputs
):
output.unlink(missing_ok=True)
package_manifest.unlink(missing_ok=True)
raise ScriptError("initramfs source inputs changed during the build")
if provenance_inputs is not None:
provenance_path.write_text(
json.dumps(
{
"format": InitramfsBuildConstants.PROVENANCE_FORMAT,
"inputs": provenance_inputs,
"initramfs_sha256": sha256_file(output),
"package_manifest_sha256": sha256_file(package_manifest),
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
print(f">> built {output} ({format_size(output.stat().st_size)})")
def build_distro_layer(config: DistroLayerBuildConfig) -> None:
_require_linux("build-distro-layer")
descriptor = guest_descriptor(config.guest)
if descriptor.name != "ubuntu":
raise ScriptError("build-distro-layer currently supports only --guest ubuntu")
output = config.output.resolve()
manifest = output.with_name(f"{output.name}{BuildConstants.DISTRO_MANIFEST_SUFFIX}")
existing = [str(path) for path in (output, manifest) if path.exists()]
if existing and not config.replace:
raise ScriptError(
"refusing to replace existing Ubuntu distro artifact: "