Unverified Commit 6d0b1ef8 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] include LICENSE files as build dependencies (#50945)

parent 74e564da
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'base/context.dart'; import 'base/context.dart';
...@@ -37,6 +38,10 @@ abstract class AssetBundleFactory { ...@@ -37,6 +38,10 @@ abstract class AssetBundleFactory {
abstract class AssetBundle { abstract class AssetBundle {
Map<String, DevFSContent> get entries; Map<String, DevFSContent> get entries;
/// Additional files that this bundle depends on that are not included in the
/// output result.
List<File> get additionalDependencies;
bool wasBuiltOnce(); bool wasBuiltOnce();
bool needsBuild({ String manifestPath = defaultManifestPath }); bool needsBuild({ String manifestPath = defaultManifestPath });
...@@ -70,6 +75,8 @@ class _ManifestAssetBundle implements AssetBundle { ...@@ -70,6 +75,8 @@ class _ManifestAssetBundle implements AssetBundle {
// updated without changes to the manifest. // updated without changes to the manifest.
final Map<Uri, Directory> _wildcardDirectories = <Uri, Directory>{}; final Map<Uri, Directory> _wildcardDirectories = <Uri, Directory>{};
final LicenseCollector licenseCollector = LicenseCollector(fileSystem: globals.fs);
DateTime _lastBuildTimestamp; DateTime _lastBuildTimestamp;
static const String _assetManifestJson = 'AssetManifest.json'; static const String _assetManifestJson = 'AssetManifest.json';
...@@ -246,10 +253,15 @@ class _ManifestAssetBundle implements AssetBundle { ...@@ -246,10 +253,15 @@ class _ManifestAssetBundle implements AssetBundle {
entries[kFontManifestJson] = DevFSStringContent(json.encode(fonts)); entries[kFontManifestJson] = DevFSStringContent(json.encode(fonts));
// TODO(ianh): Only do the following line if we've changed packages or if our LICENSE file changed // TODO(ianh): Only do the following line if we've changed packages or if our LICENSE file changed
entries[_license] = _obtainLicenses(packageMap, assetBasePath, reportPackages: reportLicensedPackages); final LicenseResult licenseResult = licenseCollector.obtainLicenses(packageMap);
entries[_license] = DevFSStringContent(licenseResult.combinedLicenses);
additionalDependencies = licenseResult.dependencies;
return 0; return 0;
} }
@override
List<File> additionalDependencies = <File>[];
} }
class _Asset { class _Asset {
...@@ -336,78 +348,108 @@ List<_Asset> _getMaterialAssets(String fontSet) { ...@@ -336,78 +348,108 @@ List<_Asset> _getMaterialAssets(String fontSet) {
return result; return result;
} }
final String _licenseSeparator = '\n' + ('-' * 80) + '\n';
/// Returns a DevFSContent representing the license file. /// Processes dependencies into a string representing the license file.
DevFSContent _obtainLicenses( ///
PackageMap packageMap, /// Reads the LICENSE file from each package in the .packages file, splitting
String assetBase, { /// each one into each component license (so that we can de-dupe if possible).
bool reportPackages, /// If provided with a pubspec.yaml file, only direct depedencies are included
}) { /// in the resulting LICENSE file.
// Read the LICENSE file from each package in the .packages file, splitting ///
// each one into each component license (so that we can de-dupe if possible). /// Individual licenses inside each LICENSE file should be separated by 80
// /// hyphens on their own on a line.
// Individual licenses inside each LICENSE file should be separated by 80 ///
// hyphens on their own on a line. /// If a LICENSE file contains more than one component license, then each
// /// component license must start with the names of the packages to which the
// If a LICENSE file contains more than one component license, then each /// component license applies, with each package name on its own line, and the
// component license must start with the names of the packages to which the /// list of package names separated from the actual license text by a blank
// component license applies, with each package name on its own line, and the /// line. (The packages need not match the names of the pub package. For
// list of package names separated from the actual license text by a blank /// example, a package might itself contain code from multiple third-party
// line. (The packages need not match the names of the pub package. For /// sources, and might need to include a license for each one.)
// example, a package might itself contain code from multiple third-party // Note: this logic currently has a bug, in that we collect LICENSE information
// sources, and might need to include a license for each one.) // for dev_dependencies and transitive dev_dependencies. These are not actually
final Map<String, Set<String>> packageLicenses = <String, Set<String>>{}; // compiled into the released application and don't need to be included. Unfortunately,
final Set<String> allPackages = <String>{}; // the pubspec.yaml alone does not have enough information to determine which
for (final String packageName in packageMap.map.keys) { // dependencies are transitive of dev_dependencies, so a simple filter isn't sufficient.
final Uri package = packageMap.map[packageName]; class LicenseCollector {
if (package == null || package.scheme != 'file') { LicenseCollector({
continue; @required FileSystem fileSystem
} }) : _fileSystem = fileSystem;
final File file = globals.fs.file(package.resolve('../LICENSE'));
if (!file.existsSync()) { final FileSystem _fileSystem;
continue;
} /// The expected separator for multiple licenses.
final List<String> rawLicenses = static final String licenseSeparator = '\n' + ('-' * 80) + '\n';
file.readAsStringSync().split(_licenseSeparator);
for (final String rawLicense in rawLicenses) { /// Obtain licenses from the `packageMap` into a single result.
List<String> packageNames; LicenseResult obtainLicenses(
String licenseText; PackageMap packageMap,
if (rawLicenses.length > 1) { ) {
final int split = rawLicense.indexOf('\n\n'); final Map<String, Set<String>> packageLicenses = <String, Set<String>>{};
if (split >= 0) { final Set<String> allPackages = <String>{};
packageNames = rawLicense.substring(0, split).split('\n'); final List<File> dependencies = <File>[];
licenseText = rawLicense.substring(split + 2);
} for (final String packageName in packageMap.map.keys) {
final Uri package = packageMap.map[packageName];
if (package == null || package.scheme != 'file') {
continue;
} }
if (licenseText == null) { final File file = _fileSystem.file(package.resolve('../LICENSE'));
packageNames = <String>[packageName]; if (!file.existsSync()) {
licenseText = rawLicense; continue;
} }
packageLicenses.putIfAbsent(licenseText, () => <String>{})
..addAll(packageNames);
allPackages.addAll(packageNames);
}
}
if (reportPackages) { dependencies.add(file);
final List<String> allPackagesList = allPackages.toList()..sort(); final List<String> rawLicenses = file
globals.printStatus('Licenses were found for the following packages:'); .readAsStringSync()
globals.printStatus(allPackagesList.join(', ')); .split(licenseSeparator);
for (final String rawLicense in rawLicenses) {
List<String> packageNames;
String licenseText;
if (rawLicenses.length > 1) {
final int split = rawLicense.indexOf('\n\n');
if (split >= 0) {
packageNames = rawLicense.substring(0, split).split('\n');
licenseText = rawLicense.substring(split + 2);
}
}
if (licenseText == null) {
packageNames = <String>[packageName];
licenseText = rawLicense;
}
packageLicenses.putIfAbsent(licenseText, () => <String>{})
..addAll(packageNames);
allPackages.addAll(packageNames);
}
}
final List<String> combinedLicensesList = packageLicenses.keys
.map<String>((String license) {
final List<String> packageNames = packageLicenses[license].toList()
..sort();
return packageNames.join('\n') + '\n\n' + license;
}).toList();
combinedLicensesList.sort();
final String combinedLicenses = combinedLicensesList.join(licenseSeparator);
return LicenseResult(
combinedLicenses: combinedLicenses,
dependencies: dependencies,
);
} }
}
final List<String> combinedLicensesList = packageLicenses.keys.map<String>( /// The result of processing licenses with a [LicenseCollector].
(String license) { class LicenseResult {
final List<String> packageNames = packageLicenses[license].toList() const LicenseResult({
..sort(); @required this.combinedLicenses,
return packageNames.join('\n') + '\n\n' + license; @required this.dependencies,
} });
).toList();
combinedLicensesList.sort();
final String combinedLicenses = combinedLicensesList.join(_licenseSeparator); /// The raw text of the consumed licenses.
final String combinedLicenses;
return DevFSStringContent(combinedLicenses); /// Each license file that was consumed as input.
final List<File> dependencies;
} }
int _byBasename(_Asset a, _Asset b) { int _byBasename(_Asset a, _Asset b) {
......
...@@ -72,7 +72,7 @@ Future<Depfile> copyAssets(Environment environment, Directory outputDirectory) a ...@@ -72,7 +72,7 @@ Future<Depfile> copyAssets(Environment environment, Directory outputDirectory) a
resource.release(); resource.release();
} }
})); }));
return Depfile(inputs, outputs); return Depfile(inputs + assetBundle.additionalDependencies, outputs);
} }
/// Copy the assets defined in the flutter manifest into a build directory. /// Copy the assets defined in the flutter manifest into a build directory.
......
...@@ -17,6 +17,11 @@ Map<String, Uri> _parse(String packagesPath) { ...@@ -17,6 +17,11 @@ Map<String, Uri> _parse(String packagesPath) {
class PackageMap { class PackageMap {
PackageMap(this.packagesPath); PackageMap(this.packagesPath);
/// Create a [PackageMap] for testing.
PackageMap.test(Map<String, Uri> input)
: packagesPath = '.packages',
_map = input;
static String get globalPackagesPath => _globalPackagesPath ?? kPackagesFileName; static String get globalPackagesPath => _globalPackagesPath ?? kPackagesFileName;
static set globalPackagesPath(String value) { static set globalPackagesPath(String value) {
......
...@@ -2,35 +2,40 @@ ...@@ -2,35 +2,40 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/depfile.dart';
import 'package:flutter_tools/src/build_system/targets/assets.dart'; import 'package:flutter_tools/src/build_system/targets/assets.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:platform/platform.dart';
import '../../../src/common.dart'; import '../../../src/common.dart';
import '../../../src/testbed.dart'; import '../../../src/context.dart';
void main() { void main() {
Environment environment; Environment environment;
Testbed testbed; FileSystem fileSystem;
Platform platform;
setUp(() { setUp(() {
testbed = Testbed(setup: () { platform = FakePlatform();
environment = Environment.test( fileSystem = MemoryFileSystem.test();
globals.fs.currentDirectory, environment = Environment.test(
); fileSystem.currentDirectory,
globals.fs.file(environment.buildDir.childFile('app.dill')).createSync(recursive: true); );
globals.fs.file(globals.fs.path.join('packages', 'flutter_tools', 'lib', 'src', fileSystem.file(environment.buildDir.childFile('app.dill')).createSync(recursive: true);
'build_system', 'targets', 'assets.dart')) fileSystem.file('packages/flutter_tools/lib/src/build_system/targets/assets.dart')
..createSync(recursive: true); ..createSync(recursive: true);
globals.fs.file(globals.fs.path.join('assets', 'foo', 'bar.png')) fileSystem.file('assets/foo/bar.png')
..createSync(recursive: true); ..createSync(recursive: true);
globals.fs.file(globals.fs.path.join('assets', 'wildcard', '#bar.png')) fileSystem.file('assets/wildcard/#bar.png')
..createSync(recursive: true); ..createSync(recursive: true);
globals.fs.file('.packages') fileSystem.file('.packages')
..createSync(); ..createSync();
globals.fs.file('pubspec.yaml') fileSystem.file('pubspec.yaml')
..createSync() ..createSync()
..writeAsStringSync(''' ..writeAsStringSync('''
name: example name: example
flutter: flutter:
...@@ -38,29 +43,65 @@ flutter: ...@@ -38,29 +43,65 @@ flutter:
- assets/foo/bar.png - assets/foo/bar.png
- assets/wildcard/ - assets/wildcard/
'''); ''');
});
}); });
test('Copies files to correct asset directory', () => testbed.run(() async { testUsingContext('includes LICENSE file inputs in dependencies', () async {
fileSystem.file('.packages')
.writeAsStringSync('foo:file:///bar/lib');
fileSystem.file('bar/LICENSE')
..createSync(recursive: true)
..writeAsStringSync('THIS IS A LICENSE');
await const CopyAssets().build(environment);
final File depfile = environment.buildDir.childFile('flutter_assets.d');
expect(depfile, exists);
final DepfileService depfileService = DepfileService(
logger: null,
fileSystem: fileSystem,
platform: platform,
);
final Depfile dependencies = depfileService.parse(depfile);
expect(
dependencies.inputs.firstWhere((File file) => file.path == '/bar/LICENSE', orElse: () => null),
isNotNull,
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
testUsingContext('Copies files to correct asset directory', () async {
await const CopyAssets().build(environment); await const CopyAssets().build(environment);
expect(globals.fs.file(globals.fs.path.join(environment.buildDir.path, 'flutter_assets', 'AssetManifest.json')).existsSync(), true); expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/AssetManifest.json'), exists);
expect(globals.fs.file(globals.fs.path.join(environment.buildDir.path, 'flutter_assets', 'FontManifest.json')).existsSync(), true); expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/FontManifest.json'), exists);
expect(globals.fs.file(globals.fs.path.join(environment.buildDir.path, 'flutter_assets', 'LICENSE')).existsSync(), true); expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/LICENSE'), exists);
// See https://github.com/flutter/flutter/issues/35293 // See https://github.com/flutter/flutter/issues/35293
expect(globals.fs.file(globals.fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), true); expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/foo/bar.png'), exists);
// See https://github.com/flutter/flutter/issues/46163 // See https://github.com/flutter/flutter/issues/46163
expect(globals.fs.file(globals.fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/wildcard/%23bar.png')).existsSync(), true); expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/wildcard/%23bar.png'), exists);
})); }, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
test('FlutterPlugins updates required files as needed', () => testbed.run(() async { testUsingContext('FlutterPlugins updates required files as needed', () async {
globals.fs.file('pubspec.yaml') fileSystem.file('pubspec.yaml')
..writeAsStringSync('name: foo\ndependencies:\n foo: any\n'); ..writeAsStringSync('name: foo\ndependencies:\n foo: any\n');
await const FlutterPlugins().build(Environment.test( await const FlutterPlugins().build(Environment.test(
globals.fs.currentDirectory, fileSystem.currentDirectory,
)); ));
expect(globals.fs.file('.flutter-plugins').existsSync(), true); expect(fileSystem.file('.flutter-plugins'), exists);
})); }, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment