| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772 |
- using Invercargill;
- using Invercargill.DataStructures;
- namespace Usm.Installer {
- /**
- * Canonical source of the self-contained USM installer script baked into
- * deploy images by default (the compiled form of `installer/` in the usm
- * source tree), hosted at packages.astrologue.nz; override it per deploy
- * with `--installer-url` (whose `file://` form is the sanctioned
- * local-testing path).
- */
- public const string CANONICAL_URL = "https://packages.astrologue.nz/install-usm.sh";
- }
- /** Default container base image, overridable with `--base`. */
- const string DEPLOY_DEFAULT_BASE_IMAGE = "registry.fedoraproject.org/fedora:43";
- /** Deploy context directory created inside the packaged project. */
- const string DEPLOY_CONTEXT_DIRECTORY = ".usm-deploy";
- /** Where the USM installer tree lives inside images (installer TARGET_DIR). */
- const string DEPLOY_USM_PREFIX = "/opt/usm";
- /** In-image location that file:// repository trees are rewritten to. */
- const string DEPLOY_REPO_TREES_PATH = "/usr/share/usm-repos";
- /**
- * The system package managers `deploy --spm` can wire into the image, one
- * per shim the installer ships; {@link DeploySpm.NONE} wires none and the
- * image resolves from USM repositories alone.
- */
- private enum DeploySpm {
- DNF,
- APT,
- APK,
- EMERGE,
- NONE;
- /**
- * Whether a `--spm` value names a choice, setting {@link parsed} to it
- * (and reporting the valid values) — {@link DeploySpm.NONE} on failure.
- */
- public static bool parse(string value, out DeploySpm parsed) {
- parsed = DeploySpm.NONE;
- switch(value) {
- case "dnf":
- parsed = DeploySpm.DNF;
- return true;
- case "apt":
- parsed = DeploySpm.APT;
- return true;
- case "apk":
- parsed = DeploySpm.APK;
- return true;
- case "emerge":
- parsed = DeploySpm.EMERGE;
- return true;
- case "none":
- parsed = DeploySpm.NONE;
- return true;
- default:
- printerr(@"\"$value\" is not a valid --spm value (expected dnf, apt, apk, emerge or none)\n");
- return false;
- }
- }
- /** The lowercase name completing `usm-spm-` in shim paths. */
- public string shim_name() {
- switch(this) {
- case DeploySpm.DNF:
- return "dnf";
- case DeploySpm.APT:
- return "apt";
- case DeploySpm.APK:
- return "apk";
- case DeploySpm.EMERGE:
- return "emerge";
- default:
- return "none";
- }
- }
- /**
- * The library directory the generated config installs `lib:`
- * resources under: apk targets musl/Alpine and apt targets Debian
- * multiarch, neither of which searches /usr/lib64 (their loaders and
- * pkg-config defaults resolve /usr/lib); the rpm and portage targets
- * follow the lib64 convention.
- */
- public string lib_path() {
- return this == DeploySpm.APK || this == DeploySpm.APT ? "lib" : "lib64";
- }
- }
- /**
- * `usm deploy <package.usmc|directory> [flags…]` — the package-then-deploy
- * convenience wrapper.
- *
- * A directory is deployed directly by running the manifest deploy verb inside
- * it; a `.usmc` archive is extracted to a temporary directory first, and the
- * finished image artifact is moved back next to the invocation directory.
- * Flags pass through to {@link manifest_deploy} unchanged.
- */
- public int deploy_main(string[] args) {
- string? target = null;
- var flags = new Vector<string>();
- for(int i = 2; i < args.length; i++) {
- var argument = args[i];
- if(argument.has_prefix("--")) {
- flags.add(argument);
- if(deploy_option_takes_value(argument.split("=", 2)[0])
- && !argument.contains("=")
- && i + 1 < args.length) {
- flags.add(args[++i]);
- }
- continue;
- }
- if(target != null) {
- printerr(@"Unexpected argument \"$argument\"\n");
- return deploy_usage();
- }
- target = argument;
- }
- if(target == null) {
- return deploy_usage();
- }
- var invocation_dir = Environment.get_current_dir();
- var target_path = Path.is_absolute(target) ? target : Path.build_filename(invocation_dir, target);
- // --repository is given relative to the invocation directory, but the
- // manifest deploy verb runs inside the target project: absolutise before
- // forwarding so both interpretations agree
- var forwarded = new string[] { "usm", "deploy" };
- for(int i = 0; i < flags.length; i++) {
- var flag = flags[i];
- if(flag.has_prefix("--repository=") && !Path.is_absolute(flag.split("=", 2)[1])) {
- forwarded += @"--repository=$(Path.build_filename(invocation_dir, flag.split("=", 2)[1]))";
- }
- else if(flag == "--repository" && i + 1 < flags.length && !Path.is_absolute(flags[i + 1])) {
- forwarded += flag;
- forwarded += Path.build_filename(invocation_dir, flags[++i]);
- }
- else {
- forwarded += flag;
- }
- }
- FileInfo file_info;
- try {
- if(!File.new_for_path(target_path).query_exists()) {
- printerr(@"\"$target\" does not exist\n");
- return 255;
- }
- file_info = File.new_for_path(target_path).query_info("*", FileQueryInfoFlags.NONE);
- }
- catch(Error e) {
- printerr(@"Could not inspect \"$target\": $(e.message)\n");
- return 255;
- }
- if(file_info.get_file_type() == FileType.DIRECTORY) {
- if(!File.new_for_path(Path.build_filename(target_path, "MANIFEST.usm")).query_exists()) {
- printerr(@"\"$target\" contains no MANIFEST.usm file\n");
- return 255;
- }
- Environment.set_current_dir(target_path);
- return manifest_main(forwarded);
- }
- if(!target.has_suffix(".usmc")) {
- printerr("\"$target\" is neither a directory containing MANIFEST.usm nor a .usmc package\n");
- return deploy_usage();
- }
- // Package-then-deploy: extract the archive, deploy from the extracted
- // tree, then bring the artifact back to the invocation directory
- Usm.Manifest archive_manifest;
- var extract_dir = File.new_build_filename("/tmp", @"usm-deploy-$(Uuid.string_random())");
- try {
- archive_manifest = new Usm.Manifest.from_package(target_path);
- extract_dir.make_directory();
- Usm.Util.unarchive(target_path, extract_dir.get_path());
- }
- catch(Error e) {
- printerr(@"Could not extract \"$target\": $(e.message)\n");
- return 243;
- }
- Environment.set_current_dir(extract_dir.get_path());
- var result = manifest_main(forwarded);
- if(result == 0) {
- var artifact = @"$(archive_manifest.name)-$(archive_manifest.version.to_string()).image.tar.xz";
- try {
- var produced = File.new_for_path(Path.build_filename(extract_dir.get_path(), artifact));
- if(produced.query_exists()) {
- produced.move(File.new_for_path(Path.build_filename(invocation_dir, artifact)), FileCopyFlags.OVERWRITE);
- printerr(@"Moved image artifact to \"$invocation_dir/$artifact\"\n");
- }
- }
- catch(Error e) {
- printerr(@"The image was built, but its artifact could not be moved to \"$invocation_dir\": $(e.message)\n");
- printerr(@"It remains at \"$(extract_dir.get_path())/$artifact\"\n");
- }
- }
- printerr(@"Deploy context kept at \"$(extract_dir.get_path())\"\n");
- return result;
- }
- /**
- * `usm manifest deploy [flags…]` — generate a single-stage container deploy
- * context for the manifest in the current directory, then (unless
- * `--no-build`) build it with podman and save an xz-compressed image
- * archive:
- *
- * - `--exec CMD`: the container command, split on whitespace into the
- * exec-form ENTRYPOINT. Default: the package's single `bin:` provide
- * (an error when there are zero or several).
- * - `--base IMAGE`: base image (default {@link DEPLOY_DEFAULT_BASE_IMAGE}).
- * - `--spm dnf|apt|apk|emerge|none`: the system package manager wired into
- * the image via the matching `usm-spm-<spm>` helper and a per-SPM
- * bootstrap RUN in the Containerfile. Default `none`: no
- * `system_package_manager` section and no bootstrap RUN, so the image
- * resolves everything from USM repositories alone.
- * - `--repository FILE`: use exactly the given `.usmr` files (repeatable)
- * instead of the machine-configured repositories. `file://` repositories
- * have their trees copied into the context and their URIs rewritten to the
- * in-image location.
- * - `--installer-url URL`: override the canonical USM installer source; a
- * `file://` URL carries the script inside the context.
- * - `--no-build`: stop after generating the context.
- *
- * Only repository public keys ever enter the context or the image.
- */
- public int manifest_deploy(string[] args) {
- string? exec_command = null;
- string? base_image = null;
- string? installer_url = null;
- DeploySpm deploy_spm = DeploySpm.NONE;
- bool no_build = false;
- // The top-level --verbose scan strips the flag before this parser runs,
- // so seed from USM_VERBOSE as well
- var verbose_env = Environment.get_variable("USM_VERBOSE");
- bool verbose_deploy = verbose_env != null && verbose_env.length > 0;
- var repository_overrides = new Vector<string>();
- for(int i = 2; i < args.length; i++) {
- var argument = args[i];
- string? inline_value = null;
- if(argument.has_prefix("--") && argument.contains("=")) {
- var assignment = argument.split("=", 2);
- argument = assignment[0];
- inline_value = assignment[1];
- }
- switch(argument) {
- case "--exec":
- exec_command = deploy_option_value(args, ref i, argument, inline_value);
- if(exec_command == null) {
- return deploy_usage();
- }
- break;
- case "--base":
- base_image = deploy_option_value(args, ref i, argument, inline_value);
- if(base_image == null) {
- return deploy_usage();
- }
- break;
- case "--repository":
- var repository_file = deploy_option_value(args, ref i, argument, inline_value);
- if(repository_file == null) {
- return deploy_usage();
- }
- repository_overrides.add(repository_file);
- break;
- case "--spm":
- var spm_value = deploy_option_value(args, ref i, argument, inline_value);
- if(spm_value == null) {
- return deploy_usage();
- }
- if(!DeploySpm.parse((!)spm_value, out deploy_spm)) {
- return deploy_usage();
- }
- break;
- case "--installer-url":
- installer_url = deploy_option_value(args, ref i, argument, inline_value);
- if(installer_url == null) {
- return deploy_usage();
- }
- break;
- case "--no-build":
- no_build = true;
- break;
- case "--verbose":
- case "-v":
- verbose_deploy = true;
- break;
- default:
- printerr(@"Unknown deploy option \"$argument\"\n");
- return deploy_usage();
- }
- }
- if(manifest.is_data_package) {
- printerr("Data packages cannot be deployed: they define no build or install machinery for the in-container install.\n");
- return 246;
- }
- if(manifest.executables == null || manifest.executables.build == null) {
- printerr(@"Package \"$(manifest.name)\" defines no build executable, which the in-container \"usm install\" requires.\n");
- return 245;
- }
- var entrypoint = deploy_entrypoint(exec_command);
- if(entrypoint.length == 0) {
- return 244;
- }
- var project_dir = Environment.get_current_dir();
- var context_dir = Path.build_filename(project_dir, DEPLOY_CONTEXT_DIRECTORY);
- var version_string = manifest.version.to_string();
- printerr(@"Generating deploy context in \"$context_dir\"...\n");
- // A stale context or artifact from an earlier run must never leak into
- // the package this deploy is about to create
- if(File.new_for_path(context_dir).query_exists()) {
- try {
- Usm.Util.delete_tree(context_dir);
- }
- catch(Error e) {
- printerr(@"Could not remove the stale deploy context: $(e.message)\n");
- return 243;
- }
- }
- foreach(var stale in new string[] { @"$(manifest.name)-$version_string.image.tar", @"$(manifest.name)-$version_string.image.tar.xz" }) {
- var stale_file = File.new_for_path(Path.build_filename(project_dir, stale));
- if(stale_file.query_exists()) {
- try {
- stale_file.delete();
- }
- catch(Error e) {
- printerr(@"Could not remove the stale artifact \"$stale\": $(e.message)\n");
- return 243;
- }
- }
- }
- DirUtils.create_with_parents(context_dir, 0755);
- DirUtils.create_with_parents(Path.build_filename(context_dir, "repos"), 0755);
- DirUtils.create_with_parents(Path.build_filename(context_dir, "repo-trees"), 0755);
- DirUtils.create_with_parents(Path.build_filename(context_dir, "package"), 0755);
- printerr("Packaging the project (usm manifest package)...\n");
- try {
- var package_proc = new Subprocess.newv(new string[] { "/proc/self/exe", "manifest", "package" }, SubprocessFlags.INHERIT_FDS);
- package_proc.wait_check();
- var produced = Path.build_filename(project_dir, "..", @"$(manifest.name)-$version_string.usmc");
- File.new_for_path(produced).move(File.new_build_filename(context_dir, "package", "package.usmc"), FileCopyFlags.OVERWRITE);
- }
- catch(Error e) {
- printerr(@"Failed to package the project: $(e.message)\n");
- return 242;
- }
- bool file_trees_provisioned = false;
- var provisioned = deploy_provision_repositories(repository_overrides, context_dir, project_dir, out file_trees_provisioned);
- if(provisioned != 0) {
- return provisioned;
- }
- var effective_installer_url = installer_url ?? Usm.Installer.CANONICAL_URL;
- var bundled_installer = false;
- if(effective_installer_url.has_prefix("file://")) {
- // A file:// installer URL can only usefully refer to the container's
- // own filesystem, so the script is carried inside the context
- var installer_path = deploy_file_uri_path(effective_installer_url);
- if(installer_path == null || !File.new_for_path(installer_path).query_exists()) {
- printerr(@"--installer-url \"$effective_installer_url\" does not point at an installer script on this machine\n");
- return 241;
- }
- try {
- var local_dir = Path.build_filename(context_dir, "installer-local");
- DirUtils.create_with_parents(local_dir, 0755);
- File.new_for_path(installer_path).copy(File.new_build_filename(local_dir, "install-usm.sh"), FileCopyFlags.OVERWRITE);
- }
- catch(Error e) {
- printerr(@"Could not carry the installer script into the context: $(e.message)\n");
- return 241;
- }
- effective_installer_url = "file:///usm-installer-local/install-usm.sh";
- bundled_installer = true;
- }
- try {
- deploy_write_container_config(context_dir, deploy_spm);
- var containerfile = deploy_containerfile(
- base_image ?? DEPLOY_DEFAULT_BASE_IMAGE,
- effective_installer_url,
- bundled_installer,
- manifest.name,
- version_string,
- entrypoint,
- file_trees_provisioned,
- verbose_deploy,
- deploy_spm);
- FileUtils.set_data(Path.build_filename(context_dir, "Containerfile"), containerfile.data);
- }
- catch(Error e) {
- printerr(@"Could not write the deploy context: $(e.message)\n");
- return 240;
- }
- var tag = @"$(deploy_tag_chunk(manifest.name)):$(deploy_tag_chunk(version_string))";
- if(no_build) {
- printerr(@"Deploy context generated in \"$context_dir\" (--no-build); build it with:\n podman build -t $tag \"$context_dir\"\n");
- return 0;
- }
- printerr(@"Building image \"$tag\" with podman...\n");
- try {
- var build_proc = new Subprocess.newv(new string[] { "podman", "build", "-t", tag, context_dir }, SubprocessFlags.INHERIT_FDS);
- build_proc.wait_check();
- }
- catch(Error e) {
- printerr(@"Image build failed: $(e.message)\n");
- return 239;
- }
- var artifact_base = @"$(manifest.name)-$version_string.image.tar";
- printerr(@"Saving image to \"$artifact_base.xz\"...\n");
- try {
- var save_proc = new Subprocess.newv(new string[] { "podman", "save", "-o", artifact_base, tag }, SubprocessFlags.INHERIT_FDS);
- save_proc.wait_check();
- var compress_proc = new Subprocess.newv(new string[] { "xz", "-T0", artifact_base }, SubprocessFlags.INHERIT_FDS);
- compress_proc.wait_check();
- }
- catch(Error e) {
- printerr(@"Saving the image failed: $(e.message)\n");
- return 238;
- }
- printerr(@"Built image \"$tag\"; artifact \"$artifact_base.xz\".\n");
- printerr(@"Load and run it with:\n podman load -i \"$artifact_base.xz\"\n podman run --rm $tag\n");
- return 0;
- }
- private int deploy_usage() {
- printerr("USAGE:\n\tusm deploy <package.usmc|directory> [--exec CMD] [--base IMAGE] [--spm dnf|apt|apk|emerge|none] [--repository FILE]... [--no-build] [--installer-url URL] [--verbose]\n");
- return 255;
- }
- /** Whether a deploy option consumes the following argument as its value. */
- private bool deploy_option_takes_value(string option) {
- return option == "--exec" || option == "--base" || option == "--spm" || option == "--repository" || option == "--installer-url";
- }
- /**
- * The value of a valued option, either the `--option=value` inline form or
- * the next argument; prints the option name and returns null when no value
- * follows.
- */
- private string? deploy_option_value(string[] args, ref int index, string option, string? inline_value) {
- if(inline_value != null) {
- return inline_value;
- }
- if(index + 1 >= args.length) {
- printerr(@"Expected a value after \"$option\"\n");
- return null;
- }
- return args[++index];
- }
- /**
- * The exec-form entrypoint words: `--exec`'s command split on whitespace, or
- * by default the package's single `bin:` provide as an absolute /usr/bin
- * path. Returns an empty array (after reporting why) when `--exec` is given
- * without words or the default is ambiguous.
- */
- private string[] deploy_entrypoint(string? exec_command) {
- if(exec_command != null) {
- var words = new Vector<string>();
- foreach(var word in exec_command.split(" ")) {
- if(word.length > 0) {
- words.add(word);
- }
- }
- if(words.length == 0) {
- printerr(@"--exec \"$(exec_command)\" contains no command\n");
- return new string[0];
- }
- return words.to_array();
- }
- var binaries = new Vector<string>();
- foreach(var provide in manifest.provides) {
- if(provide.key.resource_type == Usm.ResourceType.BINARY) {
- binaries.add(provide.key.resource);
- }
- }
- if(binaries.length != 1) {
- printerr(@"Cannot pick a default entrypoint: expected exactly one \"bin:\" provide in \"$(manifest.name)\", found $(binaries.length)");
- foreach(var binary in binaries) {
- printerr(@"\n bin:$binary");
- }
- printerr("\nPass --exec to choose the container command explicitly.\n");
- return new string[0];
- }
- return new string[] { Path.build_filename("/", "usr", "bin", binaries.first_or_default()) };
- }
- /**
- * Fills the context's `repos/` and `repo-trees/` directories from either the
- * explicit {@link overrides} (exactly those `.usmr` files) or, by default,
- * the machine-configured repositories (`<config dir>/repos.d`, honouring
- * USM_CONFIGDIR like every other usm command).
- *
- * Every `.usmr` is copied with its embedded public key; a `file://` URI
- * additionally has its repository tree copied into `repo-trees/<name>/` and
- * the copied descriptor's URI rewritten to the in-image location so the
- * container resolves it without the host. Private signing key material is
- * never copied. Sets {@link file_trees_provisioned} when at least one tree
- * was carried in.
- */
- private int deploy_provision_repositories(Vector<string> overrides, string context_dir, string project_dir, out bool file_trees_provisioned) {
- file_trees_provisioned = false;
- var selected = new Vector<string>();
- if(overrides.length > 0) {
- foreach(var path in overrides) {
- selected.add(Path.is_absolute(path) ? path : Path.build_filename(project_dir, path));
- }
- }
- else {
- var repos_dir = Path.build_filename(paths.usm_config_dir, "repos.d");
- if(File.new_for_path(repos_dir).query_exists()) {
- try {
- foreach(var file in Iterate.directory(repos_dir)) {
- if(file.has_suffix(".usmr")) {
- selected.add(Path.build_filename(repos_dir, file));
- }
- }
- }
- catch(Error e) {
- printerr(@"Could not list \"$repos_dir\": $(e.message)\n");
- return 241;
- }
- }
- else {
- printerr(@"No configured repositories found in \"$repos_dir\"; the image will resolve dependencies from the system package manager alone. Pass --repository to provision USM repositories into the image.\n");
- }
- }
- foreach(var repository_file in selected) {
- Usm.Repository repository;
- try {
- repository = new Usm.Repository.from_file(repository_file);
- }
- catch(Error e) {
- printerr(@"\"$repository_file\" is not a valid repository file: $(e.message)\n");
- return 241;
- }
- var basename = Path.get_basename(repository_file);
- var stem = basename.has_suffix(".usmr") ? basename.substring(0, basename.length - ".usmr".length) : basename;
- var copied_descriptor = Path.build_filename(context_dir, "repos", basename);
- try {
- File.new_for_path(repository_file).copy(File.new_for_path(copied_descriptor), FileCopyFlags.OVERWRITE);
- if(repository.url != null && repository.url.has_prefix("file://")) {
- var tree = deploy_file_uri_path((!)repository.url);
- if(tree == null) {
- printerr(@"\"$repository_file\" has an unreadable file:// URI\n");
- return 241;
- }
- if(!File.new_for_path(tree).query_exists()) {
- printerr(@"The repository tree \"$tree\" referenced by \"$repository_file\" does not exist\n");
- return 241;
- }
- var destination = Path.build_filename(context_dir, "repo-trees", stem);
- DirUtils.create_with_parents(destination, 0755);
- deploy_copy_repository_tree(tree, destination);
- var element = new InvercargillJson.JsonElement.from_file(copied_descriptor);
- element.as<InvercargillJson.JsonObject>().set_native("url", @"file://$(DEPLOY_REPO_TREES_PATH)/$stem");
- element.write_to_file(copied_descriptor);
- printerr(@"Provisioned repository \"$stem\": tree copied from \"$tree\", URI rewritten to file://$(DEPLOY_REPO_TREES_PATH)/$stem\n");
- file_trees_provisioned = true;
- }
- else {
- printerr(@"Provisioned repository \"$stem\" from \"$repository_file\" (URI \"$(repository.url ?? "none")\")\n");
- }
- }
- catch(Error e) {
- printerr(@"Could not provision repository \"$repository_file\": $(e.message)\n");
- return 241;
- }
- }
- return 0;
- }
- /**
- * The local path a `file://` URI points at, or null when it cannot be
- * determined (GLib handles the percent-decoding).
- */
- private string? deploy_file_uri_path(string uri) {
- try {
- return Filename.from_uri(uri, null);
- }
- catch(Error e) {
- return null;
- }
- }
- /**
- * Recursively copies a repository tree into the context, refusing to carry
- * anything that looks like signing key material: only public artefacts (the
- * signed listing, packages, public keys) may ever enter a deploy context.
- */
- private void deploy_copy_repository_tree(string source, string destination) throws Error {
- var source_dir = File.new_for_path(source);
- var destination_dir = File.new_for_path(destination);
- if(!destination_dir.query_exists()) {
- destination_dir.make_directory();
- }
- var enumerator = source_dir.enumerate_children("*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
- while(true) {
- var info = enumerator.next_file();
- if(info == null) {
- break;
- }
- var name = info.get_name();
- if(info.get_file_type() == FileType.DIRECTORY) {
- if(name == "keys" || name == ".git") {
- continue;
- }
- deploy_copy_repository_tree(Path.build_filename(source, name), Path.build_filename(destination, name));
- }
- else if(!name.has_prefix("private-key")) {
- source_dir.get_child(name).copy(destination_dir.get_child(name), FileCopyFlags.OVERWRITE);
- }
- }
- }
- /**
- * Writes the minimal in-image `usm.config`: managed state under /var/usm
- * and, unless {@link spm} is {@link DeploySpm.NONE}, the system package
- * manager wired to the matching helper the installer ships so in-container
- * resolution asks the SPM before USM repositories. Hand-authored on
- * purpose — the generated image must stay independent of this machine's
- * configuration.
- */
- private void deploy_write_container_config(string context_dir, DeploySpm spm) throws Error {
- var spm_section = "";
- if(spm != DeploySpm.NONE) {
- var helper = @"$(DEPLOY_USM_PREFIX)/bin/usm-spm-$(spm.shim_name())";
- spm_section = @",
- \"system_package_manager\": {
- \"query\": [\"$helper\", \"query\"],
- \"install\": [\"$helper\", \"install\"]
- }";
- }
- var config = @"{
- \"is_managed\": true,
- \"managed\": {
- \"state_path\": \"/var/usm\"
- },
- \"paths\": {
- \"lib\": \"$(spm.lib_path())\"
- }$spm_section
- }
- ";
- FileUtils.set_data(Path.build_filename(context_dir, "usm.config"), config.data);
- }
- /**
- * Renders the single-stage Containerfile: ARG-before-FROM base image, USM
- * installed from the installer URL, the minimal configuration, repository
- * descriptors (plus rewritten `file://` trees), the package pre-seeded into
- * the USM cache and installed in-container, and the exec-form ENTRYPOINT.
- * A {@link spm} other than {@link DeploySpm.NONE} contributes the SPM's
- * bootstrap RUN (emerge needs none — the stage3 base carries portage).
- */
- private string deploy_containerfile(string base_image, string installer_url, bool bundled_installer,
- string package_name, string version_string, string[] entrypoint, bool file_trees_provisioned,
- bool verbose_deploy, DeploySpm spm) {
- var builder = new StringBuilder();
- builder.append("# Generated by `usm manifest deploy` — regenerate rather than edit\n\n");
- builder.append_printf("ARG BASE_IMAGE=%s\n", base_image);
- builder.append("FROM ${BASE_IMAGE}\n\n");
- builder.append("# Override at build time with --build-arg USM_INSTALLER_URL=...\n");
- builder.append_printf("ARG USM_INSTALLER_URL=%s\n\n", installer_url);
- if(bundled_installer) {
- builder.append("COPY installer-local/install-usm.sh /usm-installer-local/install-usm.sh\n\n");
- }
- switch(spm) {
- case DeploySpm.DNF:
- builder.append("# DNF4 python bindings for the usm SPM helper (python3 is absent from the base image)\n");
- builder.append("RUN dnf install -y python3-dnf && dnf clean all\n\n");
- break;
- case DeploySpm.APT:
- builder.append("# curl (absent from the debian base) fetches the installer; python3-apt bindings plus the apt-file contents index serve the usm SPM helper\n");
- builder.append("RUN apt-get update && apt-get install -y curl python3 python3-apt apt-file && apt-file update && apt-get clean\n\n");
- break;
- case DeploySpm.APK:
- builder.append("# The shell SPM helper needs nothing beyond busybox; bash serves the package's manage: scripts and curl (absent from the alpine base) fetches the installer\n");
- builder.append("RUN apk add --no-cache bash curl\n\n");
- break;
- case DeploySpm.EMERGE:
- case DeploySpm.NONE:
- break;
- }
- builder.append("# Install USM. The installer is downloaded to a real file first because it\n");
- builder.append("# extracts its payload relative to $0, so `curl ... | sh` cannot work.\n");
- builder.append("RUN curl -fsSL \"${USM_INSTALLER_URL}\" -o /tmp/install-usm.sh \\\n");
- builder.append(" && bash /tmp/install-usm.sh -y \\\n");
- builder.append(" && rm -f /tmp/install-usm.sh\n\n");
- builder.append("COPY usm.config /etc/usm/usm.config\n\n");
- builder.append("COPY repos/ /etc/usm/repos.d/\n");
- if(file_trees_provisioned) {
- builder.append("COPY repo-trees/ /usr/share/usm-repos/\n");
- }
- builder.append("\n");
- builder.append_printf("COPY package/package.usmc /var/usm/packages/%s-%s/package.usmc\n", package_name, version_string);
- builder.append_printf("RUN mkdir -p /var/usm/lists /var/usm/installed && usm install %s%s\n\n", package_name, verbose_deploy ? " --verbose" : "");
- builder.append_printf("ENTRYPOINT %s\n", deploy_entrypoint_json(entrypoint));
- return builder.str;
- }
- /** The exec-form JSON array for the generated ENTRYPOINT. */
- private string deploy_entrypoint_json(string[] entrypoint) {
- var words = new Vector<string>();
- foreach(var word in entrypoint) {
- words.add("\"" + word.replace("\\", "\\\\").replace("\"", "\\\"") + "\"");
- }
- return @"[$(words.to_string(w => w, ", "))]";
- }
- /**
- * A podman-safe tag chunk: tag syntax only allows `[A-Za-z0-9_.-]`, so
- * anything else (usm release suffixes like `+` in versions, for instance)
- * maps to a dash. Artifact filenames keep the unsanitised form.
- */
- private string deploy_tag_chunk(string chunk) {
- var builder = new StringBuilder();
- for(int index = 0; index < chunk.length; index++) {
- var character = chunk[index];
- var valid = (character >= 'a' && character <= 'z')
- || (character >= 'A' && character <= 'Z')
- || (character >= '0' && character <= '9')
- || character == '_' || character == '.' || character == '-';
- builder.append_unichar(valid ? character : '-');
- }
- return builder.str;
- }
|