using GLib; namespace Spry.Cli { /** * `spry docker` — generates the multi-stage `Dockerfile` (and * `.dockerignore`) for a spry application and optionally assembles the * build context and runs podman/docker. * * The Web-Stack trees carry fixes not present on their remotes, so the * image build COPYs every stack library from the local Web-Stack * checkout. The context layout the Dockerfile expects is invercargill/, * invercargill-json/, astralis/, invercargill-sql/, * invercargill-sql-inversion/, statum/, spry/ and the application as * app/; inversion, the one dependency outside Web-Stack, is cloned * inside the image pinned to a reviewed commit. */ public class Docker : Object { /** * The Web-Stack projects COPYed into the build context, in the * dependency order the Dockerfile builds them. */ private const string[] STACK_PROJECTS = { "Invercargill", "Invercargill-Json", "Astralis", "Invercargill-Sql", "Invercargill-Sql-Inversion", "Statum", "Spry" }; /** Writes `Dockerfile` and `.dockerignore` into `app_dir`. */ public static void generate(string app_dir, string app_name) throws Error { var vars = new HashTable(str_hash, str_equal); vars.insert("APP_NAME", app_name); Generator.write_file(Path.build_filename(app_dir, "Dockerfile"), Generator.fill(Templates.DOCKERFILE, vars)); Generator.write_file(Path.build_filename(app_dir, ".dockerignore"), Templates.DOCKERIGNORE); stdout.printf("Generated %s\n", Path.build_filename(app_dir, "Dockerfile")); stdout.printf("Generated %s\n", Path.build_filename(app_dir, ".dockerignore")); } /** * Assembles the build context under `app_dir/.docker-build/context` * (every Web-Stack project from `stack_dir`, the app itself as * app/) and builds the image with podman or docker, whichever is * found. */ public static void build(string app_dir, string app_name, string image_tag, string stack_dir) throws Error { var staging = Path.build_filename(app_dir, ".docker-build"); Generator.remove_tree(staging); var context = Path.build_filename(staging, "context"); foreach (var project in STACK_PROJECTS) { Generator.copy_tree(find_stack_project(stack_dir, project), Path.build_filename(context, project.down())); } Generator.copy_tree(app_dir, Path.build_filename(context, "app")); Generator.write_file(Path.build_filename(context, ".dockerignore"), "*/builddir/\n*/.git/\n*.sqlite*\napp/.docker-build/\napp/web-config.json\n"); var builder = Environment.find_program_in_path("podman") ?? Environment.find_program_in_path("docker"); if (builder == null) { throw Generator.cli_error("neither podman nor docker found on PATH"); } var dockerfile = Path.build_filename(context, "app", "Dockerfile"); stdout.printf("Running: %s build -t %s -f %s %s\n", (!)builder, image_tag, dockerfile, context); var launcher = new SubprocessLauncher(SubprocessFlags.NONE); var process = launcher.spawnv({ (!)builder, "build", "-t", image_tag, "-f", dockerfile, context }); process.wait_check(); stdout.printf("Built %s\n", image_tag); } private static string find_stack_project(string stack_dir, string name) throws Error { foreach (var candidate in new string[] { name, name.down() }) { var path = Path.build_filename(stack_dir, candidate); if (FileUtils.test(path, FileTest.IS_DIR) && has_meson_manifest(path)) { return path; } } throw Generator.cli_error("%s not found in %s — pass --stack-dir ", name, stack_dir); } /** * True when `path` carries a meson manifest, either at its root or * in `src/` (Invercargill and Invercargill-Json build from src/). */ private static bool has_meson_manifest(string path) { return FileUtils.test(Path.build_filename(path, "meson.build"), FileTest.EXISTS) || FileUtils.test(Path.build_filename(path, "src", "meson.build"), FileTest.EXISTS); } } }