Skip to content

Commit 754c753

Browse files
authored
chore(cli): Re-implement celest build (#279)
- Reimplement `celest build` to use containers maintained by others instead of Celest-specific containers - Add container tests
1 parent 920cc8b commit 754c753

File tree

7 files changed

+192
-51
lines changed

7 files changed

+192
-51
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ doc/api/
44
**/doc/api/
55
pubspec_overrides.yaml
66
pubspec.lock
7+
.flutter-plugins
8+
.flutter-plugins-dependencies
79

810
## RUST ##
911
# will have compiled files and executables

apps/cli/fixtures/fixtures_test.dart

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
@Tags(['goldens'])
44
library;
55

6+
import 'dart:async';
67
import 'dart:convert';
78
import 'dart:io' hide Directory;
89
import 'dart:isolate';
910
import 'dart:math';
1011

1112
import 'package:async/async.dart';
1213
import 'package:celest/src/runtime/serve.dart';
14+
import 'package:celest_ast/celest_ast.dart' as ast;
1315
import 'package:celest_cli/src/analyzer/analysis_result.dart';
1416
import 'package:celest_cli/src/analyzer/celest_analyzer.dart';
1517
import 'package:celest_cli/src/codegen/client_code_generator.dart';
@@ -18,12 +20,15 @@ import 'package:celest_cli/src/compiler/api/local_api_runner.dart';
1820
import 'package:celest_cli/src/context.dart';
1921
import 'package:celest_cli/src/database/project/project_database.dart';
2022
import 'package:celest_cli/src/env/config_value_solver.dart';
23+
import 'package:celest_cli/src/frontend/celest_frontend.dart';
2124
import 'package:celest_cli/src/init/project_migrator.dart';
25+
import 'package:celest_cli/src/process/port_finder.dart';
2226
import 'package:celest_cli/src/project/celest_project.dart';
2327
import 'package:celest_cli/src/project/project_linker.dart';
2428
import 'package:celest_cli/src/pub/pub_action.dart';
2529
import 'package:celest_cli/src/sdk/dart_sdk.dart';
2630
import 'package:celest_cli/src/sdk/sdk_finder.dart';
31+
import 'package:celest_cli/src/utils/cli.dart';
2732
import 'package:celest_cli/src/utils/recase.dart';
2833
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
2934
import 'package:file/file.dart';
@@ -189,6 +194,7 @@ class TestRunner {
189194
testResolve();
190195
testCodegen();
191196
testClient();
197+
testBuild();
192198

193199
final apisDir = fileSystem.directory(
194200
p.join(projectRoot, 'lib', 'src', 'functions'),
@@ -372,6 +378,78 @@ class TestRunner {
372378
});
373379
}
374380

381+
void testBuild() {
382+
// Docker only available on Linux runner in CI.
383+
final isCI = platform.environment.containsKey('CI');
384+
final shouldRun = !isCI || platform.isLinux;
385+
test('build', skip: !shouldRun, () async {
386+
final CelestAnalysisResult(:project, :errors) =
387+
await analyzer.analyzeProject(
388+
migrateProject: false,
389+
updateResources: updateGoldens,
390+
);
391+
expect(errors, isEmpty);
392+
expect(project, isNotNull);
393+
394+
final frontend = CelestFrontend();
395+
final result = await frontend.build(
396+
migrateProject: false,
397+
currentProgress: cliLogger.progress('Building project...'),
398+
environmentId: 'production',
399+
);
400+
expect(result, 0);
401+
402+
final outputDir = projectPaths.buildDir;
403+
final imageName = '$testName-${Random().nextInt(1000000)}';
404+
final dockerBuild = await processManager.run(
405+
['docker', 'build', '-t', imageName, '.'],
406+
workingDirectory: outputDir,
407+
);
408+
expect(
409+
dockerBuild.exitCode,
410+
0,
411+
reason: '${dockerBuild.stdout}\n${dockerBuild.stderr}',
412+
);
413+
414+
addTearDown(() async {
415+
await processManager.run(
416+
['docker', 'image', 'rm', imageName],
417+
);
418+
});
419+
420+
final openPort = await RandomPortFinder().findOpenPort();
421+
final dockerRun = await processManager.start(
422+
[
423+
'docker',
424+
'run',
425+
'--rm',
426+
'-p',
427+
'$openPort:8080',
428+
for (final database in project!.databases.values)
429+
if (database.config case ast.CelestDatabaseConfig(:final hostname))
430+
'--env=${hostname.name}=file::memory:',
431+
if (project.auth?.providers.isNotEmpty ?? false)
432+
'--env=CELEST_AUTH_DATABASE_HOST=file::memory:',
433+
imageName,
434+
],
435+
);
436+
addTearDown(() => dockerRun.kill(ProcessSignal.sigkill));
437+
438+
final dockerOutput = StreamController<String>.broadcast(sync: true);
439+
unawaited(dockerRun.exitCode.then((_) => dockerOutput.close()));
440+
441+
dockerRun
442+
..captureStdout()
443+
..captureStdout(sink: dockerOutput.add)
444+
..captureStderr()
445+
..captureStderr(sink: dockerOutput.add);
446+
await expectLater(
447+
dockerOutput.stream,
448+
emitsThrough('Serving on http://localhost:8080'),
449+
);
450+
});
451+
}
452+
375453
void testApis(Directory apisDir, List<String>? includeApis) {
376454
final apis = testCases?.apis ?? const {};
377455
if (apis.isEmpty) {
@@ -417,7 +495,15 @@ class TestRunner {
417495
apiRunner = await LocalApiRunner.start(
418496
resolvedProject: projectResolver.resolvedProject,
419497
path: entrypoint,
420-
configValues: configValues,
498+
configValues: {
499+
...configValues,
500+
for (final database in project.databases.values)
501+
if (database.config
502+
case ast.CelestDatabaseConfig(:final hostname))
503+
hostname.name: 'file::memory:',
504+
if (project.auth?.providers.isNotEmpty ?? false)
505+
'CELEST_AUTH_DATABASE_HOST': 'file::memory:',
506+
},
421507
environmentId: 'local',
422508
verbose: false,
423509
stdoutPipe: logSink,

apps/cli/fixtures/standalone/data/goldens/ast.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/cli/fixtures/standalone/data/goldens/ast.resolved.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/cli/lib/src/codegen/api/dockerfile_generator.dart

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,101 @@
11
import 'package:celest_ast/celest_ast.dart' as ast;
22
import 'package:celest_ast/celest_ast.dart';
33
import 'package:celest_cli/src/utils/error.dart';
4+
import 'package:mustache_template/mustache_template.dart';
45

56
/// Generates a `Dockerfile` for the user's project so they can self-host it.
67
final class DockerfileGenerator {
78
DockerfileGenerator({required this.project});
89

910
final ast.Project project;
1011

11-
static const String _dartTemplate = r'''
12-
FROM celestdev/dart-builder:{{version}} AS build
12+
static final Template _dartTemplate = Template(r'''
13+
# syntax=docker/dockerfile:1
14+
FROM dart:{{version}} AS build
1315
1416
WORKDIR /app
17+
{{ #includes_data }}
18+
# Add SQLite3
19+
RUN apt update && apt install -y libsqlite3-0
20+
RUN cp $(find / -name libsqlite3.so* -type f | head -n1) /app/libsqlite3.so
21+
{{ /includes_data }}
1522
COPY celest.aot.dill main.aot.dill
1623
1724
RUN [ "/usr/lib/dart/bin/utils/gen_snapshot", "--snapshot_kind=app-aot-elf", "--elf=/app/main.aot", "/app/main.aot.dill" ]
1825
19-
FROM celestdev/dart-runtime:{{version}}
26+
FROM scratch
27+
28+
COPY --from=build /runtime /
2029
2130
WORKDIR /app
31+
COPY --from=build /usr/lib/dart/bin/dartaotruntime .
32+
COPY --from=build /app ./
2233
COPY celest.json .
23-
COPY --from=build /app/main.aot .
2434
2535
ENV PORT=8080
2636
EXPOSE 8080
27-
''';
2837
29-
static const String _flutterTemplate = r'''
30-
FROM celestdev/flutter-builder:{{version}} AS build
38+
ENTRYPOINT [ "/app/dartaotruntime" ]
39+
CMD [ "/app/main.aot" ]
40+
''');
3141

32-
WORKDIR /app
33-
COPY celest.aot.dill main.aot.dill
42+
static final Template _flutterTemplate = Template(r'''
43+
# syntax=docker/dockerfile:1
44+
ARG DEBIAN_VERSION=12
3445
35-
RUN [ "/usr/lib/dart/bin/utils/gen_snapshot", "--snapshot_kind=app-aot-elf", "--elf=/app/main.aot", "/app/main.aot.dill" ]
46+
FROM debian:${DEBIAN_VERSION}-slim
47+
48+
ARG TARGETARCH
3649
37-
FROM celestdev/flutter-runtime:{{version}}
50+
# Set up fonts for the Flutter engine
51+
RUN apt update && apt install -y \
52+
fontconfig \
53+
fonts-cantarell \
54+
fonts-liberation2
55+
RUN fc-cache -f
56+
57+
# Add CA certificates
58+
RUN apt install -y ca-certificates
59+
60+
WORKDIR /celest
61+
{{ #includes_data }}
62+
# Add SQLite3
63+
RUN apt install -y libsqlite3-0
64+
RUN cp $(find / -name libsqlite3.so* -type f | head -n1) /celest/libsqlite3.so
65+
{{ /includes_data }}
66+
COPY --from=ghcr.io/cirruslabs/flutter:{{version}} /sdks/flutter/bin/cache/artifacts/engine/linux-${TARGETARCH/amd64/x64}/icudtl.dat .
67+
COPY --from=ghcr.io/cirruslabs/flutter:{{version}} /sdks/flutter/bin/cache/artifacts/engine/linux-${TARGETARCH/amd64/x64}/*.so ./
68+
COPY --from=ghcr.io/cirruslabs/flutter:{{version}} /sdks/flutter/bin/cache/artifacts/engine/linux-${TARGETARCH/amd64/x64}/*.so* ./
69+
COPY --from=ghcr.io/cirruslabs/flutter:{{version}} /sdks/flutter/bin/cache/artifacts/engine/linux-${TARGETARCH/amd64/x64}/flutter_tester flutter_runner
70+
71+
# Clean up
72+
RUN apt-get clean
3873
3974
WORKDIR /app
75+
COPY flutter_assets/ ./
4076
COPY celest.json .
41-
COPY --from=build /app/main.aot .
4277
78+
ENV LD_LIBRARY_PATH="/app:/celest:${LD_LIBRARY_PATH}"
4379
ENV PORT=8080
4480
EXPOSE 8080
45-
''';
81+
82+
ENTRYPOINT [ "/celest/flutter_runner", "--non-interactive", "--run-forever", "--disable-vm-service", "--icu-data-file-path=/celest/icudtl.dat", "--verbose-logging", "--enable-platform-isolates", "--force-multithreading", "--cache-dir-path=/tmp", "--flutter-assets-dir=/app", "--snapshot-asset-path=/app" ]
83+
CMD [ "/app/kernel_blob.bin" ]
84+
''');
4685

4786
String generate() {
87+
// TODO(dnys1): Add this database to the project AST so that it's included
88+
// in `project.databases`.
89+
final hasAuthDatabase = project.auth?.providers.isNotEmpty ?? false;
4890
return switch (project.sdkConfig.targetSdk) {
49-
SdkType.flutter => _flutterTemplate.replaceAll(
50-
'{{version}}',
51-
project.sdkConfig.flutter!.version.canonicalizedVersion,
52-
),
53-
SdkType.dart => _dartTemplate.replaceAll(
54-
'{{version}}',
55-
project.sdkConfig.dart.version.canonicalizedVersion,
56-
),
91+
SdkType.flutter => _flutterTemplate.renderString({
92+
'version': project.sdkConfig.flutter!.version.canonicalizedVersion,
93+
'includes_data': project.databases.isNotEmpty || hasAuthDatabase,
94+
}),
95+
SdkType.dart => _dartTemplate.renderString({
96+
'version': project.sdkConfig.dart.version.canonicalizedVersion,
97+
'includes_data': project.databases.isNotEmpty || hasAuthDatabase,
98+
}),
5799
final unknown => unreachable('Unknown SDK: $unknown'),
58100
};
59101
}

apps/cli/lib/src/frontend/celest_frontend.dart

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ import 'package:watcher/watcher.dart';
2929
enum RestartMode { hotReload, fullRestart }
3030

3131
final class CelestFrontend {
32-
factory CelestFrontend() => instance ??= CelestFrontend._();
33-
34-
CelestFrontend._() {
32+
CelestFrontend() {
3533
// Initialize immediately instead of lazily since _stopSub is never accessed
3634
// directly until `close`.
3735
_stopSub = StreamGroup.merge([
@@ -491,13 +489,6 @@ final class CelestFrontend {
491489
resolvedProject: resolvedProject,
492490
);
493491
final outputs = codeGenerator.generate();
494-
final outputsDir = Directory(projectPaths.outputsDir);
495-
if (outputsDir.existsSync() && !_didFirstCompile) {
496-
await outputsDir.delete(recursive: true);
497-
}
498-
if (stopped) {
499-
throw const CancellationException('Celest was stopped');
500-
}
501492
await (outputs.write(), celestProject.invalidate(outputs.keys)).wait;
502493
if (stopped) {
503494
throw const CancellationException('Celest was stopped');
@@ -530,24 +521,44 @@ final class CelestFrontend {
530521
required ResolvedProject resolvedProject,
531522
required String environmentId,
532523
}) async {
533-
final entrypointCompiler = EntrypointCompiler(
534-
logger: logger,
535-
verbose: verbose,
536-
enabledExperiments: celestProject.analysisOptions.enabledExperiments,
537-
);
538-
final kernel = await entrypointCompiler.compile(
539-
resolvedProject: resolvedProject,
540-
entrypointPath: projectPaths.localApiEntrypoint,
541-
);
542-
543524
final buildOutputs = fileSystem.directory(projectPaths.buildDir);
544525
if (!buildOutputs.existsSync()) {
545526
await buildOutputs.create(recursive: true);
546527
}
547-
548-
await buildOutputs
549-
.childFile('celest.aot.dill')
550-
.writeAsBytes(kernel.outputDill);
528+
switch (resolvedProject.sdkConfig.targetSdk) {
529+
case ast.SdkType.dart:
530+
final entrypointCompiler = EntrypointCompiler(
531+
logger: logger,
532+
verbose: verbose,
533+
enabledExperiments: celestProject.analysisOptions.enabledExperiments,
534+
);
535+
final kernel = await entrypointCompiler.compile(
536+
resolvedProject: resolvedProject,
537+
entrypointPath: projectPaths.localApiEntrypoint,
538+
);
539+
await buildOutputs
540+
.childFile('celest.aot.dill')
541+
.writeAsBytes(kernel.outputDill);
542+
case ast.SdkType.flutter:
543+
final bundleRes = await processManager.run(
544+
[
545+
'flutter',
546+
'build',
547+
'bundle',
548+
'--packages=${projectPaths.packagesConfig}',
549+
'--asset-dir=${p.join(buildOutputs.path, 'flutter_assets')}',
550+
'--target=${projectPaths.localApiEntrypoint}',
551+
'--target-platform=linux-x64',
552+
],
553+
workingDirectory: projectPaths.projectRoot,
554+
);
555+
if (bundleRes.exitCode != 0) {
556+
throw CliException(
557+
'Failed to build project:\n'
558+
'${bundleRes.stdout}\n${bundleRes.stderr}',
559+
);
560+
}
561+
}
551562

552563
final dockerfile = DockerfileGenerator(project: project);
553564
await buildOutputs

apps/cli/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ dependencies:
5151
logging: ^1.2.0
5252
mason_logger: ^0.3.0
5353
meta: ^1.10.0
54+
mustache_template: ^2.0.0
5455
native_assets_builder: ^0.8.1
5556
native_assets_cli: ^0.7.2
5657
native_storage: ^0.2.2
@@ -100,7 +101,6 @@ dev_dependencies:
100101
jose: ^0.3.4
101102
json_serializable: ^6.8.0
102103
mocktail: ^1.0.2
103-
mustache_template: ^2.0.0
104104
test: ^1.24.9
105105
test_descriptor: ^2.0.1
106106

0 commit comments

Comments
 (0)