Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## 0.4.4

* `buildShaderBundleJson` now consumes `impellerc` depfiles when the resolved
compiler advertises `--depfile` support. This lets newer Flutter SDKs track
transitive shader `#include` dependencies while preserving the existing
manifest dependency scan for older `impellerc` builds that do not support the
flag or do not emit a depfile in `--shader-bundle` mode.
* `buildShaderBundleJson` accepts an `includeDirectories` parameter:
extra directories appended to `impellerc`'s `#include` search path,
after the manifest directory and the bundled `shader_lib`. This lets a
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ Use native asset build hooks to import Flutter GPU shader bundle assets.
});
}
```
`buildShaderBundleJson` always declares the manifest and directly listed
shader files as build dependencies. With Flutter SDKs whose `impellerc`
supports `--depfile` for shader bundles, it also declares transitive
`#include` dependencies from the generated depfile. Older `impellerc`
builds continue to use the manifest scan fallback.
4. In your project's `pubspec.yaml`, add an asset import rule to package the built shader bundles (this will become unnecessary once the build hook system supports `DataAsset` registration in a future release):
```yaml
flutter:
Expand Down
92 changes: 83 additions & 9 deletions lib/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,11 @@ Future<void> _buildShaderBundleJson({
///
/// The build hook needs to rerun when any input that influences the
/// produced shader bundle changes. We track the manifest itself and
/// every shader source file it references. The build hook framework
/// reruns this hook the next time any listed dependency's mtime
/// changes.
///
/// Transitive `#include`s aren't tracked yet. `impellerc`'s
/// `--depfile` switch (which would let us capture them) is a no-op
/// in `--shader-bundle` mode today; that's filed as an upstream
/// follow-up.
/// every shader source file it references before invoking `impellerc`,
/// so older compilers still get the same dependency tracking as previous
/// package versions. Newer compilers also emit a depfile that captures
/// transitive `#include`s; those dependencies are merged after a
/// successful compile.

buildOutput.dependencies.addAll(
collectShaderBundleDependencies(inputManifestFilePath, decodedManifest),
Expand All @@ -47,12 +44,21 @@ Future<void> _buildShaderBundleJson({

final impellercExec = await findImpellerC();
final shaderLibPath = impellercExec.resolve('./shader_lib');
final depfilePath = Uri.file('${outputBundleFilePath.toFilePath()}.d');
final depfile = File.fromUri(depfilePath);
final supportsDepfile = impellerCHelpSupportsDepfile(
_impellerCHelpText(impellercExec),
);
if (supportsDepfile && await depfile.exists()) {
await depfile.delete();
}
final impellercArgs = shaderBundleImpellercArguments(
outputBundleFilePath: outputBundleFilePath,
manifestJson: reconstitutedManifest,
manifestDirectory: inputManifestFilePath.resolve('./'),
shaderLibDirectory: shaderLibPath,
includeDirectories: includeDirectories,
depfilePath: supportsDepfile ? depfilePath : null,
);

final impellerc = Process.runSync(
Expand All @@ -65,6 +71,25 @@ Future<void> _buildShaderBundleJson({
'Failed to build shader bundle: ${impellerc.stderr}\n${impellerc.stdout}',
);
}

if (supportsDepfile && await depfile.exists()) {
final depfileContents = await depfile.readAsString();
buildOutput.dependencies.addAll(
parseImpellerCDepfileDependencies(
depfileContents,
relativeTo: packageRoot,
),
);
await depfile.delete();
}
}

String _impellerCHelpText(Uri impellercExec) {
final result = Process.runSync(impellercExec.toFilePath(), ['--help']);
if (result.exitCode != 0) {
return '';
}
return '${result.stdout}\n${result.stderr}';
}

/// Collects the build-system dependencies declared by a shader bundle
Expand Down Expand Up @@ -95,6 +120,50 @@ List<Uri> collectShaderBundleDependencies(
return result;
}

/// Returns whether the `impellerc --help` text advertises `--depfile`.
///
/// Older `impellerc` builds either omit the flag entirely or accept it without
/// producing a depfile in `--shader-bundle` mode. Build hooks use this helper
/// to avoid passing an unsupported flag while preserving the existing manifest
/// dependency fallback.
bool impellerCHelpSupportsDepfile(String helpText) {
return helpText.contains('--depfile=');
}

/// Parses dependency paths from an `impellerc` depfile.
///
/// `impellerc` emits a Ninja-style single-line depfile:
/// `<target>: <dep1> <dep2> ...`. The target is ignored. Dependency paths are
/// converted to file URIs; relative paths are resolved against [relativeTo]
/// when it is provided.
List<Uri> parseImpellerCDepfileDependencies(
String depfileContents, {
Uri? relativeTo,
}) {
final separator = RegExp(r':(?:\s|$)').firstMatch(depfileContents);
if (separator == null) {
return const [];
}
final dependencies = depfileContents.substring(separator.end).trim();
if (dependencies.isEmpty) {
return const [];
}
return dependencies
.split(RegExp(r'\s+'))
.where((dependency) => dependency.isNotEmpty)
.map((dependency) {
if (_isAbsoluteFilePath(dependency)) {
return Uri.file(dependency);
}
return relativeTo?.resolve(dependency) ?? Uri.file(dependency);
})
.toList();
}

bool _isAbsoluteFilePath(String path) {
return path.startsWith('/') || RegExp(r'^[a-zA-Z]:[/\\]').hasMatch(path);
}

/// Builds the `impellerc` argument list for a shader-bundle compile.
///
/// The first two `--include` directories are always the manifest's own
Expand All @@ -111,10 +180,12 @@ List<String> shaderBundleImpellercArguments({
required Uri manifestDirectory,
required Uri shaderLibDirectory,
List<Uri> includeDirectories = const [],
Uri? depfilePath,
}) {
return [
'--sl=${outputBundleFilePath.toFilePath()}',
'--shader-bundle=$manifestJson',
if (depfilePath != null) '--depfile=${depfilePath.toFilePath()}',
'--include=${manifestDirectory.toFilePath()}',
'--include=${shaderLibDirectory.toFilePath()}',
for (final directory in includeDirectories)
Expand All @@ -137,7 +208,10 @@ List<String> shaderBundleImpellercArguments({
///
/// The hook declares the manifest and every shader source file it
/// references as build-system dependencies, so the bundle is rebuilt
/// when any input changes.
/// when any input changes. When the resolved `impellerc` supports
/// `--depfile` in `--shader-bundle` mode, the hook also declares transitive
/// `#include` dependencies from the generated depfile. Older `impellerc`
/// builds fall back to the manifest scan.
///
/// The optional [includeDirectories] are added to `impellerc`'s `#include`
/// search path, after the manifest's directory and `impellerc`'s built-in
Expand Down
73 changes: 73 additions & 0 deletions test/build_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ void main() {
]);
});

test('emits depfile argument when a depfile path is provided', () {
final depfile = Uri.parse(
'file:///pkg/build/shaderbundles/bundle.shaderbundle.d',
);
final args = shaderBundleImpellercArguments(
outputBundleFilePath: out,
manifestJson: '{}',
manifestDirectory: manifestDir,
shaderLibDirectory: shaderLib,
depfilePath: depfile,
);
expect(args, contains('--depfile=${depfile.toFilePath()}'));
});

test('appends an --include for each extra directory, in order', () {
final depA = Uri.parse('file:///dep_a/shaders/');
final depB = Uri.parse('file:///dep_b/glsl/');
Expand Down Expand Up @@ -124,4 +138,63 @@ void main() {
expect(build(includeDirectories: const []), build());
});
});

group('impellerCHelpSupportsDepfile', () {
test('returns true when help advertises the depfile flag', () {
expect(
impellerCHelpSupportsDepfile(
'[optional] --depfile=<depfile_path>',
),
isTrue,
);
});

test('returns false when help omits the depfile flag', () {
expect(
impellerCHelpSupportsDepfile(
'[optional,multiple] --include=<include_directory>',
),
isFalse,
);
});
});

group('parseImpellerCDepfileDependencies', () {
test('parses dependency paths from a single-line depfile', () {
final deps = parseImpellerCDepfileDependencies(
'/pkg/build/shaderbundles/bundle.shaderbundle: '
'/pkg/shaders/main.frag /pkg/shaders/include/common.glsl\n',
);
expect(deps, [
Uri.parse('file:///pkg/shaders/main.frag'),
Uri.parse('file:///pkg/shaders/include/common.glsl'),
]);
});

test('resolves relative dependency paths against a base URI', () {
final deps = parseImpellerCDepfileDependencies(
'build/shaderbundles/bundle.shaderbundle: '
'shaders/main.frag shaders/include/common.glsl',
relativeTo: Uri.parse('file:///pkg/'),
);
expect(deps, [
Uri.parse('file:///pkg/shaders/main.frag'),
Uri.parse('file:///pkg/shaders/include/common.glsl'),
]);
});

test('handles a drive-letter colon in the target path', () {
final deps = parseImpellerCDepfileDependencies(
r'C:\pkg\build\shaderbundles\bundle.shaderbundle: '
'shaders/main.frag',
relativeTo: Uri.parse('file:///pkg/'),
);
expect(deps, [Uri.parse('file:///pkg/shaders/main.frag')]);
});

test('returns no dependencies when the depfile is missing deps', () {
expect(parseImpellerCDepfileDependencies('target:'), isEmpty);
expect(parseImpellerCDepfileDependencies(''), isEmpty);
});
});
}
Loading