Skip to content

Commit 989ef50

Browse files
authored
[native_assets_cli] Add outputDirectoryShared to config (#1557)
1 parent 4af11a0 commit 989ef50

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+785
-46
lines changed

.github/workflows/native.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ jobs:
9696
- run: dart pub get -C test_data/add_asset_link/
9797
if: ${{ matrix.package == 'native_assets_builder' }}
9898

99+
- run: dart pub get -C test_data/transformer/
100+
if: ${{ matrix.package == 'native_assets_builder' }}
101+
99102
- run: dart pub get -C test_data/treeshaking_native_libs/
100103
if: ${{ matrix.package == 'native_assets_builder' }}
101104

pkgs/native_assets_builder/CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 0.8.4-wip
22

3-
- Nothing yet.
3+
- Also lock `BuildConfig` and `LinkConfig` `outputDirectoryShared` when invoking
4+
hooks to prevent concurrency issues with shared output caching.
45

56
## 0.8.3
67

pkgs/native_assets_builder/lib/src/build_runner/build_runner.dart

+38-9
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import 'dart:async';
66
import 'dart:io';
77

88
import 'package:logging/logging.dart';
9-
import 'package:native_assets_cli/locking.dart';
109
import 'package:native_assets_cli/native_assets_cli.dart' as api;
1110
import 'package:native_assets_cli/native_assets_cli_internal.dart';
1211
import 'package:package_config/package_config.dart';
1312

13+
import '../locking/locking.dart';
1414
import '../model/build_dry_run_result.dart';
1515
import '../model/build_result.dart';
1616
import '../model/hook_result.dart';
@@ -263,13 +263,21 @@ class NativeAssetsBuildRunner {
263263
);
264264
final buildDirUri =
265265
packageLayout.dartToolNativeAssetsBuilder.resolve('$buildDirName/');
266-
final outDirUri = buildDirUri.resolve('out/');
267-
final outDir = Directory.fromUri(outDirUri);
266+
final outputDirectory = buildDirUri.resolve('out/');
267+
final outDir = Directory.fromUri(outputDirectory);
268268
if (!await outDir.exists()) {
269269
// TODO(https://dartbug.com/50565): Purge old or unused folders.
270270
await outDir.create(recursive: true);
271271
}
272272

273+
final outputDirectoryShared = packageLayout.dartToolNativeAssetsBuilder
274+
.resolve('shared/${package.name}/$hook/');
275+
final outDirShared = Directory.fromUri(outputDirectoryShared);
276+
if (!await outDirShared.exists()) {
277+
// TODO(https://dartbug.com/50565): Purge old or unused folders.
278+
await outDirShared.create(recursive: true);
279+
}
280+
273281
if (hook == Hook.link) {
274282
File? resourcesFile;
275283
if (resourceIdentifiers != null) {
@@ -279,7 +287,8 @@ class NativeAssetsBuildRunner {
279287
}
280288

281289
return LinkConfigImpl(
282-
outputDirectory: outDirUri,
290+
outputDirectory: outputDirectory,
291+
outputDirectoryShared: outputDirectoryShared,
283292
packageName: package.name,
284293
packageRoot: package.root,
285294
targetOS: target.os,
@@ -297,7 +306,8 @@ class NativeAssetsBuildRunner {
297306
);
298307
} else {
299308
return BuildConfigImpl(
300-
outputDirectory: outDirUri,
309+
outputDirectory: outputDirectory,
310+
outputDirectoryShared: outputDirectoryShared,
301311
packageName: package.name,
302312
packageRoot: package.root,
303313
targetOS: target.os,
@@ -428,8 +438,11 @@ class NativeAssetsBuildRunner {
428438
continue;
429439
}
430440
// TODO(https://github.com/dart-lang/native/issues/1321): Should dry runs be cached?
431-
var (buildOutput, packageSuccess) = await runUnderDirectoryLock(
432-
Directory.fromUri(config.outputDirectory.parent),
441+
var (buildOutput, packageSuccess) = await runUnderDirectoriesLock(
442+
[
443+
Directory.fromUri(config.outputDirectoryShared.parent),
444+
Directory.fromUri(config.outputDirectory.parent),
445+
],
433446
timeout: singleHookTimeout,
434447
logger: logger,
435448
() => _runHookForPackage(
@@ -482,8 +495,11 @@ class NativeAssetsBuildRunner {
482495
PackageLayout packageLayout,
483496
) async {
484497
final outDir = config.outputDirectory;
485-
return await runUnderDirectoryLock(
486-
Directory.fromUri(config.outputDirectory.parent),
498+
return await runUnderDirectoriesLock(
499+
[
500+
Directory.fromUri(config.outputDirectoryShared.parent),
501+
Directory.fromUri(config.outputDirectory.parent),
502+
],
487503
timeout: singleHookTimeout,
488504
logger: logger,
489505
() async {
@@ -784,15 +800,27 @@ ${compileResult.stdout}
784800
hook: hook,
785801
linkingEnabled: linkingEnabled,
786802
);
803+
787804
final outDirUri = buildParentDir.resolve('$buildDirName/out/');
788805
final outDir = Directory.fromUri(outDirUri);
789806
if (!await outDir.exists()) {
790807
await outDir.create(recursive: true);
791808
}
809+
810+
// Shared between dry run and wet run.
811+
final outputDirectoryShared =
812+
buildParentDir.resolve('shared/${package.name}/$hook/out/');
813+
final outDirShared = Directory.fromUri(outputDirectoryShared);
814+
if (!await outDirShared.exists()) {
815+
// TODO(https://dartbug.com/50565): Purge old or unused folders.
816+
await outDirShared.create(recursive: true);
817+
}
818+
792819
switch (hook) {
793820
case Hook.build:
794821
return BuildConfigImpl.dryRun(
795822
outputDirectory: outDirUri,
823+
outputDirectoryShared: outputDirectoryShared,
796824
packageName: packageName,
797825
packageRoot: packageRoot,
798826
targetOS: targetOS,
@@ -803,6 +831,7 @@ ${compileResult.stdout}
803831
case Hook.link:
804832
return LinkConfigImpl.dryRun(
805833
outputDirectory: outDirUri,
834+
outputDirectoryShared: outputDirectoryShared,
806835
packageName: packageName,
807836
packageRoot: packageRoot,
808837
targetOS: targetOS,

pkgs/native_assets_cli/lib/src/locking/locking.dart renamed to pkgs/native_assets_builder/lib/src/locking/locking.dart

+22
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ import 'dart:io';
77

88
import 'package:logging/logging.dart';
99

10+
Future<T> runUnderDirectoriesLock<T>(
11+
List<Directory> directories,
12+
Future<T> Function() callback, {
13+
Duration? timeout,
14+
Logger? logger,
15+
}) async {
16+
if (directories.isEmpty) {
17+
return await callback();
18+
}
19+
return await runUnderDirectoryLock(
20+
directories.first,
21+
() => runUnderDirectoriesLock<T>(
22+
directories.skip(1).toList(),
23+
callback,
24+
timeout: timeout,
25+
logger: logger,
26+
),
27+
timeout: timeout,
28+
logger: logger,
29+
);
30+
}
31+
1032
/// Run [callback] with this Dart process having exclusive access to
1133
/// [directory].
1234
///
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:native_assets_builder/src/utils/run_process.dart'
8+
show RunProcessResult;
9+
import 'package:native_assets_cli/native_assets_cli_internal.dart';
10+
import 'package:test/test.dart';
11+
12+
import '../helpers.dart';
13+
import 'helpers.dart';
14+
15+
const Timeout longTimeout = Timeout(Duration(minutes: 5));
16+
17+
void main() async {
18+
const packageName = 'transformer';
19+
20+
test('Concurrent invocations', timeout: longTimeout, () async {
21+
await inTempDir((tempUri) async {
22+
final packageUri = tempUri.resolve('$packageName/');
23+
await copyTestProjects(
24+
sourceUri: testDataUri.resolve('$packageName/'),
25+
targetUri: packageUri,
26+
);
27+
28+
await runPubGet(
29+
workingDirectory: packageUri,
30+
logger: logger,
31+
);
32+
33+
Future<RunProcessResult> runBuildInProcess(
34+
Target target,
35+
) async {
36+
final result = await runProcess(
37+
executable: dartExecutable,
38+
arguments: [
39+
pkgNativeAssetsBuilderUri
40+
.resolve(
41+
'test/build_runner/concurrency_shared_test_helper.dart',
42+
)
43+
.toFilePath(),
44+
packageUri.toFilePath(),
45+
target.toString(),
46+
],
47+
workingDirectory: packageUri,
48+
logger: logger,
49+
);
50+
expect(result.exitCode, 0);
51+
return result;
52+
}
53+
54+
// Simulate running `dart run` concurrently in 3 different terminals.
55+
// Twice for the same target and also for a different target.
56+
final results = await Future.wait([
57+
runBuildInProcess(Target.androidArm64),
58+
runBuildInProcess(Target.current),
59+
runBuildInProcess(Target.current),
60+
]);
61+
final stdouts = results.map((e) => e.stdout).join('\n');
62+
// One run for the two targets wins in running first.
63+
expect(stdouts, stringContainsInOrder(['Reused 0 cached files.']));
64+
// The other run reuses the cached results.
65+
expect(stdouts, stringContainsInOrder(['Reused 10 cached files.']));
66+
// One of the two runs for the same target will not be invoked at all.
67+
expect(
68+
stdouts,
69+
stringContainsInOrder([
70+
'Waiting to be able to obtain lock of directory',
71+
'Skipping build for',
72+
]),
73+
);
74+
});
75+
});
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:logging/logging.dart';
6+
import 'package:native_assets_builder/native_assets_builder.dart';
7+
import 'package:native_assets_cli/native_assets_cli.dart';
8+
import 'package:native_assets_cli/native_assets_cli_internal.dart';
9+
10+
import '../helpers.dart';
11+
12+
// Is invoked concurrently multiple times in separate processes.
13+
void main(List<String> args) async {
14+
final packageUri = Uri.directory(args[0]);
15+
final target = Target.fromString(args[1]);
16+
17+
final logger = Logger('')
18+
..level = Level.ALL
19+
..onRecord.listen((event) => print(event.message));
20+
21+
final result = await NativeAssetsBuildRunner(
22+
logger: logger,
23+
dartExecutable: dartExecutable,
24+
).build(
25+
buildMode: BuildModeImpl.release,
26+
linkModePreference: LinkModePreferenceImpl.dynamic,
27+
target: target,
28+
workingDirectory: packageUri,
29+
includeParentEnvironment: true,
30+
linkingEnabled: false,
31+
supportedAssetTypes: [DataAsset.type],
32+
targetAndroidNdkApi: target.os == OS.android ? 30 : null,
33+
);
34+
if (!result.success) {
35+
throw Error();
36+
}
37+
print('done');
38+
}

pkgs/native_assets_cli/test/locking/locking_test.dart renamed to pkgs/native_assets_builder/test/locking/locking_test.dart

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import '../helpers.dart';
1414
const Timeout longTimeout = Timeout(Duration(minutes: 5));
1515

1616
void main() async {
17+
final packageUri = findPackageRoot('native_assets_builder');
18+
1719
test('Concurrent invocations', timeout: longTimeout, () async {
1820
await inTempDir((tempUri) async {
1921
Future<ProcessResult> runInProcess() async {

pkgs/native_assets_cli/test/locking/locking_test_helper.dart renamed to pkgs/native_assets_builder/test/locking/locking_test_helper.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import 'dart:io';
66

7-
import 'package:native_assets_cli/locking.dart';
7+
import 'package:native_assets_builder/src/locking/locking.dart';
88

99
void main(List<String> args) async {
1010
final directory = Directory.fromUri(Uri.directory(args[0]));

pkgs/native_assets_builder/test/test_data/native_dynamic_linking_test.dart

+12-4
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@ void main() async {
2424
test(
2525
'native_dynamic_linking build$testSuffix',
2626
() => inTempDir((tempUri) async {
27+
final outputDirectory = tempUri.resolve('out/');
28+
await Directory.fromUri(outputDirectory).create();
29+
final outputDirectoryShared = tempUri.resolve('out_shared/');
30+
await Directory.fromUri(outputDirectoryShared).create();
2731
final testTempUri = tempUri.resolve('test1/');
2832
await Directory.fromUri(testTempUri).create();
2933
final testPackageUri = testDataUri.resolve('$name/');
3034
final dartUri = Uri.file(Platform.resolvedExecutable);
3135

3236
final config = BuildConfigImpl(
33-
outputDirectory: tempUri,
37+
outputDirectory: outputDirectory,
38+
outputDirectoryShared: outputDirectoryShared,
3439
packageName: name,
3540
packageRoot: testPackageUri,
3641
targetOS: OSImpl.current,
@@ -62,7 +67,7 @@ void main() async {
6267
}
6368
expect(processResult.exitCode, 0);
6469

65-
final buildOutputUri = tempUri.resolve('build_output.json');
70+
final buildOutputUri = outputDirectory.resolve('build_output.json');
6671
final buildOutput = HookOutputImpl.fromJsonString(
6772
await File.fromUri(buildOutputUri).readAsString());
6873
final assets = buildOutput.assets;
@@ -100,9 +105,12 @@ void main() async {
100105
environment: {
101106
// Add the directory containing the linked dynamic libraries to
102107
// the PATH so that the dynamic linker can find them.
108+
// TODO(https://github.com/dart-lang/sdk/issues/56551): We could
109+
// skip this if Dart would implicitly add dylib containing
110+
// directories to the PATH.
103111
if (Platform.isWindows)
104-
'PATH':
105-
'${tempUri.toFilePath()};${Platform.environment['PATH']}',
112+
'PATH': '${outputDirectory.toFilePath()};'
113+
'${Platform.environment['PATH']}',
106114
},
107115
throwOnUnexpectedExitCode: true,
108116
logger: logger,

0 commit comments

Comments
 (0)