Open dcharkes opened 1 year ago
My original thought was that the configuration system is based on hierarchical json
and modification/patching of it:
The final build configuration for a particular package's build.dart
invocation is the default json configuration from the build/bundle tool, modified with the user-defined global values, modified with user-defined per-package values.
Those user-defined global or per-package settings can come from command line arguments to the build (e.g. --config build.cxx.optlevel=3
for global setting, --config foo:build.cxx.optlevel=3
for package level or put those into a file via --config native-build-config.json
).
The actual build/bundling tool (e.g. dart build
) would only know about very few config settings (e.g. target abi, ...) and blindly pass-through all other settings to the individual package's build.dart
invocation.
The Dart code that is operating on such json can use wrappers on the json that view into things they care about (e.g. via inline classes):
class BuildConfig {
final Map<String, Object> config;
}
extension on BuildConfig {
CxxConfig get cxxConfig => CxxConfig(config['build.cxx']);
}
inline class CxxConfig {
final Map<String, Object> config;
int get optimizationLevel => ...;
}
Then we have flexible configuration mechanism:
build.dart
invocations (it will only add specific things, e.g. target abi, linking preference)build.dart
implementations can use helper packages that can have structured view on json-like config. (And one can pass sub-parts of the hierarchy down to those helper packages (e.g. BuildConfig.cxxConfig
) - so package:c_compiler
doesn't get a config containing rust related things)Having configuration that can be used by multiple targets such as optimization level is a good idea indeed. Then if some package wants some specific configuration, they can just use their own package-name as the key. (That at least rules out using dependencies
in pubspec yaml.)
My original thought was that the configuration system is based on hierarchical
json
and modification/patching of it:
- build configuration has abi, linking-preference, ...
- users can modify this configuration by e.g. specifying values on the command line or from config files
- users can modify this configuration per-package
I was thinking that instead of modification it would be in non-conflicting. The final config:
out_dir: ...
target: ...
dependency_metadata:
my_package: // some meta data set by the build.dart of my_package, used in later native asset builds
key: value
c_compiler:
ar: ...
cc: ...
ld: ...
user_defines:
cxx: // generic which can be used by any package
optlevel: 3
my_package: // likely only used during the build.dart of my_package.
key: value
The user_defines
is populated from the dart run
&flutter run
defines, --build_config=..., pubspec.yaml -> build_config.
I'm not sure yet about the env vars from dart run
&flutter run
, see https://github.com/dart-lang/native/issues/32
But maybe nesting them in user_defines
is the wrong thing to do and we should put this type of config in the top-level, so that it can be used to override whatever dart run
and flutter run
is trying to pass to the various build.dart
builds. (One would be able to override out_dir
? c_compiler
? do we want that? What about overriding dependency_metadata
? Overriding target
will most likely break the build.)
If we nest in user_defines
(or user_metadata
, or ...). Then it's up to the packages to decide whether they consider something as overridable.
For what it's worth, I think it's fine to add a top-level key in pubspec.yaml
like:
build_config:
<package>:
<config-key>: <config-value>
my_package_with_native_assets:
my_config_option: true
optlevel: 3
...
It's sometimes nice not to have a lot of top-level config files.
Nesting it under dependencies
is very bad idea.
Do consider that a package published on pub.dev might have a build_config
section, but author of said package should be a aware that build_config
will be ignored when someone depends on their package. If foo
contains build_config
and bar
depends on foo
, the build_config
in foo
will be ignored when building bar
(otherwise, you'll have a hard time merging it).
My current thinking based on the above.
End users can specify config in the pubspec
native_assets:
defines:
<package>:
<config-key>: <config-value>
my_package_with_native_assets: # Only passed to my_package_with_native_assets's build.dart
enable_some_feature: true
_all: # Passed to every build.dart (`package:_all` should never exist.)
optlevel: 3
If we would like to support branching on build_mode / architecture / other already predefined keys, we could consider adding something like filters:
native_assets:
defines:
my_package_with_native_assets: # Only passed to my_package_with_native_assets's build.dart
_where:
_build_mode: release
optimize: true
However, the downside is that this kind of logic doesn't scale. What if we add more predefined things to the config passed in to build.dart
, we'd need to add it.
We kind of want to opposite of the metadata here, metadata is output by build.dart
and flows from dependencies to dependents. "Inverse metadata" would be a programmatic way to have the root package build.dart
give user-defines to all dependency build.dart invocations. However, sticking it into build.dart
is a bad idea, because then it looks like that the package itself would want to bundle native assets. So maybe it would be better to introduce something like a user_defines.dart
.
I think for now we should skip conditional user-defines in the pubspec, and do these via command-line args.
@GregoryConrad What is your use case for conditional user-defines?
The config in the pubspec.yaml can be overridden in invocations of dart
/ flutter
similar to how user-defines are specified for dart code.
The defines for dart code are with -D
$ dart --help -v
[...]
--define=<key>=<value> or -D<key>=<value>
Define an environment declaration. To specify multiple declarations,
use multiple instances of this option.
(And for flutter --dart-define
.)
Since these arguments are already taken, we could use --native-assets-define=<key>=<value>
.
We could follow Flutters --dart-defines-from-file
and support --native-assets-defines-from-file
as well.
We should stop passing through environment variables to prevent builds accidentally depending on them and breaking build caching that way.
The BuildConfig
gets a user-defines key:
out_dir: ...
target: ...
dependency_metadata:
my_package: // some meta data set by the build.dart of my_package, used in later native asset builds
key: value
c_compiler:
ar: ...
cc: ...
ld: ...
user_defines:
optlevel: 3
enable_some_feature: true
These are defines just for the build for the native assets of this package.
Needs an extra argument for build
and dryRun
, probably something like:
class UserDefines {
final Map<String, Object> all;
final Map<String, Map<String, Object>> perPackage;
}
And it gets the right information per package to put in the BuildConfig
for that package.
(Note that if we ever add conditional defines in pubspec or a script, they should not be able to depend on the fields not passed to dry-run such as architecture and API levels.)
dartdev
(the implementation of dart run
and dart build
) and flutter_tools
(the implementation of flutter run
and flutter build
) take the info from the pubspec and --native-assets-define=
What naming should we use. Should we settle on native-assets
or on build
?
We settled on build.dart
as the name for the top-level file. The idea was that we could add other "builds" besides native-assets later.
The question is whether these other types of "builds" would benefit from defines as well. If they do, we could settle on the "build" name here:
build_defines:
<package>:
<config-key>: <config-value>
dart run --build-define=<key>=<value>
flutter run --build-define=<key>=<value>
cc @mkustermann @jonasfj @bkonyi @stuartmorgan @mit-mit
cc @simolus3
The yaml-based end-user specification covers my use-cases, but given that there are more complex ideas mentioned here (like conditional user defines), I want to bring up the idea of allowing users to configure their dependencies in Dart? E.g. if I was able to put say a build_config.dart
in my application with content like:
void main(List<String> args) async {
final global = await BuildConfig.fromArgs(args);
global.configure(allPackages, (config) {
config.addToList('c_compiler.flags', '-Wall');
// There could also be type-safe extensions on config to describe common options
config.linker.preferredMode = Linker.staticLibrary;
});
// Or configure a specific package
global.configure(package('sqlite3'), (config) async {
config['source'] = 'url';
config['url'] = 'https://sqlite.org/2023/sqlite-amalgamation-3430100.zip';
// Since this is code, I could make some options conditional
if (global.buildMode == BuildMode.release) {
// Read additional options we want from a file
final optionsFile = global.trackInput('assets/release_options.txt');
final content = await optionsFile.readAsString();
config.cCompiler.addDefine('foo': content);
}
});
}
With a nice DSL and typed extensions to configure common things like the C toolchain, this may be as convenient as a declarative configuration in yaml. A benefit is that it can scale to complex configuration options much better.
@GregoryConrad What is your use case for conditional user-defines?
I can't remember what my exact use case was, but a big one is enabling Rust cargo features based on a user's preferences/needs. In C/C++ land, another might be to supply a value to clang -D MY_DEFINE=xyz
RE: build_config.dart
. The user-defines are a special case of a more general problem, namely inverse meta-data: data flowing from dependencies to source.
One other use case would be package:messages
, where build.dart
does resource shaking, but the usages of that package declare message-files that need to be resource shaken. A package dependency graph could look something like:
The three message files could be defined in the build_config.dart
of the three packages.
User defines are basically the build_config.dart
for the root package. So user-defines are a special case.
(Having two files build.dart
and build_config.dart
is not that pretty, so we should consider having one file instead. But then it would be better to have a single file that implements an interface rather than having a main function. https://github.com/dart-lang/native/issues/152)
Related:
cc @mosuem
E.g. if I was able to put say a
build_config.dart
in my application with content like...
Then any change to build rules easily becomes a breaking change for the entire ecosystem.
I'm generally very concerned if using native-assets requires using an API that isn't specified in a dart:
library. Because changing such an API easily becomes a breaking change for the entire ecosystem -- while side-stepping established process for breaking changes in the Dart SDK.
Of course build_config.dart
could write out a JSON file with the configuration and one or more packages could provide a high-level API for configuring stuff.
The user-defines are a special case of a more general problem, namely inverse meta-data: data flowing from dependencies to source.
@dcharkes, I like your example, but when you say: "user-defines" do you mean:
Is it only the root package that is allowed to have "user-defines", and are they "user-defines" specified in dependencies and transitive dependencies ignored?
(like how dev_dependencies
from dependencies and transitive dependencies are ignored).
@sigurdm this is a bit similar to what is proposed in https://github.com/dart-lang/pub/issues/3917
Should we seriously consider making a generic configuration mechanism?
@dcharkes, I would suggest avoiding magic values like _all
. I'm sure we'll enable people to make the _all
package, and things will be bad :see_no_evil: :see_no_evil: :see_no_evil:
_all: # Passed to every build.dart (`package:_all` should never exist.)
optlevel: 3
If you must just do:
native_assets:
defines:
<package>:
<config-key>: <config-value>
my_package_with_native_assets: # Only passed to my_package_with_native_assets's build.dart
enable_some_feature: true
define_for_all: # Passed to every build.dart
optlevel: 3
But it might also be reasonable to ship the initial MVP without a _all
or define_for_all
keys, people will be fine copy/pasting. You also want features you can add later :rofl:
In fact, it might be worth down scoping and not shipping with "user-defines" initially, is there any reason it can't be added at a later stage?
@dcharkes, I like your example, but when you say: "user-defines" do you mean:
- Inverse meta-data from the root package ONLY to its dependencies, or,
- Inverse meta-data from any package to its direct dependencies.
The latter. The former would not support package:localization
, because the root package is package:my_app
.
A third option would be: Inverse meta-data from any package to its transitive dependencies. Some conceptual use case that would benefit from that is a bunch of packages standardizing on some config keys, and then some package similar to flutter_lints would provide a bunch of default "inverse meta data" (we should probably just call it config). That being said, it's a bit of a contrived example, so only showing it to direct dependencies would probably be better.
I would suggest avoiding magic values like
_all
. I'm sure we'll enable people to make the_all
package, and things will be bad π π π
If the config is a config.dart
or build_config.dart
script, and not a yaml, we have an API, so we don't have to resort to magic config keys in a yaml file.
In fact, it might be worth down scoping and not shipping with "user-defines" initially, is there any reason it can't be added at a later stage?
The native assets (dynamic library bundling), can be shipped without indeed. I was pushing on this to unblock "data assets" to unblock @mosuem on the localization package.
From options:
I thought you wanted (A). I see little point in doing (C). Because if you know that package:foo
is in your transitive dependencies and you want to supply configuration for it, then you can simply add a dependency on package:foo
.
I suppose (C) makes some sense if (C) is an optional dependency, but we don't really have that concept. And if you supply config to package foo
, you should probably have a dependency constraint on foo
-- otherwise, you package will break if the next major version of foo
requires config on a different format / shape.
We'd like users to be able to configure the build of their native dependencies.
We'd probably want to support multiple ways of providing this user-defined config, similar to
package:cli_config
. However, in this case we also need to reason about the command-line API of the launcher scripts (dartdev and flutter_tools).One downside of the
package:cli_config
standard is that it requires passing--config=
to pass the config file. Instead (or in addition) we might want to consider a default place to pass configuration. This could be thepubspec.yaml
to have a single location for configuration.Two options would be (1) a top level
Originally posted by @GregoryConrad in https://github.com/dart-lang/sdk/issues/50565#issuecomment-1544616520 (modified by me)
@jonasfj probably has something to say about listing arbitrary key-values under the packages independencies
.Alternatively, we could make a toplevel
build_config.yaml
(which corresponds in naming tobuild.dart
).(Priority: not part of MVP, but will add this soon after.)