-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[native_assets_cli] Nest CodeConfig
OS-specific config
#1824
Conversation
This is by design and IMHO the right thing to do. The validators are intended to validate semantics, not syntactic things (this separation of syntax and semantic was done by my refactoring). Imagine we choose protobuf instead of JSON as the format. Then proto decoding would fail if a required field wasn't passed. The reason this surfaces now is that it's a breaking change to the "syntax" (if you will) to make an optional thing a mandatory thing. (That being said, there's an escape hatch. The data structures that view the config are intentionally providing access to the underlying json. So one can (if one really wants) use
Well, it turned from a semantic error into a syntactic error. So what had to be maintained with checking semantic variants is now automatically checked via syntax.
We probably don't have unit tests for all the different syntax errors one could make (e.g. providing strings where numbers are expected, arrays where maps are expected, etc). Maybe we don't have to have such tests in the first place (would we have these tests if the format was protobuf? would we craft invalid protobuf messages and check proto decoding fails with exceptions?). (One could imagine generating the "syntax parser code" from a json schema specification) |
That means we would never ever be able to upgrade a nullable field to a non-nullable field. However, we still want to provide our users with an API that makes semantically the most sense. So if it's never null (after some SDK version), a non-nullable accessor.
I like the distinction between syntax and semantics. However, we don't want syntactic errors to simply lead to an
If we never ever allow breaking changes, and we validate that the protobuf schema evolution ensures this, then no. If we follow that logic, we cannot change the field in the syntax to be required. But we'd still want a nicer API to our users than the protobuf. E.g. if the field is now non-nullable the users should get a non-nullable API and the deserialized protobuf has a nullable API. Are you suggesting we should have two representations? One for our users (non-nullable) and one that has only done the deserialization?
I guess that was my last question. Then we could write the validator and the unit tests on the intermediate data structure.
We could do such refactoring at this point ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm with comments addressed
import 'link_mode.dart'; | ||
|
||
// TODO: The validation should work on the JSON, not on the parsed config. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree, the validation should do semantic validation (not syntactic things) - imagine we choose Protobuf instead of Json, would we do validation on the raw protobuf bytes instead of the parsed protobuf?
Also the BuildConfig
does allow accessing the JSON itself (you even take advantage of it). So there's really no reason why we should be changing the validation APIs to work on JSON.
// The dry run will be removed soon. | ||
if (dryRun) return const []; | ||
|
||
final errors = <String>[]; | ||
final targetOSJson = json[targetOSConfigKey] as String?; | ||
final targetOS = targetOSJson == null ? null : OS.fromString(targetOSJson); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not particularly fond of these changes here. These changes occur because
- the underlying json schema makes the macos/android/ios/... configs optional, but they have to be present depending on OS
- we chose an API that makes them non-nullable
The second point: This pattern with non-nullable sub-fields (which are nullable on the protocol level) that should only be used under guard conditions usually have has*
getters. If we followed that style here as well, we'd have user code like this:
final codeConfig = buildConfig.codeConfig;
if (codeConfig.hasMacOSConfig) { // <-- could also use `codeConfig.targetOS == OS.macos`
use(codeConfig.macOSConfig); // <-- use non-nullable API
}
The validation would then look like this
switch (codeConfig.targetOS) {
case OS.macOS:
if (!codeConfig.hasMacOSConfig)) {
errors.add('...');
}
break;
...
}
That would make us not have to deal with JSON here at all. It would all be encapsulated in the CodeConfig
/... objects.
It's a choice the flutter tools and dartdev have to make. The file could not be valid json, the json could not comply with the schema or there could be semantic issues (e.g. duplicate dylib names). In general there's a contract between the Flutter/Dart SDK and the hook. If the hook violates the contract, then something will probably go wrong at some point. Though the question is: how likely is it going to violate a contract. For example:
There's an infinite of things that can go wrong and one cannot guard against all of them: For example we don't actually verify that a We should be checking common things that hook writers may do wrong or errors that occur for end users. Checking more than that is nice - and I'm not opposed to it - but wouldn't say it's absolutely neccesary. |
Yeah, that's not the thing I'm worried about. It's about us (me?) making a mistake in evolving the schema, that being a syntax error instead of a semantic error, and that then leading to an uncaught error in dartdev and flutter_tools. Anyway, I'll address the comments. 👍 Thanks for the review! |
I've changed the PR to not make it a syntactic error when the field is missing. E.g. constructing the |
@dcharkes I see you didn't take my suggestion but implemented it like this: IOSConfig.fromHookConfig(HookConfig config)
: _targetVersion = config.json.optionalInt(_targetIOSVersionKey),
_targetSdk = switch (config.json.optionalString(_targetIOSSdkKey)) {
null => null,
String e => IOSSdk.fromString(e)
};
}
extension IOSConfigSyntactic on IOSConfig {
IOSSdk? get targetSdkSyntactic => _targetSdk;
int? get targetVersionSyntactic => _targetVersion;
} This means accessing I view Similarly class AndroidConfig {
int get targetNdkApi => _targetNdkApi!;
final int? _targetNdkApi;
AndroidConfig({
required int targetNdkApi,
}) : _targetNdkApi = targetNdkApi;
} If we have an Another comment: We have now inconsistency between the class's constructors class CodeConfig {
CodeConfig(HookConfig config);
}
class IOSConfig {
IOSConfig.fromHookConfig(HookConfig config); // <-- uses named .fromHookConfig
} Could we keep constructor naming consistent? The final class CCompilerConfig {
late final Uri compiler;
late final Uri linker;
late final Uri archiver;
CCompilerConfig({
required this.archiver,
required this.compiler,
required this.linker,
this.envScript,
this.envScriptArgs,
});
factory CCompilerConfig.fromJson(Map<String, Object?> json) => CCompilerConfig(...);
Map<String, Object> toJson() => { ... } ? |
Yes, I've changed the fields being null to be a semantic error rather than a syntactic error. In order to be able to run the Since we don't have a way to check for syntactic errors, I want to avoid introducing new possible syntactic errors. And you don't want the
It's non-nullable for the semantic API. |
I imagine the json looking something like this
Now if the It can be avoided by either making if (codeConfig.targetOS == codeConfig.android) {
use(codeConfig.android!);
} That's somewhat similar to |
One step at the time. We want to get there. But I want to restructure the API how we want it to be first, before tackling the JSON. I don't want 5 JSON format breaking changes.
That's not the semantic API we want to give to users.
The
The |
👍 We never had a |
Dart and Flutter have been passing the minimum OS SDK versions for a while (in non-dry-run), which means we can mark these fields non-optional if the target OS is used.
This PR changes the
CodeConfig
to nest the OS versions (and iOS target SDK: device or simulator) to be nested under the OS and required.Other side effects of this PR:
native_toolchain_c
can no longer test without the minimum OS versions, which changed the iOS testotool
output.Addressing:
BuildConfig.targetOS
toBuildConfig.codeConfig.targetOS
, remove it entirely or extend to cover web #1738 (comment)