Skip to content
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] Move BuildConfig.targetOS to BuildConfig.codeConfig.targetOS, remove it entirely or extend to cover web #1738

Open
mkustermann opened this issue Nov 20, 2024 · 14 comments
Assignees
Labels
P1 A high priority bug; for example, a single project is unusable or has many test failures package:native_assets_cli

Comments

@mkustermann
Copy link
Member

Currently the CLI protocol mandates BuildConfig.targetOS. Though making builds for the web platform wouldn't have any operating system. So we could either move this to BuildConfig.codeConfig.targetOS or remove it entirely.

We may decide that we introduce a BuildConfig.targetPlatform which is the platform we're going to run Dart code on - but this is different from an operating system concept ("web" is a target platform, but not an operating system).

/cc @dcharkes

@dcharkes
Copy link
Collaborator

remove it entirely

This is not an option right? hook/build.dart needs to know what OS a dylib must be built for.

Move [...] or extend to cover web

BuildConfig.codeConfig.targetOS missing while BuildConfig.codeConfig.targetArchitecture is there is slightly annoying, so moving could make sense.

BuildConfig.targetPlatform

That would be native and web?

If users have data assets that are OS dependent, they might start misusing config.codeConfig.targetOS to emit different icons for Android/iOS. How confident are you that we don't need an OS for data assets?

@mkustermann
Copy link
Member Author

mkustermann commented Nov 20, 2024

Again, the concept of "operating system" does not make sense in general, because for web builds there's no operating system.

The more general concept of "target platform" is a superset, it gives the same information as OS but also allows expressing we compile for the web. So that would allow removing targetOS (as it can be deducted from the more general targetPlatform).

We could structure it slightly differently and have

BuildConfig
  .targetPlatform (if it's native, .nativeConfig will be set, if it's web .webConfig will be set)
  .nativeConfig
     .targetOS
     .codeConfig
         .cCompiler
           .archiver/.linker/....
  .webConfig
     .compiler (JS or Wasm)
     .crossOriginIsolated (or whathever web specific options)

In any case I believe for web builds it doesn't make sense to have OS - except if made some artificial OS.web but that seems very weird because the actual OS the web build runs on a particular operating system, we just don't know at compile-time.

I think we should reduce the top-level scope on BuildConfig to an absolute minimum.

@dcharkes
Copy link
Collaborator

.nativeConfig .webConfig

👍
I like this more than .codeConfig and .dataConfig. Because it feels somewhat weird to duplicate info such as targetOS in both .codeConfig and .dataConfig or say .dataConfig does not get access to to OS because it might not exist on the web.

@mkustermann
Copy link
Member Author

mkustermann commented Nov 20, 2024

I like this more than .codeConfig and .dataConfig. Because it feels somewhat weird to duplicate info such as targetOS in both .codeConfig and .dataConfig or say .dataConfig does not get access to to OS because it might not exist on the web.

I would never introduce .dataConfig. But .codeConfig may still make sense because one can imagine situations where a dart build targets native (e.g. linux) but doesn't support code assets. In which case the bundling tool doesn't have to provide c compiler configuration, target abi levels, etc (basically codeConfig is there or not there and it groups multiple things that are needed when code assets are enabled - otherwise we'd have several fields on .nativeConfig that are either all set or none set depending on whether code assets are enabled)

@dcharkes dcharkes added the P1 A high priority bug; for example, a single project is unusable or has many test failures label Nov 28, 2024
@dcharkes
Copy link
Collaborator

BuildConfig
  .targetPlatform (if it's native, .nativeConfig will be set, if it's web .webConfig will be set)
  .nativeConfig
     .targetOS
     .codeConfig
         .cCompiler
           .archiver/.linker/....
  .webConfig
     .compiler (JS or Wasm)
     .crossOriginIsolated (or whathever web specific options)

I think we should reduce the top-level scope on BuildConfig to an absolute minimum.

This should probably be addressed together with restructuring the JSON:

@dcharkes dcharkes changed the title Move BuildConfig.targetOS to BuildConfig.codeConfig.targetOS, remove it entirely or extend to cover web [native_assets_cli] Move BuildConfig.targetOS to BuildConfig.codeConfig.targetOS, remove it entirely or extend to cover web Nov 28, 2024
@dcharkes dcharkes self-assigned this Dec 4, 2024
@dcharkes
Copy link
Collaborator

dcharkes commented Dec 9, 2024

.codeConfig may still make sense because one can imagine situations where a dart build targets native (e.g. linux) but doesn't support code assets. In which case the bundling tool doesn't have to provide c compiler configuration, target abi levels, etc (basically codeConfig is there or not there and it groups multiple things that are needed when code assets are enabled - otherwise we'd have several fields on .nativeConfig that are either all set or none set depending on whether code assets are enabled)

Thinking a bit further along these lines. Once we add support for WasmAssets, we'll have a wasmConfig (detailing whether we need emscripten ABI, WASI is supported etc.) It might be conceivable that there could be native embedders that would include a wasm runtime, in which case it would make more sense to have config.wasmConfig rather than config.webConfig.wasmConfig.

We might as well adopt the convention that we have toplevel configs for

  1. platforms - optional based on if that is the targetPlatform,
  2. asset types - optional for if those asset types are in buildAssetTypes, and
  3. embedders - optional for if an embedder wants to provide it.
BuildConfig
  // 0. shared
  .outputDirectory
  .outputDirectoryShared
  .packageName
  .packageRoot
  // 1. per target platform
  .targetPlatform // if it's native, .nativeConfig will be set, if it's web .webConfig will be set
  .nativeConfig
    .targetOS
  .webConfig
    .compiler // JS or Wasm
    .crossOriginIsolated // or whatever web specific options
  // 2. per asset
  .buildAssetTypes // every asset type has it's own config, optionally
  .codeConfig
    .targetArchitecture
    .buildMode // This is `config.buildMode` currently, but shouldn't this be for code assets only?
    .linkModePreference
    .targetIOSVersion
    .targetMacOSVersion
    .targetAndroidNdkApi
    .targetIOSSdk
    .cCompiler
      .archiver
      .compiler
      .linker
      ...
  .dataConfig // we haven't found a need yet, so doesn't exist currently
  .wasmConfig
    .abi // emscripten
    .wasiVersion // 'Preview 1' currently
  .javaConfig // or named differently if we'd include Kotlin things?
    .gradlePath // would something like this be useful?
  .iconConfig // in Flutter only, for IconAsset
    .resolution // xxx dpi
  .fontConfig // in Flutter only, for FontAsset
    ... //  Maybe no need for this either
  // 3. per embedder
  .flutterConfig
    .buildMode // release, debug, profile (different from codeConfig.buildMode)

Current target platforms: native and web. dart list flutter list.

(Side note: Using nesting to detail whether things are available is nice. However it doesn't always work. config.codeConfig.targetIOSVersion is only available if config.nativeConfig.os == OS.iOS. It would be tempting to have config.nativeConfig.iOSConfig.targetVersion, but then it would be only have to be if config.buildAssetTypes.contains('code'). So it has to be nullable in either location unfortunately.)

@HosseinYousefi For JarAssets, I'd expect Flutter could provide a config.javaConfig or config.jarConfig and provide a path to Gradle? etc. Any thoughts/ideas on what would be in there?

We could consider whether asset types should be nested under nativeConfig and webConfig. CodeAssets will likely only ever exist on native. WasmAsset, IconAsset, and FontAsset can be supported both on web and native. However, if we nest under platforms the uses of such configs will likely be written as config.nativeConfig.iconConfig ?? config.webConfig.iconConfig by helper libraries. So, I'm leaning towards toplevel for asset types.

@mkustermann Do you have any need for config.buildMode in the data assets or in the fonts/icons exploration in Flutter? If not, we should probably move it inside config.codeConfig.

(Edit: Maybe we should even leave config.targetPlatform out, then new embedders can even add target platforms without having to do a PR. config.isNative => config.nativeConfig != null.)

@mkustermann
Copy link
Member Author

mkustermann commented Dec 10, 2024

@mkustermann Do you have any need for config.buildMode in the data assets or in the fonts/icons exploration in Flutter? If not, we should probably move it inside config.codeConfig.

I think we should remove it entirely.

(Edit: Maybe we should even leave config.targetPlatform out, then new embedders can even add target platforms without having to do a PR. config.isNative => config.nativeConfig != null.)

I prefer us having config.nativeConfig etc as non-nullables as we already will have config.buildNativeCode getters (based on supportedAssetTypes / buildAssetTypes). So users can write

if (config.buildCodeAssets) {
  print(config.codeAsset.abi); // <- can access as non-nullable, no need for `!` operator
}
  .nativeConfig
   .targetOS
 .webConfig
  .compiler // JS or Wasm
  .crossOriginIsolated // or whatever web specific options
 // 2. per asset
 .buildAssetTypes // every asset type has it's own config, optionally
 .codeConfig
   .targetArchitecture
   .buildMode // This is `config.buildMode` currently, but shouldn't this be for code assets only?
   .linkModePreference
   .targetIOSVersion
   .targetMacOSVersion
   .targetAndroidNdkApi
   .targetIOSSdk
   .cCompiler

I do find it slightly confusing to have config.nativeConfig.targetOS and config.codeConfig.targetArchitecture.
I would prefer if everything a e.g. CLibraryBuilder needs is in one object that can accessed on config and passed to CLibraryBuilder - the information should ideally not been split between several objects.

Yes, theoretically one could have an arm32 interpreter running in the browser and one could theoretically then say a flutter web build should therefore set a C compiler configuration and ask packages to build arm32 C code which it will run in it's arm32 interpreter in the browser. But this is super contrived. I'm not sure we should be considering that use case.

Similarly, yes, theoretically one could have a wasm interpreter in an AOT-compiled android app. Therefore android flutter build apk builds should set a wasm compiler and bundle wasm modules. Though again this is a very contrived example.

What we should remember is that an asset type is usually coupled with a runtime API on how to access those asset. For example a hook emitting code assets is 1 <-> 1 coupled with dart code using dart:ffi. So from that perspective one could even think about having this (assuming we make dart:ffi work on the web)

// config.ffiConfig configures things for hooks
// hooks can produce wasm/native code
// that works with runtime's  `dart:ffi`s `@Native` API
config.ffiConfig.nativeCode.{cCompiler,abi,targetOS,...}
config.ffiConfig.wasmCode.{wasmCompiler,...}

The place where this would not be ideal if there was a need for a cCompiler for non-dart:ffi stuff. That would be a stronger argument for a top-level entry. But it's unclear if that would ever be a usecase. And if so, we would have a new runtime API on how to access them and possibly a new config (which make have overlaps with config.ffiConfig or not).

I'll think a bit more about this. Maybe we can discuss this in the next meeting.

@dcharkes
Copy link
Collaborator

dcharkes commented Dec 10, 2024

@mkustermann Do you have any need for config.buildMode in the data assets or in the fonts/icons exploration in Flutter? If not, we should probably move it inside config.codeConfig.

I think we should remove it entirely.

👍 As discussed offline, this brings it in line with our reasoning for not having optimization levels in the protocol as discussed in #1267.

(Edit: Maybe we should even leave config.targetPlatform out, then new embedders can even add target platforms without having to do a PR. config.isNative => config.nativeConfig != null.)

I prefer us having config.nativeConfig etc as non-nullables as we already will have config.buildNativeCode getters (based on supportedAssetTypes / buildAssetTypes). So users can write

if (config.buildCodeAssets) {
  print(config.codeAsset.abi); // <- can access as non-nullable, no need for `!` operator
}

👍

I do find it slightly confusing to have config.nativeConfig.targetOS and config.codeConfig.targetArchitecture.

Agreed. Confusing and not pretty!

I think it's likely that we want to use the targetOS for icons/images etc. Maybe config.codeConfig.targetOS should be an alias of config.nativeConfig.targetOS in the programmatic API. I do think targetOS needs to live outside code config. Most Flutter apps have different visuals (icons/images) for iOS vs Android. If we store it in the JSON in config.codeConfig.targetOS we're going to duplicate it in config.dataConfig.targetOS (for the images) and config.iconConfig.targetOS for the IconAssets.

What we should remember is that an asset type is usually coupled with a runtime API on how to access those asset. For example a hook emitting code assets is 1 <-> 1 coupled with dart code using dart:ffi. So from that perspective one could even think about having this (assuming we make dart:ffi work on the web)

// config.ffiConfig configures things for hooks
// hooks can produce wasm/native code
// that works with runtime's  `dart:ffi`s `@Native` API
config.ffiConfig.nativeCode.{cCompiler,abi,targetOS,...}
config.ffiConfig.wasmCode.{wasmCompiler,...}

That's an interesting point. Because we could potentially share the dart:ffi code between the CodeAsset and WasmAsset (as our FFIgenPad project has proven). We could kind of communicate config.hasFfi instead of assuming it based on config.platform = 'native'. On the other hand, having buildAssetTypes contain CodeAsset means dart:ffi is supported. I'm assuming the same is true for WasmAsset. Unless we want to support outputting WASM but using jsinterop to call that wasm. But in that case it should probably be output as a DataAsset and wired up in your JS code.

We might have use cases where we three ways adding an asset. Where two share a runtime API and the third does not:

// package:mysql hook/build.dart
if(config.nativeConfig.targetOS == OS.Android) {
  output.java.addJarAsset(...);
  // use dart:ffi via JNIgen generated code
} else if(config.buildCodeAssets) {
  CBuilder(...).build();
} else if(config.buildWasmAssets) {
  WasmBuilder(...).build(); // or maybe it should be called EmscriptenBuilder
}

But it would be nice if we can abstract the shared things using the same FFI code away as you suggest:

// package:mysql hook/build.dart
if(config.nativeConfig.targetOS == OS.Android) {
  output.java.addJarAsset(...);
  // use dart:ffi via JNIgen generated code
} else if(FfiBuilder.canBuild(config)) {
  FfiBuilder(...).build(); // delegates to CBuilder and WasmBuilder.
}

I'm not fully happy with FfiBuilder as a name. Maybe a better name would be MultiPlatCBuilder or CWasmBuilder.

// package:mysql hook/build.dart
if(config.nativeConfig.targetOS == OS.Android) {
  output.java.addJarAsset(...);
  // use dart:ffi via JNIgen generated code
} else if(CWasmBuilder.canBuild(config)) {
  CWasmBuilder(...).build(); // delegates to CBuilder and WasmBuilder.
}

I am not entirely sure if we need a shared config for the CWasmBuilder though, config where both the CBuilder and EmscriptenBuilder access what they need should be enough. Do you have any ideas what we would put in config.ffiConfig? (I also don't like that name, it's not FFI configuration, it's configuration for the two asset types that are accessible via the shared API).

The place where this would not be ideal if there was a need for a cCompiler for non-dart:ffi stuff. That would be a stronger argument for a top-level entry. But it's unclear if that would ever be a usecase. And if so, we would have a new runtime API on how to access them and possibly a new config (which make have overlaps with config.ffiConfig or not).

I can't think of a use case either.

auto-submit bot pushed a commit that referenced this issue Dec 10, 2024
The first in a series of refactorings to address:

* #1738

We align `BuildMode` with `OptimizationLevel`, it's configured by the hook author in the build hook. Likely, every package should ship with max optimization level and build mode set to release. Only while developing a package (when it is the root package or path-depsed in) would the build mode be set to debug.

This will require a manual roll into dartdev and flutter tools due to the removal of `BuildMode` from the `native_assets_builder` API.

The native assets builder will still emit the field in the JSON, until we can bump the `native_assets_cli` SDK constraint to 3.7 stable. Then only people with older versions of the `native_assets_cli` package but a newer SDK break.
@dcharkes
Copy link
Collaborator

Notes from discussion with @mkustermann:

  • The overall idea of having top level keys (1) per asset-type, (2) per platform, and (3) per embedder is a good design.
  • We don't necessarily have to avoid duplicate information. If there is duplicate information in two asset type configs (for example targetOS for CodeAsset and IconAsset) that information might still be omitted of neither of those asset types are supported, which would make it optional in the root, while it might be mandatory within those two asset type configs. So it's better to move things under asset types.
    • Data assets for now should not have access to the target OS. We can consider adding that in the future.
    • The targetOS can be moved under .codeConfig for now.
  • Existing hooks will be broken once Data assets for web get added by Flutter, since it will not pass the .codeConfig and thus will not pass the targetArchitecture which existing hooks expect to be there. We don't really have a way around this.
    • The existing hooks should be rewritten in a if (config.buildCodeAssets){ CBuilder(...) style.
    • Or alternatively, CBuilder should be a no-op if code assets should not be built, with a message to the logger that it was skipped.
  • Small points
    • config.codeConfig -> config.code
    • linkConfig.codeAssets -> linkConfig.assets.code or linkConfig.assetsForLinking.code
    • config.codeConfig.targetIOSVersion -> config.codeConfig.ios.targetVersion (ditto for the other OS-specific fields)

@mkustermann
Copy link
Member Author

(Maybe an additional thing: I don't like that CCompilerConfig is non-nullable but CCompilerConfig.{ar,cc,...} are nullable. It seems better that CCompilerConfig is optional (some end users may not have a c toolchain installed) and therefore nullable, but if the c toolchain is installed, all/most fields are non-nullable)

@dcharkes
Copy link
Collaborator

We should also consider if we want to run the envScript early and capture all the modified environment variables and pass them in instead of the script path. Downside: We'll need to do a process invocation every flutter_tools invocation, even if only data assets are used.

@mkustermann
Copy link
Member Author

We should also consider if we want to run the envScript early and capture all the modified environment variables and pass them in instead of the script path. Downside: We'll need to do a process invocation every flutter_tools invocation, even if only data assets are used.

Yes we may also want to do that as part of CLI cleanup: #1606

auto-submit bot pushed a commit that referenced this issue Dec 11, 2024
Next refactoring in:

* #1738

We move `config.targetOS` to `config.codeConfig.targetOS`. Other asset types might not get access on the target OS.

This will require a manual roll into dartdev and flutter tools due to the targetOS now having to be passed in via `setupCodeConfig` instead of the base config.

The native assets builder will still emit the same JSON if code assets are supported. Since both Dart and Flutter support code assets currently, this will not break anything. Once data assets for web in Flutter are added, all existing build hooks that assume code assets are always supported will break.
auto-submit bot pushed a commit that referenced this issue Dec 12, 2024
Addressing:

* #1738 (comment)

Currently on Flutter and the Dart CI provide this information, and they always provide all 3. So this should not be a breaking change on the protocol level.

It is a breaking change on the API level.
auto-submit bot pushed a commit that referenced this issue Dec 18, 2024
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:

* Many unit tests where missing these (now) mandatory fields
* `native_toolchain_c` can no longer test without the minimum OS versions, which changed the iOS test `otool` output.

Addressing:

* #1738 (comment)
@dcharkes
Copy link
Collaborator

  • linkConfig.codeAssets -> linkConfig.assets.code or linkConfig.assetsForLinking.code

I think I like linkConfig.code.assets better, that makes it consistent with config.code and output.code. I've prototyped both options:

@mkustermann Let me know what you think.

@dcharkes
Copy link
Collaborator

dcharkes commented Dec 19, 2024

Moving the discussion back to here:

Based on recent discussions with @mosuem @liamappelbe about what is used to hash the build directory and this PR restructuring, we may want to consider rename a few things and do the following

// build hook
main(...) asyc {
  await build((BuildInput input, BuildOutputBuilder output) {
    input.outputDirectory;
    input.config.{code,...} // <-- only this goes into hashing
    input.metadata.* // <-- metadata emitted from package deps we can utilize

    output.assets.code.add();
  });
}

// link hook
main(...) asyc {
  await link((LinkInput input, LinkOutputBuilder output) {
    input.outputDirectory
    input.config.{code,...}. // <-- only this goes into hashing
    input.assets.{code,data,...} // <-- symmetric to `hook/build.dart`'s `output.assets.{code,data,...}`

    output.assets.code.add();
  });
}

That would achieve

  • very clear separation of configuration (target platform, ...) from input data (metadata, assets to be linked, etc)
  • very clear what goes into build dir hash and what doesn't

Originally posted by @mkustermann in #1830 (comment)


I like the idea of input and output being symmetric.

I like the idea of having very clear what leads to directory sharing and not.

Any input or dependencies changing means being reinvoked. That's also easy to understand.

I'm not entirely sure about "config". #startBrainDump It feels more like a "target" e.g. how build systems can target multiple target tripples. And some build systems use target for the asset being built rather than the target tripple, which corresponds more closely to our buildAssetTypes which is also a part of our target config. It's a configuration for what? For the target we're building for. But, I also don't really like target. #endBrainDump

And then we have to decide where packageName and packageRoot go. In the input or the config?

  • packageRoot can go in the input instead of the config, any change to the input would reinvoke the hook, and if you change package version via pubspec but your native assets didn't change, you don't have to redownload anything
  • packageName. Well we for sure need to have different directories for different packages, but we can do <packagename>/<checksum> instead of embedding the name inside the checksum.

With packageName and packageRoot being part of input. We only have config for the target asset types and the embedder left over in "target config".

Holistic overview of the JSON

BuildInput // all influence whether the hook is reinvoked
  // a. shared
  .packageName
  .packageRoot
  .outputDirectory
  .outputDirectoryShared
  .outputJson // instead of a predefined filename in outputDirectory
  // b. hook specific
  .assetsForLinking // link only
  .metaData // build only
  // c. target config - all influence whether the output dir is shared
  .target // TargetConfig - shared for both link and build
    // 1. per target platform
    .platform // if it's native, .native will be set, if it's web .web will be set
    .native // we have no current need for this, everything lives in input.target.code
    .web
      .compiler // JS or Wasm
      .crossOriginIsolated // or whatever web specific options
    // 2. per asset
    .assetTypes // every asset type has it's own config, optionally
    .code
      .linkModePreference
      .targetArchitecture
      .targetOS
      .iOS
        .targetVersion
        .targetIOSSdk
      .macOS
        .targetVersion
      .android
        .targetNdkApi
      .cCompiler
        .archiver
        .compiler
        .linker
      .dartCApi // Dynamically linked via VM, so available in every embedder
    .data // we haven't found a need yet, so doesn't exist currently
    .wasm
      .abi // emscripten
      .wasiVersion // 'Preview 1' currently
    .java // or named differently if we'd include Kotlin things?
      .gradlePath // would something like this be useful?
    .icon // in Flutter only, for IconAsset
      .resolution // xxx dpi
    .font // in Flutter only, for FontAsset
      ... //  Maybe no need for this either
    // 3. per embedder
    .flutter
      .buildMode // release, debug, profile
      .code
        .cApi

Re Dart API:

This changes output.codeAssets.add( to output.code.addAsset(.

I prefer we don't use addAsset() and just have add() because it's somewhat clear that we're adding assets. I prefer output.assets.code.add() that groups all assets under one output.assets.

That's more in line with the v1 prototype. With the explicit split between input.target.code and linkInput.assets, linkInput.assets.code probably fits well.

Sorry for the very long write up. 😄 PTAL @liamappelbe @mosuem @mkustermann (Optional @HosseinYousefi) if anything looks weird before I start a big refactoring! 🚧

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
P1 A high priority bug; for example, a single project is unusable or has many test failures package:native_assets_cli
Projects
None yet
Development

No branches or pull requests

2 participants