Skip to content

Commit bbb5c35

Browse files
authored
fix(cli): Ensure pub cache is fixed during startup (#337)
Perform pub cache fixes during initialization if needed to ensure projects do not default to using Flutter SDK.
1 parent 9263eba commit bbb5c35

File tree

9 files changed

+164
-93
lines changed

9 files changed

+164
-93
lines changed

apps/cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- fix: Pass `flutter_assets` to backend when using `celest deploy`
44
- fix: Sentry integration
5+
- fix: Ensure pub cache is fixed during startup
56

67
## 1.0.10+2
78

apps/cli/lib/src/commands/auth/logout_command.dart

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,8 @@ final class LogoutCommand extends CelestCommand with Authenticate {
2525

2626
// Remove local cloud cache
2727
try {
28-
final config = await CelestConfig.load();
29-
final cloudDb = config.configDir.childFile('cloud.db');
30-
if (cloudDb.existsSync()) {
31-
await cloudDb.delete();
32-
}
28+
final config = CelestConfig();
29+
await config.secureSettings.clear();
3330
} on Object catch (e, st) {
3431
performance.captureError(e, stackTrace: st);
3532
}
Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'dart:convert';
2-
31
import 'package:celest_cli/src/config/find_application_home.dart';
42
import 'package:celest_cli/src/context.dart';
53
import 'package:file/file.dart';
@@ -9,42 +7,26 @@ import 'package:native_storage/native_storage.dart';
97
final class CelestConfig {
108
CelestConfig._(this.configDir);
119

12-
static final Logger logger = Logger('CelestConfig');
13-
14-
static Future<CelestConfig> load({Directory? configHome}) async {
10+
factory CelestConfig({Directory? configHome}) {
1511
configHome ??= fileSystem.directory(applicationConfigHome('Celest'));
16-
logger.finest('Loading configuration from $configHome');
17-
if (!await configHome.exists()) {
18-
await configHome.create(recursive: true);
19-
}
20-
21-
// Migrate the old config JSON to local storage if it exists.
22-
final configJson = configHome.childFile('config.json');
23-
if (configJson.existsSync()) {
24-
logger.finest('Migrating configuration to local storage');
25-
final config =
26-
jsonDecode(await configJson.readAsString()) as Map<String, Object?>;
27-
await Future.wait(
28-
config.entries.map(
29-
(entry) => _settings.write(entry.key, entry.value.toString()),
30-
),
31-
);
32-
await configJson.delete();
33-
logger.finest('Successfully migrated configuration to local storage');
34-
} else {
35-
logger.finest('Configuration already migrated to local storage');
12+
if (!configHome.existsSync()) {
13+
configHome.createSync(recursive: true);
3614
}
3715
return CelestConfig._(configHome);
3816
}
3917

40-
static CelestConfigValues get _settings =>
41-
CelestConfigValues(storage.isolated);
18+
static final Logger logger = Logger('CelestConfig');
19+
4220
final Directory configDir;
43-
CelestConfigValues get settings => _settings;
21+
22+
CelestSettings get settings => CelestSettings(storage);
23+
CelestSecureSettings get secureSettings =>
24+
CelestSecureSettings(storage.isolated);
4425

4526
Future<void> delete() async {
46-
await _settings.clear();
47-
if (await configDir.exists()) {
27+
settings.clear();
28+
await secureSettings.clear();
29+
if (configDir.existsSync()) {
4830
logger.fine('Removing Celest config dir: $configDir');
4931
await configDir.delete(recursive: true);
5032
}
@@ -54,7 +36,7 @@ final class CelestConfig {
5436
String toString() => 'CelestConfig: $configDir';
5537
}
5638

57-
extension type CelestConfigValues(IsolatedNativeStorage settings)
39+
extension type CelestSecureSettings(IsolatedNativeStorage settings)
5840
implements IsolatedNativeStorage {
5941
Future<String?> getOrganizationId() async {
6042
return settings.read('organization_id');
@@ -68,3 +50,17 @@ extension type CelestConfigValues(IsolatedNativeStorage settings)
6850
}
6951
}
7052
}
53+
54+
extension type CelestSettings(NativeStorage settings) implements NativeStorage {
55+
String? get pubCacheFixDigest {
56+
return settings.read('pub_cache_fix_digest');
57+
}
58+
59+
set pubCacheFixDigest(String? value) {
60+
if (value == null) {
61+
settings.delete('pub_cache_fix_digest');
62+
} else {
63+
settings.write('pub_cache_fix_digest', value);
64+
}
65+
}
66+
}

apps/cli/lib/src/init/project_init.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22
import 'dart:io';
33

4+
import 'package:async/async.dart';
45
import 'package:celest_cli/src/commands/celest_command.dart';
56
import 'package:celest_cli/src/commands/init_command.dart';
67
import 'package:celest_cli/src/commands/start_command.dart';
@@ -11,6 +12,7 @@ import 'package:celest_cli/src/init/project_migrate.dart';
1112
import 'package:celest_cli/src/init/templates/project_template.dart';
1213
import 'package:celest_cli/src/project/celest_project.dart';
1314
import 'package:celest_cli/src/pub/pub_action.dart';
15+
import 'package:celest_cli/src/pub/pub_cache.dart';
1416
import 'package:celest_cli/src/sdk/dart_sdk.dart';
1517
import 'package:celest_cli/src/utils/error.dart';
1618
import 'package:celest_cli/src/utils/recase.dart';
@@ -150,6 +152,8 @@ base mixin Configure on CelestCommand {
150152

151153
yield const Initializing();
152154
await init(projectRoot: projectRoot, parentProject: parentProject);
155+
await _fixPubCacheIfNeeded();
156+
logger.finest('Celest project initialized');
153157

154158
var needsAnalyzerMigration = false;
155159
Future<void>? upgradePackages;
@@ -316,6 +320,33 @@ base mixin Configure on CelestCommand {
316320
);
317321
}
318322

323+
/// Fixes the pub cache if needed.
324+
Future<void> _fixPubCacheIfNeeded() async {
325+
final pubCacheFixDigest = PubCache.packagesToFixDigest;
326+
final previousDigest = celestProject.config.settings.pubCacheFixDigest;
327+
if (pubCacheFixDigest != previousDigest) {
328+
logger.finest('Hydrating pub cache...');
329+
final result = await pubCache.hydrate();
330+
if (result case ErrorResult(:final error)) {
331+
logger.finest('Failed to hydrate pub cache', error);
332+
performance.captureError(error, stackTrace: StackTrace.current);
333+
return;
334+
}
335+
logger.finest('Fixing pub cache...');
336+
try {
337+
await pubCache.fix(throwOnError: true);
338+
} on Object catch (e, st) {
339+
logger.finest('Failed to fix pub cache', e, st);
340+
performance.captureError(e, stackTrace: st);
341+
return;
342+
}
343+
logger.finest('Pub cache fixed.');
344+
celestProject.config.settings.pubCacheFixDigest = pubCacheFixDigest;
345+
} else {
346+
logger.finest('Skipping pub cache fix, already up-to-date.');
347+
}
348+
}
349+
319350
// TODO(dnys1): Improve logic here so that we don't run pub upgrade if
320351
// the dependencies in the lockfile are already up to date.
321352
Future<void> _pubUpgrade() async {

apps/cli/lib/src/project/celest_project.dart

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,12 @@ final class CelestProject {
100100
clientDir: clientDir,
101101
outputsDir: outputsDir,
102102
);
103-
final [
104-
config as CelestConfig,
105-
analysisOptions as AnalysisOptions,
106-
] = await Future.wait([
107-
CelestConfig.load(configHome: configHome?.let(fileSystem.directory)),
108-
AnalysisOptions.load(projectPaths.analysisOptionsYaml),
109-
]);
103+
final config = CelestConfig(
104+
configHome: configHome?.let(fileSystem.directory),
105+
);
106+
final analysisOptions = await AnalysisOptions.load(
107+
projectPaths.analysisOptionsYaml,
108+
);
110109
_logger
111110
..finest('Loaded analysis options: $analysisOptions')
112111
..finest('Loaded Celest config: $config');

apps/cli/lib/src/pub/pub_cache.dart

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import 'dart:convert';
12
import 'dart:io';
23

4+
import 'package:async/async.dart';
35
import 'package:celest_cli/src/context.dart';
46
import 'package:celest_cli/src/pub/project_dependency.dart';
57
import 'package:celest_cli/src/sdk/dart_sdk.dart';
68
import 'package:celest_cli/src/utils/process.dart';
79
import 'package:collection/collection.dart';
10+
import 'package:crypto/crypto.dart';
811
import 'package:file/file.dart';
912
import 'package:logging/logging.dart';
1013
import 'package:meta/meta.dart';
@@ -22,12 +25,19 @@ final class PubCache {
2225
// This is the only syntax that reliably works with both dart/flutter
2326
'native_storage': '>=0.2.2 <1.0.0',
2427
'native_authentication': '>=0.1.0 <1.0.0',
25-
'jni': '>=0.11.0 <1.0.0',
28+
'jni': '>=0.14.0 <0.15.0',
2629
'celest_auth': '>=$currentMinorVersion <2.0.0',
2730
'celest': '>=$currentMinorVersion <2.0.0',
2831
'celest_core': '>=$currentMinorVersion <2.0.0',
29-
'objective_c': '>=2.0.0 <8.0.0',
32+
'objective_c': '>=7.0.0 <8.0.0',
3033
};
34+
35+
/// MD5 hash of the [packagesToFix] map.
36+
static final String packagesToFixDigest = () {
37+
final pubCacheFixJson = JsonUtf8Encoder().convert(PubCache.packagesToFix);
38+
return md5.convert(pubCacheFixJson).toString();
39+
}();
40+
3141
static final _logger = Logger('PubCache');
3242

3343
String? _cachePath;
@@ -96,44 +106,66 @@ final class PubCache {
96106
/// Runs `pub cache add` for each package in [packagesToFix].
97107
///
98108
/// Returns the exit codes and output for each package.
99-
Future<List<(int, String)>> hydrate() async {
100-
final results = <(int, String)>[];
101-
for (final package in packagesToFix.entries) {
102-
// Run serially to avoid flutter lock
103-
final result = await processManager.start(runInShell: true, [
104-
Sdk.current.sdkType.name,
109+
Future<Result<void>> hydrate() async {
110+
Future<void> hydratePackage(String name, String constraint) async {
111+
final command = [
112+
Sdk.current.dart,
105113
'pub',
106114
'cache',
107115
'add',
108-
package.key,
116+
name,
109117
'--version',
110-
package.value,
118+
constraint,
111119
'--all',
112-
]).then((process) async {
113-
final combinedOutput = StringBuffer();
114-
process.captureStdout(
115-
sink: (line) {
116-
_logger.finest(line);
117-
combinedOutput.writeln(line);
118-
},
119-
);
120-
process.captureStderr(
121-
sink: (line) {
122-
_logger.finest(line);
123-
combinedOutput.writeln(line);
124-
},
120+
];
121+
final process = await processManager.start(command, runInShell: true);
122+
final combinedOutput = StringBuffer();
123+
process.captureStdout(
124+
sink: (line) {
125+
_logger.finest(line);
126+
combinedOutput.writeln(line);
127+
},
128+
);
129+
process.captureStderr(
130+
sink: (line) {
131+
_logger.finest(line);
132+
combinedOutput.writeln(line);
133+
},
134+
);
135+
if (await process.exitCode case final exitCode && != 0) {
136+
throw ProcessException(
137+
command.first,
138+
command.sublist(1),
139+
combinedOutput.toString(),
140+
exitCode,
125141
);
126-
return (await process.exitCode, combinedOutput.toString());
127-
});
128-
results.add(result);
142+
}
143+
}
144+
145+
final hydrations = <Future<void>>[];
146+
for (final package in packagesToFix.entries) {
147+
hydrations.add(hydratePackage(package.key, package.value));
148+
}
149+
final results = await Result.captureAll(hydrations);
150+
151+
var failed = false;
152+
final errors = <Object>[];
153+
for (final result in results) {
154+
if (result.isError) {
155+
failed = true;
156+
errors.add(result.asError!.error);
157+
}
158+
}
159+
if (failed) {
160+
return Result.error(ParallelWaitError(<Object?>[], errors));
129161
}
130-
return results;
162+
return Result.value(null);
131163
}
132164

133165
/// Fixes the pubspec for each package in [packagesToFix].
134166
///
135167
/// Returns the number of packages fixed.
136-
Future<int> fix({@visibleForTesting bool throwOnError = false}) async {
168+
Future<int> fix({bool throwOnError = false}) async {
137169
final cachePath = _cachePath ??= findCachePath();
138170
if (cachePath == null) {
139171
if (throwOnError) {

apps/cli/test/commands/uninstall_test.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ void main() {
3131
ctx.fileSystem = MemoryFileSystem.test();
3232
await init(projectRoot: ctx.fileSystem.systemTempDirectory.path);
3333

34-
await celestProject.config.settings.setOrganizationId('org-id');
34+
await celestProject.config.secureSettings.setOrganizationId('org-id');
3535

36-
expect(await celestProject.config.settings.getOrganizationId(), 'org-id');
36+
expect(await celestProject.config.secureSettings.getOrganizationId(),
37+
'org-id');
3738

3839
await const CelestUninstaller().uninstall();
3940

40-
expect(await celestProject.config.settings.getOrganizationId(), isNull);
41+
expect(await celestProject.config.secureSettings.getOrganizationId(),
42+
isNull);
4143
});
4244

4345
group('uninstall AOT', () {

apps/cli/test/config/celest_config_test.dart

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,41 @@ void main() {
99
group('CelestConfig', () {
1010
setUp(initTests);
1111

12-
test('migrates to local storage', () async {
12+
test('delete clears all storage interfaces', () async {
1313
ctx.fileSystem = MemoryFileSystem.test();
14-
final configHome = ctx.fileSystem.systemTempDirectory.childDirectory(
15-
'config',
16-
)..createSync(recursive: true);
17-
18-
final configJson = configHome.childFile('config.json');
19-
configJson.writeAsStringSync('{"organization_id": "org-id"}');
2014
await init(
2115
projectRoot: ctx.fileSystem.systemTempDirectory.path,
22-
configHome: configHome.path,
2316
);
2417

25-
expect(await celestProject.config.settings.getOrganizationId(), 'org-id');
26-
expect(await configJson.exists(), isFalse);
18+
expect(
19+
await celestProject.config.secureSettings.getOrganizationId(),
20+
isNull,
21+
);
22+
await celestProject.config.secureSettings.setOrganizationId('org-id');
23+
expect(
24+
await celestProject.config.secureSettings.getOrganizationId(),
25+
'org-id',
26+
);
27+
28+
expect(
29+
celestProject.config.settings.pubCacheFixDigest,
30+
isNull,
31+
);
32+
celestProject.config.settings.pubCacheFixDigest = 'digest';
33+
expect(
34+
celestProject.config.settings.pubCacheFixDigest,
35+
'digest',
36+
);
37+
38+
await celestProject.config.delete();
39+
expect(
40+
await celestProject.config.secureSettings.getOrganizationId(),
41+
isNull,
42+
);
43+
expect(
44+
celestProject.config.settings.pubCacheFixDigest,
45+
isNull,
46+
);
2747
});
2848
});
2949
}

0 commit comments

Comments
 (0)