mirror of
https://github.com/toyota-connected/emb_cli.git
synced 2026-06-21 07:19:37 -07:00
feat(cross): content-address the toolchain image by sysroot + toolset
The publish skip-on-exists keyed only on sysroot_key, which omits the baked toolset (the Dockerfile's base image, apt list, and slim prune list). A toolset change with an unchanged sysroot (e.g. adding build-essential) left the old image in place — skip-on-exists matched and never rebuilt. The published image's primary tag is now `<sysrootKey>-<toolsetHash>`, where the toolset hash covers the emitted Dockerfile + .dockerignore (ToolchainImage. tagFor/imageTag). An emitter/toolset change yields a new tag, so publish rebuilds and re-points the aliases. The baked in-image paths stay keyed by sysrootKey, so a consume build still cache-hits; the build matrix references the moving :<os> alias, so it always gets the latest image. Signed-off-by: Joel Winarske <joel.winarske@gmail.com>
This commit is contained in:
@@ -1042,9 +1042,9 @@ class CrossCommand extends Command<int> {
|
||||
/// providers don't lay out a self-contained dir to bake.
|
||||
int _emitDockerfile(CrossProfile profile, CrossTarget target) {
|
||||
if (!_canBakeImage(profile)) return ExitCode.usage.code;
|
||||
final (ctx, key) = _writeImageBuildContext(profile, target);
|
||||
final (ctx, imageTag) = _writeImageBuildContext(profile, target);
|
||||
|
||||
final tag = 'emb-cross-${profile.targetTriple}:$key';
|
||||
final tag = 'emb-cross-${profile.targetTriple}:$imageTag';
|
||||
_logger
|
||||
..info('Wrote ${p.join(ctx.path, "Dockerfile")}')
|
||||
..info('Build: docker build -t $tag ${ctx.path}')
|
||||
@@ -1075,17 +1075,20 @@ class CrossCommand extends Command<int> {
|
||||
) {
|
||||
final ctx = Directory(p.dirname(profile.targetSysroot));
|
||||
final key = sysrootKey(target);
|
||||
File(p.join(ctx.path, 'Dockerfile')).writeAsStringSync(
|
||||
ToolchainImage.dockerfile(
|
||||
triple: profile.targetTriple,
|
||||
sysrootKey: key,
|
||||
toolchainVersion: target.toolchainVersion,
|
||||
),
|
||||
final dockerfile = ToolchainImage.dockerfile(
|
||||
triple: profile.targetTriple,
|
||||
sysrootKey: key,
|
||||
toolchainVersion: target.toolchainVersion,
|
||||
);
|
||||
File(
|
||||
p.join(ctx.path, '.dockerignore'),
|
||||
).writeAsStringSync(ToolchainImage.dockerignore());
|
||||
return (ctx, key);
|
||||
final dockerignore = ToolchainImage.dockerignore();
|
||||
File(p.join(ctx.path, 'Dockerfile')).writeAsStringSync(dockerfile);
|
||||
File(p.join(ctx.path, '.dockerignore')).writeAsStringSync(dockerignore);
|
||||
// Content-address the image tag by the sysroot AND the baked toolset, so an
|
||||
// emitter/toolset change yields a new tag (publish re-builds instead of
|
||||
// serving a stale image). The baked paths stay keyed by sysrootKey, so a
|
||||
// consume build still hits cache.
|
||||
final imageTag = ToolchainImage.tagFor(key, dockerfile, dockerignore);
|
||||
return (ctx, imageTag);
|
||||
}
|
||||
|
||||
/// Emit, then build + push the toolchain image to a registry. Registry-
|
||||
@@ -1113,15 +1116,16 @@ class CrossCommand extends Command<int> {
|
||||
return ExitCode.unavailable.code;
|
||||
}
|
||||
|
||||
final (ctx, key) = _writeImageBuildContext(profile, target);
|
||||
final (ctx, imageTag) = _writeImageBuildContext(profile, target);
|
||||
final plan = ImagePublishPlan(
|
||||
tool: tool,
|
||||
contextDir: ctx.path,
|
||||
imagePrefix: imagePrefix,
|
||||
// Always publish (and skip-check) the immutable content-addressed key as
|
||||
// the primary tag, so a changed manifest is never masked by a moving
|
||||
// alias; --tag values are pushed as additional aliases on top.
|
||||
tags: [key, ...tags.where((t) => t != key)],
|
||||
// Always publish (and skip-check) the content-addressed image tag
|
||||
// (sysroot + toolset) as the primary tag, so a changed manifest or
|
||||
// toolset is never masked by a moving alias; --tag values are pushed as
|
||||
// additional aliases on top.
|
||||
tags: [imageTag, ...tags.where((t) => t != imageTag)],
|
||||
push: push,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
/// Emits a Dockerfile (+ `.dockerignore`) that bakes a resolved **arm-gnu**
|
||||
/// cross toolchain + sysroot into an OCI image, so CI pulls a ready toolchain
|
||||
/// instead of re-downloading and extracting it every run.
|
||||
@@ -109,4 +113,38 @@ LABEL org.opencontainers.image.title="emb cross toolchain: $triple" \\
|
||||
$prune
|
||||
''';
|
||||
}
|
||||
|
||||
/// Content-addressed image tag `<sysrootKey>-<toolset hash>`, where the
|
||||
/// toolset hash covers [dockerfileContent] + [dockerignoreContent]. A
|
||||
/// toolset/emitter change (new base image, apt list, prune list) yields a new
|
||||
/// tag even when [sysrootKey] is unchanged, so a publish re-builds instead of
|
||||
/// skipping and serving a stale image.
|
||||
static String tagFor(
|
||||
String sysrootKey,
|
||||
String dockerfileContent,
|
||||
String dockerignoreContent,
|
||||
) {
|
||||
final hash = sha256
|
||||
.convert(utf8.encode('$dockerfileContent\n$dockerignoreContent'))
|
||||
.toString()
|
||||
.substring(0, 12);
|
||||
return '$sysrootKey-$hash';
|
||||
}
|
||||
|
||||
/// [tagFor] computed from this image's own emitted Dockerfile + dockerignore.
|
||||
static String imageTag({
|
||||
required String triple,
|
||||
required String sysrootKey,
|
||||
String? toolchainVersion,
|
||||
String fromImage = defaultFrom,
|
||||
}) => tagFor(
|
||||
sysrootKey,
|
||||
dockerfile(
|
||||
triple: triple,
|
||||
sysrootKey: sysrootKey,
|
||||
toolchainVersion: toolchainVersion,
|
||||
fromImage: fromImage,
|
||||
),
|
||||
dockerignore(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,31 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('ToolchainImage.imageTag', () {
|
||||
test('is <sysrootKey>-<12 hex toolset hash>', () {
|
||||
final tag = ToolchainImage.imageTag(
|
||||
triple: 'aarch64-none-linux-gnu',
|
||||
sysrootKey: '809d8bb7bb71',
|
||||
toolchainVersion: '12.3.rel1',
|
||||
);
|
||||
expect(tag, matches(RegExp(r'^809d8bb7bb71-[0-9a-f]{12}$')));
|
||||
});
|
||||
|
||||
test('stable for same inputs; a toolset change rekeys, prefix kept', () {
|
||||
String tag({String from = ToolchainImage.defaultFrom}) =>
|
||||
ToolchainImage.imageTag(
|
||||
triple: 't',
|
||||
sysrootKey: 'KEY',
|
||||
fromImage: from,
|
||||
);
|
||||
expect(tag(), tag()); // deterministic
|
||||
// A different baked toolset (base image) -> different suffix, same key.
|
||||
expect(tag(from: 'ubuntu:24.04'), isNot(tag()));
|
||||
expect(tag().startsWith('KEY-'), isTrue);
|
||||
expect(tag(from: 'ubuntu:24.04').startsWith('KEY-'), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
test('dockerignore keeps only toolchain + sysroot in the context', () {
|
||||
final di = ToolchainImage.dockerignore();
|
||||
expect(di, contains('*'));
|
||||
|
||||
Reference in New Issue
Block a user