124 lines
4.7 KiB
Dart
124 lines
4.7 KiB
Dart
|
|
// 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:<version> 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 <sources…> <destination>`, from the build's context.
|
||
|
|
final class const CopyStep(final List<String> sources, final String destination)
|
||
|
|
extends BuildStep;
|
||
|
|
|
||
|
|
/// `RUN <command>`, through `sh -c`.
|
||
|
|
final class const RunStep(final String command) extends BuildStep;
|
||
|
|
|
||
|
|
/// `WORKDIR <path>`.
|
||
|
|
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<BuildStep> 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 = <BuildStep>[];
|
||
|
|
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:<version> 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<String> _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<String, String> copiedFiles(
|
||
|
|
CopyStep step,
|
||
|
|
Iterable<String> 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';
|
||
|
|
}
|