-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[hook] hook/generate.dart
🪝
#56512
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
Comments
Summary: This issue proposes a standardized way to generate code in Dart packages without requiring user intervention. It aims to address limitations of existing solutions like macros and |
Another option would be to include |
TL;DR: Either run automatically or check in the code, not both.
That's an assumption for the input. We don't currently let macros access the file system, but we could. The alternative is to take the entire file, convert it to a Dart constant value, and have that in the macro annotation, which is just a two-step code generation where you generate the macro application code. (I'd totally do that.)
That's a very strong warning signal for me. Never check in auto-generated code. It's OK to have a generator that you run manually to generate some code, and then you check in the generated code. It's a code updater then, not a code generator. It's also OK to have an automatically run generator, which generates files on demand, before they are needed. If an automatically run generator is for Dart code, macros should be perfect for that. That's exactly the job macros are designed for, as long as they can get the data needed to generate the code. It's automatically generated code, guaranteed up-to-date, that you can still inspect, because every tool that processes Dart programs are aware of macros. It avoids any need ever storing the code, and checking it in. Now, for non-Dart output, macros are obviously not the answer. There will need to be some code to generate such files. And a standard for where to place them. (Should the be in If files are checked in, I'd be more comfortable with a non-automatically run step, where you have to run
That doesn't compute for me. Macros are code generators. You may have some heavy use-cases in mind, but whatever framework we create here should be able to handle both small and big tasks. And so should macros. |
Hm, I don't think we ever call these things code updaters, maybe we should. Then the hook should be called If we'd like to hook
https://pub.dev/packages/build_runner cc @davidmorgan |
Thanks Daco! Looking forward to getting into the discussion here. Re: checking in generated code, I don't think that's quite the right distinction to make. What's important is whether, after generation, the generator retains ownership of the output: has an opinion about whether it's correct. I've been calling generators that do not retain ownership of their output "offline generators". A web page can be an offline generator, you copy its output into your source tree and you're done. An online generator is the type of generator you want to run automatically. Whether you check in the output of online generators or not is mostly a secondary issue related to convenience of other tools, for example you might do it so the output and diffs are visible on GitHub. And for pub, you publish the generated output today because otherwise your package doesn't work :) Re: code generators being heavy to run--I'm working on this one, they should usually be about the same cost as lints. |
FFIgen and JNIgen are online generators in this distinction.
I'm not sure that FFIgen and JNIgen will be able to be that fast. So I think we'll always have faster and slower generators in our ecosystem. That being said, I'd love for FFIgen and JNIgen to be so fast that we can run them on-save on the source files. That way users can edit native code and live see the Dart bindings being updated. 😄 cc @HosseinYousefi @liamappelbe |
It's always necessary to support slow generators, but we can try to make ours fast ;) |
I haven't benchmarked it but JNIgen is quite fast on my machine. Especially if we're not recompiling code. Both tools are not going be called frequently if we're just generating ready libraries because they don't change. One case where we will be updating is pigeon. @tarrinneal is working on making pigeon work with FFIgen and JNIgen. We're generating the interfaces for Kotlin and Swift from a Dart IDL using |
+1 My expectation is that a "generator" would produce output based on some external input, while an "updater" would makes changes to a source file based on the content or changes made to that file. AFAIK we don't have any implementation which reads a file and updates it, only ones which overwrite the entire file. |
Some notes from a discussion with @mosuem:
// Sketch for a hook/generate.dart
import 'package:hook/generate.dart';
import 'package:ffigen/ffigen.dart';
import 'package:build_runner/build_runner.dart';
void main(List<String> arguments) async {
// This function doesn't return in `watch` mode, but does in batch mode.
await generate(arguments, (input, output) async {
final packageName = input.packageName;
final ffigen1 = FfiGen(
// ...
);
final ffigen2 = FfiGen(
// ...
);
final buildRunner = BuildRunner(
// ...
);
final customGenerator = MyGenerator(
// ... might not support watch mode
);
final generators = [ffigen1, ffigen2, buildRunner, customGenerator];
for (final generator in generators) {
// input contains whether it's `batch` or `watch` mode.
await generator.run(
input: input,
output: output,
logger:
Logger('')
..level = Level.ALL
..onRecord.listen((record) {
print(record.message);
}),
);
}
});
} |
I suspect that delegating "watch" responsibility to each generator is going to get complicated: it means there is no overall control, each generator will independently pick up changes and produce outputs. For cases where generators are independent this is just awkward, for cases where one generator output feeds into another's input it starts to break :) It might actually be simpler to turn ffigen and intl into badly behaved build_runner generators that do extra work on the side, in order to get build_runner to manage running them correctly interleaved with build_runner generators. |
I think the idea is to a single watcher, which notifiers each generator on changes.
I don't see the awkward part - and I would just disallow feeding inputs into other generators for starters. Long term, we might think about having a similar mechanism as for build hooks, where a DAG is constructed and invoked in order. |
Ah okay--possibly I was reading too much into the pseudocode :)
build_runner generators need to read config and need to know what files are on disk in order to figure out their inputs+outputs; so for example a generator might match build_runner generators that use the analyzer need to track transitive imports and count those as inputs; and this can include files that don't exist at the start of the build, and generated files that have imports. So ... yeah, complicated ;) It's fine to start simple, of course. |
That's an interesting situation indeed. And if we have multiple generators inside a single Maybe we should have different sockets for different generators if there's more than one generator specified in
I don't think that's a good idea, we'd want composability.
I think
In general, code generators can traverse the file system and come up with dependencies at runtime. That's why you produce a And in general, code generators can produce multiple output files, and come up with these at runtime. (Packaging outputs in a zip file to have a single output seems like a weird workaround. And we would also have to package the whole file system in a zip to have a single predefined input conceptually. Of course that would not work with caching, that zip would change all the time.) My hope would be that |
This is an extremely well explored space; what If a generator has full freedom to define its own outputs at runtime, then in order to determine the build graph you have to repeatedly run all the generators until the graph reaches a stable state. You end up with a build system that is extremely fragile and cannot be parallelized. What I 100% agree with addressing the use cases of generators that want more flexible output. Archives seems to me--at first glance--to be the right solution. There is no reason for archives to break caching: quite the reverse, an archive can provide digests for itself and for the files it contains, making caching simpler. There is no reason to use archives for input, individual file inputs are fine, any input can be generated as long as it comes from a generator that can cheaply compute its outputs. I'm sure we can come up with a simple+correct hack that works before archives are available / before we can build the needed support in build_runner. For example just running the noncompliant generators in the generate hook after build_runner in a fixed order, and forbidding any dependency in the other direction, is probably sufficient; we just hide the non-build_runner output completely from build_runner. Thanks. |
Some musings with @davidmorgan:
|
We can not produce a fully ordered package graph because we support dependency cycles at the package level.
dart-lang/build#967 has some discussion of how we might have built this support. I do think that it could work well to add a hook generators can implement to provide a |
We'd like a standardized way to generate code in Dart packages that does not require a user step.
package:build_runner
requires users to do a manual extra stepsIn some
hook/generate.dart
this would be apackage:build_runner
equivalent, but it would be aware of the Dart/Flutter SDK and have no race conditions between saving files and runningdart
andflutter
commands (see below).In #54334, we've discussed multiple aspects w.r.t. code generators. Today we discussed some more requirements offline:
pub publish
to ensure no files are outdated.pub get
, beforedart analyze
. (Only needed if the generated code is not checked in.)Assumptions:
Some open questions:
Should it be a different hook thanhook/build.dart
? Or a mode for the build hook?If it's the build hook, when do we run this hook in which modes.If it's not the build hook, how do we ensure thisgenerate
hook is finished before running the build hook? (E.g. generate is run on-save of its dependencies, likebuild_runner
. But then if you hot-restart in Flutter, you want that on-save action to be fully done first.)hook/build.dart
, it's run in a different phase in the developer workflow. Build hooks are run onflutter build
/dart build
when you want to make an application bundle for a specific target (target OS, target architecture, target OS API levels, specific flavor, etc.) The generate hook should be run after dependencies change (and before runningdart analyze
, to prevent dart analysis errors showing up when both the source and target is Dart.Use cases:
build_runner
setupsBackground knowledge:
build.rs
enables generating of Rust files. https://doc.rust-lang.org/cargo/reference/build-script-examples.html#code-generationThanks @mosuem @HosseinYousefi @mkustermann @liamappelbe for the discussion! Please elaborate on things I left out.
The text was updated successfully, but these errors were encountered: