Skip to content

Commit 3fbbf07

Browse files
authored
[native_assets_cli] Add user_defines (#2165)
Bug: #39 Note passing in the user-defines should be done by the Dart and Flutter SDK, they need to decide where to take the command-line arguments, and how to read from the `pubspec.yaml`. The `NativeAssetsBuildRunner` provides a suggestion about where to put user-defines in the `pubspec.yaml`: ```yaml hooks: user_defines: my_package: user_define_key: user_define_value user_define_key2: foo: bar some_other_package: user_define_key3: user_define_value3 ``` Moreover, it provides a helper function that can be reused in SDKs after the pubspec yaml is parsed. ### Design choices (According to the discussion on the bug.) * User-defines are name-spaced per package, we don't want new user-defines to invalidate the cache for other packages. * User-defines can be provided for any package in the dependency graph. * If user-defines need to be shared across packages, the common dependency package can export it as metadata. ### Test This change is tested by invoking the build with user-defines, and in the hook failing if the defines are not available.
1 parent 8a1f836 commit 3fbbf07

File tree

16 files changed

+229
-0
lines changed

16 files changed

+229
-0
lines changed

.github/workflows/native.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ jobs:
114114
- run: dart pub get -C test_data/reuse_dynamic_library/
115115
if: ${{ matrix.package == 'native_assets_builder' }}
116116

117+
- run: dart pub get -C test_data/user_defines/
118+
if: ${{ matrix.package == 'native_assets_builder' }}
119+
117120
- run: dart pub get -C test_data/no_hook/
118121
if: ${{ matrix.package == 'native_assets_builder' }}
119122

pkgs/code_assets/test/data/build_input_macos.json

+3
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,8 @@
7171
"out_file": "/Users/dacoharkes/src/dacoharkes/playground/my_package/example/.dart_tool/native_assets_builder/my_package/ca4e7d3d4e7b8912cbd24d9e8a6cecdc/output.json",
7272
"package_name": "my_package",
7373
"package_root": "/Users/dacoharkes/src/dacoharkes/playground/my_package/",
74+
"user_defines": {
75+
"some_key": "some_value"
76+
},
7477
"version": "1.9.0"
7578
}

pkgs/code_assets/test/data/link_input_macos.json

+3
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,8 @@
7171
"out_file": "/Users/dacoharkes/src/dacoharkes/playground/my_package/example/.dart_tool/native_assets_builder/my_package/ca4e7d3d4e7b8912cbd24d9e8a6cecdc/output.json",
7272
"package_name": "my_package",
7373
"package_root": "/Users/dacoharkes/src/dacoharkes/playground/my_package/",
74+
"user_defines": {
75+
"some_key": "some_value"
76+
},
7477
"version": "1.9.0"
7578
}

pkgs/data_assets/test/data/build_input.json

+3
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,8 @@
4646
"out_file": "/Users/dacoharkes/src/dacoharkes/playground/my_package/example/.dart_tool/native_assets_builder/my_package/ca4e7d3d4e7b8912cbd24d9e8a6cecdc/output.json",
4747
"package_name": "my_package",
4848
"package_root": "/Users/dacoharkes/src/dacoharkes/playground/my_package/",
49+
"user_defines": {
50+
"some_key": "some_value"
51+
},
4952
"version": "1.9.0"
5053
}

pkgs/data_assets/test/data/link_input.json

+3
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,8 @@
3232
"out_file": "/Users/dacoharkes/src/dacoharkes/playground/my_package/example/.dart_tool/native_assets_builder/my_package/ca4e7d3d4e7b8912cbd24d9e8a6cecdc/output.json",
3333
"package_name": "my_package",
3434
"package_root": "/Users/dacoharkes/src/dacoharkes/playground/my_package/",
35+
"user_defines": {
36+
"some_key": "some_value"
37+
},
3538
"version": "1.9.0"
3639
}

pkgs/hooks/doc/schema/shared/shared_definitions.schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@
174174
"package_root": {
175175
"$ref": "#/definitions/absolutePath"
176176
},
177+
"user_defines": {
178+
"type": "object",
179+
"additionalProperties": true
180+
},
177181
"version": {
178182
"type": "string"
179183
}

pkgs/hooks/test/data/build_input.json

+3
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,8 @@
3535
"out_file": "/Users/dacoharkes/src/dacoharkes/playground/my_package/example/.dart_tool/native_assets_builder/my_package/ca4e7d3d4e7b8912cbd24d9e8a6cecdc/output.json",
3636
"package_name": "my_package",
3737
"package_root": "/Users/dacoharkes/src/dacoharkes/playground/my_package/",
38+
"user_defines": {
39+
"some_key": "some_value"
40+
},
3841
"version": "1.9.0"
3942
}

pkgs/hooks/test/data/link_input.json

+3
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@
2424
"out_file": "/Users/dacoharkes/src/dacoharkes/playground/my_package/example/.dart_tool/native_assets_builder/my_package/ca4e7d3d4e7b8912cbd24d9e8a6cecdc/output.json",
2525
"package_name": "my_package",
2626
"package_root": "/Users/dacoharkes/src/dacoharkes/playground/my_package/",
27+
"user_defines": {
28+
"some_key": "some_value"
29+
},
2730
"version": "1.9.0"
2831
}

pkgs/hooks/test/schema/helpers.dart

+1
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ FieldsReturn _hookFields({
323323
([r'$schema'], expectOptionalFieldMissing),
324324
(['version'], versionMissingExpectation),
325325
if (inputOrOutput == InputOrOutput.input) ...[
326+
(['user_defines'], expectOptionalFieldMissing),
326327
(['out_dir_shared'], expectRequiredFieldMissing),
327328
(['out_dir'], expectRequiredFieldMissing),
328329
(['package_name'], expectRequiredFieldMissing),

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

+78
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class NativeAssetsBuildRunner {
4646
final Uri dartExecutable;
4747
final Duration singleHookTimeout;
4848
final Map<String, String> hookEnvironment;
49+
final Map<String, Map<String, Object?>?> userDefines;
4950
final PackageLayout packageLayout;
5051

5152
NativeAssetsBuildRunner({
@@ -55,6 +56,7 @@ class NativeAssetsBuildRunner {
5556
required this.packageLayout,
5657
Duration? singleHookTimeout,
5758
Map<String, String>? hookEnvironment,
59+
this.userDefines = const {},
5860
}) : _fileSystem = fileSystem,
5961
singleHookTimeout = singleHookTimeout ?? const Duration(minutes: 5),
6062
hookEnvironment =
@@ -131,6 +133,7 @@ class NativeAssetsBuildRunner {
131133
outputFile: buildDirUri.resolve('output.json'),
132134
outputDirectory: outDirUri,
133135
outputDirectoryShared: outDirSharedUri,
136+
userDefines: userDefines[package.name],
134137
);
135138

136139
final input = BuildInput(inputBuilder.json);
@@ -228,6 +231,7 @@ class NativeAssetsBuildRunner {
228231
outputFile: buildDirUri.resolve('output.json'),
229232
outputDirectory: outDirUri,
230233
outputDirectoryShared: outDirSharedUri,
234+
userDefines: userDefines[package.name],
231235
);
232236
inputBuilder.setupLink(
233237
assets: buildResult.encodedAssetsForLinking[package.name] ?? [],
@@ -861,6 +865,80 @@ ${compileResult.stdout}
861865
? BuildOutput(hookOutputJson)
862866
: LinkOutput(hookOutputJson);
863867
}
868+
869+
/// Returns a list of errors for [readHooksUserDefinesFromPubspec].
870+
static List<String> validateHooksUserDefinesFromPubspec(
871+
Map<Object?, Object?> pubspec,
872+
) {
873+
final hooks = pubspec['hooks'];
874+
if (hooks == null) return [];
875+
if (hooks is! Map) {
876+
return ["Expected 'hooks' to be a map. Found: '$hooks'"];
877+
}
878+
final userDefines = hooks['user_defines'];
879+
if (userDefines == null) return [];
880+
if (userDefines is! Map) {
881+
return [
882+
"Expected 'hooks.user_defines' to be a map. Found: '$userDefines'",
883+
];
884+
}
885+
886+
final errors = <String>[];
887+
for (final MapEntry(:key, :value) in userDefines.entries) {
888+
if (key is! String) {
889+
errors.add(
890+
"Expected 'hooks.user_defines' to be a map with string keys."
891+
" Found key: '$key'.",
892+
);
893+
}
894+
if (value is! Map) {
895+
errors.add(
896+
"Expected 'hooks.user_defines.$key' to be a map. Found: '$value'",
897+
);
898+
continue;
899+
}
900+
for (final childKey in value.keys) {
901+
if (childKey is! String) {
902+
errors.add(
903+
"Expected 'hooks.user_defines.$key' to be a "
904+
"map with string keys. Found key: '$childKey'.",
905+
);
906+
}
907+
}
908+
}
909+
return errors;
910+
}
911+
912+
/// Reads the user-defines from a pubspec.yaml in the suggested location.
913+
///
914+
/// SDKs do not have to follow this, they might support user-defines in a
915+
/// different way.
916+
///
917+
/// The [pubspec] is expected to be the decoded yaml, a Map.
918+
///
919+
/// Before invoking, check errors with [validateHooksUserDefinesFromPubspec].
920+
static Map<String, Map<String, Object?>> readHooksUserDefinesFromPubspec(
921+
Map<Object?, Object?> pubspec,
922+
) {
923+
assert(validateHooksUserDefinesFromPubspec(pubspec).isEmpty);
924+
final hooks = pubspec['hooks'];
925+
if (hooks is! Map) {
926+
return {};
927+
}
928+
final userDefines = hooks['user_defines'];
929+
if (userDefines is! Map) {
930+
return {};
931+
}
932+
return {
933+
for (final MapEntry(:key, :value) in userDefines.entries)
934+
if (key is String)
935+
key: {
936+
if (value is Map)
937+
for (final MapEntry(:key, :value) in value.entries)
938+
if (key is String) key: value,
939+
},
940+
};
941+
}
864942
}
865943

866944
/// Parses depfile contents.

pkgs/native_assets_builder/test/build_runner/helpers.dart

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Future<BuildResult?> build(
7575
bool linkingEnabled = false,
7676
required List<BuildAssetType> buildAssetTypes,
7777
Map<String, String>? hookEnvironment,
78+
Map<String, Map<String, Object?>?>? userDefines,
7879
}) async {
7980
final targetOS = target?.os ?? OS.current;
8081
final runPackageName_ =
@@ -91,6 +92,7 @@ Future<BuildResult?> build(
9192
fileSystem: const LocalFileSystem(),
9293
hookEnvironment: hookEnvironment,
9394
packageLayout: packageLayout,
95+
userDefines: userDefines ?? {},
9496
).build(
9597
extensions: [
9698
if (buildAssetTypes.contains(BuildAssetType.code))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) 2025, 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+
@OnPlatform({'mac-os': Timeout.factor(2), 'windows': Timeout.factor(10)})
6+
library;
7+
8+
import 'dart:io';
9+
10+
import 'package:native_assets_builder/native_assets_builder.dart';
11+
import 'package:test/test.dart';
12+
import 'package:yaml/yaml.dart';
13+
14+
import '../build_runner/helpers.dart';
15+
import '../helpers.dart';
16+
17+
void main() async {
18+
const name = 'user_defines';
19+
20+
test(
21+
'$name build',
22+
() => inTempDir((tempUri) async {
23+
await copyTestProjects(targetUri: tempUri);
24+
final packageUri = tempUri.resolve('$name/');
25+
26+
await runPubGet(workingDirectory: packageUri, logger: logger);
27+
28+
final pubspec =
29+
loadYamlDocument(
30+
File.fromUri(
31+
packageUri.resolve('pubspec.yaml'),
32+
).readAsStringSync(),
33+
).contents
34+
as YamlMap;
35+
expect(
36+
NativeAssetsBuildRunner.validateHooksUserDefinesFromPubspec(pubspec),
37+
isEmpty,
38+
);
39+
final userDefines =
40+
NativeAssetsBuildRunner.readHooksUserDefinesFromPubspec(pubspec);
41+
42+
final logMessages = <String>[];
43+
final result =
44+
(await build(
45+
packageUri,
46+
logger,
47+
dartExecutable,
48+
capturedLogs: logMessages,
49+
buildAssetTypes: [BuildAssetType.data],
50+
userDefines: userDefines,
51+
))!;
52+
53+
expect(result.encodedAssets.length, 1);
54+
}),
55+
);
56+
}

pkgs/native_assets_builder/test_data/manifest.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@
179179
- use_all_api/hook/build.dart
180180
- use_all_api/hook/link.dart
181181
- use_all_api/pubspec.yaml
182+
- user_defines/hook/build.dart
183+
- user_defines/pubspec.yaml
182184
- wrong_build_output/hook/build.dart
183185
- wrong_build_output/pubspec.yaml
184186
- wrong_build_output_2/hook/build.dart
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2025, 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:convert';
6+
import 'dart:io';
7+
8+
import 'package:native_assets_cli/data_assets.dart';
9+
10+
void main(List<String> arguments) async {
11+
await build(arguments, (input, output) async {
12+
final value1 = input.userDefines['user_define_key'];
13+
if (value1 != 'user_define_value') {
14+
throw Exception(
15+
'User-define user_define_key does not have the right value.',
16+
);
17+
}
18+
final value2 = input.userDefines['user_define_key2'];
19+
final dataAsset = DataAsset(
20+
file: input.outputDirectoryShared.resolve('my_asset.json'),
21+
name: 'my_asset',
22+
package: input.packageName,
23+
);
24+
File.fromUri(dataAsset.file).writeAsStringSync(jsonEncode(value2));
25+
output.assets.data.add(dataAsset);
26+
});
27+
}

pkgs/native_assets_cli/lib/src/config.dart

+14
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ sealed class HookInput {
8989
String toString() => const JsonEncoder.withIndent(' ').convert(json);
9090

9191
HookConfig get config => HookConfig._(this);
92+
93+
/// The user-defines for this [packageName].
94+
HookInputUserDefines get userDefines => HookInputUserDefines._(this);
95+
}
96+
97+
extension type HookInputUserDefines._(HookInput _input) {
98+
/// The value for the user-define for [key] for this package.
99+
///
100+
/// This can be arbitrary JSON/YAML if provided from the SDK from such source.
101+
/// If it's provided from command-line arguments, it's likely a string.
102+
Object? operator [](String key) => _input._syntax.userDefines?.json[key];
92103
}
93104

94105
sealed class HookInputBuilder {
@@ -110,13 +121,16 @@ sealed class HookInputBuilder {
110121
required Uri outputDirectory,
111122
required Uri outputDirectoryShared,
112123
required Uri outputFile,
124+
Map<String, Object?>? userDefines,
113125
}) {
114126
_syntax.version = latestVersion.toString();
115127
_syntax.packageRoot = packageRoot;
116128
_syntax.packageName = packageName;
117129
_syntax.outDir = outputDirectory;
118130
_syntax.outDirShared = outputDirectoryShared;
119131
_syntax.outFile = outputFile;
132+
_syntax.userDefines =
133+
userDefines == null ? null : syntax.JsonObject.fromJson(userDefines);
120134
}
121135

122136
/// Constructs a checksum for a [BuildInput].

0 commit comments

Comments
 (0)