// The images built from the workspace, as tool/check.dart replays their // resolution: pub refuses in an image what it accepts on a laptop with Flutter // (the deploys of 4 Nov 2026: overrides of Flutter plugins, exit 69), and only // the deploy builds the images, on the server, after every check has passed. import 'package:path/path.dart' as p; /// The Dockerfiles whose build stage resolves the workspace with Dart's own /// image (`FROM dart: AS build`), built by the deploy. const dartImages = [ 'apps/learn/halkyon_learn_server/Dockerfile', 'apps/learn/halkyon_learn_curator/Dockerfile', 'apps/train/halkyon_train_server/Dockerfile', 'apps/todo/halkyon_todo_server/Dockerfile', ]; /// One instruction of a Dockerfile's build stage that the replay follows. sealed class const BuildStep(); /// `COPY `, from the build's context. final class const CopyStep(final List sources, final String destination) extends BuildStep; /// `RUN `, through `sh -c`. final class const RunStep(final String command) extends BuildStep; /// `WORKDIR `. final class const WorkdirStep(final String path) extends BuildStep; /// The resolution of an image: the Dart its build stage runs on, and the /// stage's steps up to and including `RUN dart pub get`. typedef ImageResolution = ({String dartVersion, List steps}); final _from = RegExp(r'^FROM dart:(\S+) AS build$'); /// The build stage of [dockerfile] (its text) up to and including its /// `RUN dart pub get`. Throws a [FormatException] where it has no such stage, /// or an instruction the replay doesn't know comes before `pub get`, so that /// a changed Dockerfile fails the check rather than being replayed wrongly. ImageResolution imageResolution(String dockerfile) { String? version; final steps = []; for (final line in _instructions(dockerfile)) { if (version == null) { version = _from.firstMatch(line)?[1]; continue; } final [instruction, ...words] = line.split(RegExp(r'\s+')); final argument = line.substring(instruction.length).trim(); switch (instruction) { case 'COPY' when words.length >= 2 && !argument.startsWith('--'): steps.add(CopyStep(words.sublist(0, words.length - 1), words.last)); case 'RUN': steps.add(RunStep(argument)); if (argument == 'dart pub get') { return (dartVersion: version, steps: steps); } case 'WORKDIR': steps.add(WorkdirStep(argument)); default: throw FormatException( "The replay of an image's resolution doesn't know: $line", ); } } throw FormatException( version == null ? 'No build stage FROM dart: AS build' : 'No RUN dart pub get in the build stage', ); } /// The Dockerfile's instructions, comments and blank lines left out, lines /// continued with a backslash joined. Iterable _instructions(String dockerfile) sync* { var pending = ''; for (final line in dockerfile.split('\n')) { final trimmed = line.trim(); if (trimmed.isEmpty || trimmed.startsWith('#')) { continue; } if (trimmed.endsWith(r'\')) { pending += '${trimmed.substring(0, trimmed.length - 1).trim()} '; continue; } yield pending + trimmed; pending = ''; } } /// Where [step] puts each of [files] (the repository's files, paths relative /// to the build's context), relative to [workdir], as Docker's `COPY` does: /// a folder's content into the destination, a file into it where it ends in /// a slash or several sources are named, else as the destination itself. Map copiedFiles( CopyStep step, Iterable files, { String workdir = '/app', }) { final destination = p.posix.normalize( p.posix.join(workdir, step.destination), ); final intoFolder = step.destination.endsWith('/') || step.destination == '.' || step.sources.length > 1; return { for (final source in step.sources.map(p.posix.normalize)) for (final file in files) if (file == source) file: intoFolder ? p.posix.join(destination, p.posix.basename(file)) : destination else if (p.posix.isWithin(source, file)) file: p.posix.join(destination, p.posix.relative(file, from: source)), }; } /// The name of the Dart SDK's archive for this machine, as Google's archive /// names it (`dartsdk-linux-x64-release.zip`); [version] is /// `Platform.version`, and [os] `Platform.operatingSystem`. String dartSdkArchive(String os, String version) { final arch = RegExp(r'"\w+_(\w+)"').firstMatch(version)?[1] ?? 'x64'; return 'dartsdk-${os == 'macos' ? 'macos' : 'linux'}-$arch-release.zip'; }