Commit 69d78325 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Licenses (#4984)

This makes the about page show the licenses of all the Dart packages that a Flutter app uses.

Issues that this does not yet resolve:
- I'm still working on getting the full list of licenses for the sky_engine package.
- Some of the licenses don't print very readably.
- There's no scrollbar on the license page.

I'll provide fixes for the first two in the coming days, but this should unblock anyone who is wanting to see something here, even if it's not quite complete. :-)

----

The patch makes the following changes: 

- The license registry is now asynchronous, since the data comes from disk.
- I moved the default license collector from the foundation package to the services package since it uses the default asset bundle now.
- The FLX builder now includes the LICENSE files of each Dart package mentioned in the `.packages` file.
parent 79559d39
......@@ -9,7 +9,7 @@ detect_error_on_exit() {
set +x
if [[ $exit_code -ne 0 ]]; then
echo -e "\x1B[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1B[0m"
echo -e "\x1B[31m\x1B[1mError:\x1B[0m\x1B[31m script exited early due to error ($exit_code)"
echo -e "\x1B[1mError:\x1B[31m script exited early due to error ($exit_code)\x1B[0m"
echo -e "\x1B[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1B[0m"
fi
}
......
......@@ -11,7 +11,6 @@ import 'package:meta/meta.dart';
import 'assertions.dart';
import 'basic_types.dart';
import 'licenses.dart';
/// Signature for service extensions.
///
......@@ -75,14 +74,9 @@ abstract class BindingBase {
/// `initInstances()`.
void initInstances() {
assert(!_debugInitialized);
LicenseRegistry.addLicense(_addLicenses);
assert(() { _debugInitialized = true; return true; });
}
Iterable<LicenseEntry> _addLicenses() sync* {
// TODO(ianh): Populate the license registry.
}
/// Called when the binding is initialized, to register service
/// extensions.
///
......
......@@ -2,8 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
/// Signature for callbacks passed to [LicenseRegistry.addLicense].
typedef Iterable<LicenseEntry> LicenseEntryCollector();
typedef Stream<LicenseEntry> LicenseEntryCollector();
/// A string that represents one paragraph in a [LicenseEntry].
///
......@@ -230,8 +232,14 @@ class LicenseEntryWithLineBreaks extends LicenseEntry {
/// A registry for packages to add licenses to, so that they can be displayed
/// together in an interface such as the [LicensePage].
///
/// Packages should register their licenses using [addLicense]. User interfaces
/// Packages can register their licenses using [addLicense]. User interfaces
/// that wish to show all the licenses can obtain them by calling [licenses].
///
/// The flutter tool will automatically collect the contents of all the LICENSE
/// files found at the root of each package into a single LICENSE file in the
/// default asset bundle. Each license in that file is separated from the next
/// by a line of eighty hyphens (`-`). The `services` package registers a
/// license collector that splits that file and adds each entry to the registry.
class LicenseRegistry {
LicenseRegistry._();
......@@ -251,11 +259,9 @@ class LicenseRegistry {
/// Returns the licenses that have been registered.
///
/// Each time the iterable returned by this function is called, the callbacks
/// registered with [addLicense] are called again, which is relatively
/// expensive. For this reason, consider immediately converting the results to
/// a list with [Iterable.toList].
static Iterable<LicenseEntry> get licenses sync* {
/// Because generating the list of licenses is expensive, this function should
/// only be called once.
static Stream<LicenseEntry> get licenses async* {
if (_collectors == null)
return;
for (LicenseEntryCollector collector in _collectors)
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
......@@ -13,6 +15,7 @@ import 'drawer_item.dart';
import 'flat_button.dart';
import 'icon.dart';
import 'page.dart';
import 'progress_indicator.dart';
import 'scaffold.dart';
import 'theme.dart';
......@@ -340,40 +343,51 @@ class LicensePage extends StatefulWidget {
}
class _LicensePageState extends State<LicensePage> {
List<Widget> _licenses = _initLicenses();
static List<Widget> _initLicenses() {
List<Widget> result = <Widget>[];
for (LicenseEntry license in LicenseRegistry.licenses) {
@override
void initState() {
super.initState();
_initLicenses();
}
List<Widget> _licenses = <Widget>[];
bool _loaded = false;
Future<Null> _initLicenses() async {
await for (LicenseEntry license in LicenseRegistry.licenses) {
bool haveMargin = true;
result.add(new Padding(
padding: new EdgeInsets.symmetric(vertical: 18.0),
child: new Text(
'🍀‬', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere.
textAlign: TextAlign.center
)
));
for (LicenseParagraph paragraph in license.paragraphs) {
if (paragraph.indent == LicenseParagraph.centeredIndent) {
result.add(new Padding(
padding: new EdgeInsets.only(top: haveMargin ? 0.0 : 16.0),
child: new Text(
paragraph.text,
style: new TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center
)
));
} else {
assert(paragraph.indent >= 0);
result.add(new Padding(
padding: new EdgeInsets.only(top: haveMargin ? 0.0 : 8.0, left: 16.0 * paragraph.indent),
child: new Text(paragraph.text)
));
setState(() {
_licenses.add(new Padding(
padding: new EdgeInsets.symmetric(vertical: 18.0),
child: new Text(
'🍀‬', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere.
textAlign: TextAlign.center
)
));
for (LicenseParagraph paragraph in license.paragraphs) {
if (paragraph.indent == LicenseParagraph.centeredIndent) {
_licenses.add(new Padding(
padding: new EdgeInsets.only(top: haveMargin ? 0.0 : 16.0),
child: new Text(
paragraph.text,
style: new TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center
)
));
} else {
assert(paragraph.indent >= 0);
_licenses.add(new Padding(
padding: new EdgeInsets.only(top: haveMargin ? 0.0 : 8.0, left: 16.0 * paragraph.indent),
child: new Text(paragraph.text)
));
}
haveMargin = false;
}
haveMargin = false;
}
});
}
return result;
setState(() {
_loaded = true;
});
}
@override
......@@ -390,6 +404,14 @@ class _LicensePageState extends State<LicensePage> {
new Container(height: 24.0),
];
contents.addAll(_licenses);
if (!_loaded) {
contents.add(new Padding(
padding: new EdgeInsets.symmetric(vertical: 24.0),
child: new Center(
child: new CircularProgressIndicator()
)
));
}
return new Scaffold(
appBar: new AppBar(
title: new Text('Licenses')
......
......@@ -2,8 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'asset_bundle.dart';
import 'shell.dart';
/// Ensures that the [MojoShell] singleton is created synchronously
......@@ -15,10 +18,24 @@ import 'shell.dart';
/// [MojoShell] is then created in an earlier call stack than the
/// server for that service is provided, then the request will be
/// rejected as not matching any registered servers.
///
/// The ServicesBinding also registers a [LicenseEntryCollector] that exposes
/// the licenses found in the LICENSE file stored at the root of the asset
/// bundle.
abstract class ServicesBinding extends BindingBase {
@override
void initInstances() {
super.initInstances();
new MojoShell();
LicenseRegistry.addLicense(_addLicenses);
}
static final String _licenseSeparator = '\n' + ('-' * 80) + '\n';
Stream<LicenseEntry> _addLicenses() async* {
final String rawLicenses = await rootBundle.loadString('LICENSE', cache: false);
final List<String> licenses = rawLicenses.split(_licenseSeparator);
for (String license in licenses)
yield new LicenseEntryWithLineBreaks(license);
}
}
......@@ -172,22 +172,18 @@ S
expect(paragraphs, hasLength(1));
});
test('LicenseRegistry', () {
expect(LicenseRegistry.licenses, isEmpty);
LicenseRegistry.addLicense(() {
return <LicenseEntry>[
new LicenseEntryWithLineBreaks('A'),
new LicenseEntryWithLineBreaks('B'),
];
test('LicenseRegistry', () async {
expect(await LicenseRegistry.licenses.toList(), isEmpty);
LicenseRegistry.addLicense(() async* {
yield new LicenseEntryWithLineBreaks('A');
yield new LicenseEntryWithLineBreaks('B');
});
LicenseRegistry.addLicense(() {
return <LicenseEntry>[
new LicenseEntryWithLineBreaks('C'),
new LicenseEntryWithLineBreaks('D'),
];
LicenseRegistry.addLicense(() async* {
yield new LicenseEntryWithLineBreaks('C');
yield new LicenseEntryWithLineBreaks('D');
});
expect(LicenseRegistry.licenses, hasLength(4));
List<LicenseEntry> licenses = LicenseRegistry.licenses.toList();
expect(await LicenseRegistry.licenses.toList(), hasLength(4));
List<LicenseEntry> licenses = await LicenseRegistry.licenses.toList();
expect(licenses, hasLength(4));
expect(licenses[0].paragraphs.single.text, 'A');
expect(licenses[1].paragraphs.single.text, 'B');
......
......@@ -28,8 +28,7 @@ class PackageMap {
final String packagesPath;
Map<String, Uri> get map {
if (_map == null)
_map = _parse(packagesPath);
_map ??= _parse(packagesPath);
return _map;
}
Map<String, Uri> _map;
......
......@@ -66,7 +66,7 @@ class _Asset {
/// The entry to list in the generated asset manifest.
String get assetEntry => _assetEntry ?? relativePath;
/// Where the resource is on disk realtive to [base].
/// Where the resource is on disk relative to [base].
final String relativePath;
final String source;
......@@ -200,6 +200,34 @@ Map<_Asset, List<_Asset>> _parseAssets(
return result;
}
final String _licenseSeparator = '\n' + ('-' * 80) + '\n';
/// Returns a ZipEntry representing the license file.
ZipEntry _obtainLicenses(
PackageMap packageMap,
String assetBase
) {
// 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).
final Set<String> packageLicenses = new Set<String>();
final List<Uri> packages = packageMap.map.values;
for (Uri package in packages) {
if (package != null && package.scheme == 'file') {
final File file = new File.fromUri(package.resolve('../LICENSE'));
if (file.existsSync())
packageLicenses.addAll(file.readAsStringSync().split(_licenseSeparator));
}
}
final List<String> combinedLicensesList = packageLicenses.toList();
combinedLicensesList.sort();
final String combinedLicenses = combinedLicensesList.join(_licenseSeparator);
return new ZipEntry.fromString('LICENSE', combinedLicenses);
}
_Asset _resolveAsset(
PackageMap packageMap,
String assetBase,
......@@ -362,13 +390,13 @@ Future<int> build({
}
return assemble(
manifestDescriptor: manifestDescriptor,
snapshotFile: snapshotFile,
assetBasePath: assetBasePath,
outputPath: outputPath,
privateKeyPath: privateKeyPath,
workingDirPath: workingDirPath,
includeRobotoFonts: includeRobotoFonts
manifestDescriptor: manifestDescriptor,
snapshotFile: snapshotFile,
assetBasePath: assetBasePath,
outputPath: outputPath,
privateKeyPath: privateKeyPath,
workingDirPath: workingDirPath,
includeRobotoFonts: includeRobotoFonts
);
}
......@@ -383,8 +411,10 @@ Future<int> assemble({
}) async {
printTrace('Building $outputPath');
final PackageMap packageMap = new PackageMap(path.join(assetBasePath, '.packages'));
Map<_Asset, List<_Asset>> assetVariants = _parseAssets(
new PackageMap(path.join(assetBasePath, '.packages')),
packageMap,
manifestDescriptor,
assetBasePath,
excludeDirs: <String>[workingDirPath, path.join(assetBasePath, 'build')]
......@@ -434,6 +464,8 @@ Future<int> assemble({
if (fontManifest != null)
zipBuilder.addEntry(fontManifest);
zipBuilder.addEntry(_obtainLicenses(packageMap, assetBasePath));
ensureDirectoryExists(outputPath);
printTrace('Encoding zip file to $outputPath');
......
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