blob: 08d2798d4e3b6fef589eba6bc5edaeedb12d8a52 [file]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// This library generates Mojo bindings for a Dart package.
library generate;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'utils.dart';
class _GenerateIterData extends SubDirIterData {
final Directory _packageRoot;
_GenerateIterData(this._packageRoot) : super(null);
Directory get packageRoot => _packageRoot;
}
class MojomGenerator {
Map<String, String> _duplicateDetection;
bool _errorOnDuplicate;
bool _verbose;
bool _dryRun;
bool _profile;
Directory _mojoSdk;
int _generationMs;
MojomGenerator(this._mojoSdk,
{bool errorOnDuplicate: true,
bool verbose: false,
bool profile: false,
bool dryRun: false})
: _errorOnDuplicate = errorOnDuplicate,
_verbose = verbose,
_dryRun = dryRun,
_profile = profile,
_generationMs = 0,
_duplicateDetection = new Map<String, String>();
/// Generate bindings for [pacakge]. Fail if there is no .mojoms file, or if
/// the .mojoms file refers to .mojom files lacking a DartPackage annotation
/// for [pacakge]
generateForPackage(Directory package) async {
Directory temp = await package.createTemp();
Directory packageRoot = new Directory(path.join(package.path, 'packages'));
File mojomsFile = new File(path.join(package.path, '.mojoms'));
if (!(await mojomsFile.exists())) {
await temp.delete(recursive: true);
throw new FetchError(
"--single-package specified but no .mojoms file found: $mojomsFile");
}
DateTime dotMojomsTime = (await mojomsFile.stat()).modified;
DateTime mojomTime =
await _fetchFromDotMojoms(package.uri, temp, mojomsFile);
DateTime mojomDartTime = await _findOldestMojomDart(package);
if (_shouldRegenerate(dotMojomsTime, mojomTime, mojomDartTime)) {
await for (var dir in temp.list()) {
if (dir is! Directory) continue;
var mojomDir = new Directory(path.join(dir.path, 'mojom'));
if (_verbose) print("pathSegments = ${package.uri.pathSegments}");
await _generateForMojomDir(mojomDir, packageRoot,
packageName: package.uri.pathSegments.lastWhere((s) => s != ""));
}
}
await temp.delete(recursive: true);
}
generateForAllPackages(Directory packageRoot, Directory mojomPackage,
List<Directory> additionalDirs,
{bool fetch: false, bool generate: false}) async {
// Fetch .mojom files. These will be picked up by the generation step
// below.
if (fetch) {
await subDirIter(packageRoot, null, _fetchAction);
}
// Generate mojom files.
if (generate) {
await _generateDirIter(packageRoot, new _GenerateIterData(packageRoot));
}
// TODO(zra): As mentioned above, this should go away.
// Copy pregenerated files from specified external directories into the
// mojom package.
final data = new _GenerateIterData(packageRoot);
data.subdir = mojomPackage;
for (var mojomDir in additionalDirs) {
await _copyBindingsAction(data, mojomDir);
if (generate) {
await _generateAction(data, mojomDir);
}
}
}
void printProfile() {
print("Generation time: $_generationMs ms");
}
/// Under [package]/lib, returns the oldest modification time for a
/// .mojom.dart file.
_findOldestMojomDart(Directory package) async {
DateTime oldestModTime;
Directory libDir = new Directory(path.join(package.path, 'lib'));
if (!await libDir.exists()) return null;
await for (var file in libDir.list(recursive: true)) {
if (file is! File) continue;
if (!isMojomDart(file.path)) continue;
DateTime modTime = (await file.stat()).modified;
if ((oldestModTime == null) || oldestModTime.isAfter(modTime)) {
oldestModTime = modTime;
}
}
return oldestModTime;
}
/// If the .mojoms file or the newest .mojom is newer than the oldest
/// .mojom.dart, then regenerate everything.
bool _shouldRegenerate(
DateTime dotMojomsTime, DateTime mojomTime, DateTime mojomDartTime) {
return (dotMojomsTime == null) ||
(mojomTime == null) ||
(mojomDartTime == null) ||
dotMojomsTime.isAfter(mojomDartTime) ||
mojomTime.isAfter(mojomDartTime);
}
/// Given a .mojoms file in |mojomsFile|, fetch the listed .mojom files and
/// store them in a directory tree rooted at |destination|. Relative file URIs
/// are resolved relative to |base|. The "mojom" directories populated under
/// |destination| are suitable for passing to |generateForMojomDir| above.
///
/// The .mojoms file should be formatted as follows:
/// '''
/// root: https://www.example.com/mojoms
/// path/to/some/mojom1.mojom
/// path/to/some/other/mojom2.mojom
///
/// root: https://www.example-two.com/mojoms
/// path/to/example/two/mojom1.mojom
/// ...
///
/// root: file:///absolute/path/to/mojoms
/// ...
///
/// root /some/absolute/path
/// ...
///
/// root: ../../path/relative/to/|base|/mojoms
/// ...
///
/// Lines beginning with '#' are ignored.
///
/// Returns the modification time of the newest .mojom found.
_fetchFromDotMojoms(Uri base, Directory destination, File mojomsFile) async {
DateTime newestModTime;
Directory mojomsDir;
var httpClient = new HttpClient();
int repoCount = 0;
int mojomCount = 0;
Uri repoRoot;
for (String line in await mojomsFile.readAsLines()) {
line = line.trim();
if (line.isEmpty || line.startsWith('#')) continue;
if (line.startsWith('root:')) {
if ((mojomsDir != null) && (mojomCount == 0)) {
throw new FetchError("root with no mojoms: $repoRoot");
}
mojomCount = 0;
var rootWords = line.split(" ");
if (rootWords.length != 2) {
throw new FetchError("Malformed root: $line");
}
repoRoot = Uri.parse(rootWords[1]);
String scheme = repoRoot.scheme;
if (!scheme.startsWith("http")) {
// If not an absolute path. resolve relative to packageUri.
if (!repoRoot.isAbsolute) {
repoRoot = base.resolveUri(repoRoot);
}
}
if (_verbose) print("Found repo root: $repoRoot");
mojomsDir = new Directory(
path.join(destination.path, 'mojm.repo.$repoCount', 'mojom'));
await mojomsDir.create(recursive: true);
repoCount++;
} else {
if (mojomsDir == null) {
throw new FetchError('Malformed .mojoms file: $mojomsFile');
}
Uri uri = repoRoot.resolve(line);
DateTime modTime = await getModificationTime(uri);
if (_verbose) print("Fetching $uri");
String fileString = await fetchUri(httpClient, uri);
String filePath = path.join(mojomsDir.path, line);
var file = new File(filePath);
if (!await file.exists()) {
await file.create(recursive: true);
await file.writeAsString(fileString);
if (_verbose) print("Wrote $filePath");
}
if ((newestModTime == null) || newestModTime.isBefore(modTime)) {
newestModTime = modTime;
}
mojomCount++;
}
}
return newestModTime;
}
/// Generate bindings for .mojom files found in [source].
/// Bindings will be generated into [destination]/$package/... where
/// $package is the package specified by the bindings generation process.
/// If [packageName] is given, an exception is thrown if
/// $package != [packageName]
_generateForMojomDir(Directory source, Directory destination,
{String packageName}) async {
await for (var mojom in source.list(recursive: true)) {
if (mojom is! File) continue;
if (!isMojom(mojom.path)) continue;
if (_verbose) print("Found $mojom");
final script = path.join(
_mojoSdk.path, 'tools', 'bindings', 'mojom_bindings_generator.py');
final sdkInc = path.normalize(path.join(_mojoSdk.path, '..', '..'));
final outputDir = await destination.createTemp();
final output = outputDir.path;
final arguments = [
'--use_bundled_pylibs',
'-g',
'dart',
'-o',
output,
// TODO(zra): Are other include paths needed?
'-I',
sdkInc,
'-I',
source.path,
mojom.path
];
if (_verbose || _dryRun) {
print('Generating $mojom');
print('$script ${arguments.join(" ")}');
}
if (!_dryRun) {
var stopwatch;
if (_profile) stopwatch = new Stopwatch()..start();
final result = await Process.run(script, arguments);
if (_profile) {
stopwatch.stop();
_generationMs += stopwatch.elapsedMilliseconds;
}
if (result.exitCode != 0) {
await outputDir.delete(recursive: true);
throw new GenerationError("$script failed:\n${result.stderr}");
}
// Generated .mojom.dart is under $output/dart-pkg/$PACKAGE/lib/$X
// Move $X to |destination|/$PACKAGE/$X
final generatedDirName = path.join(output, 'dart-pkg');
final generatedDir = new Directory(generatedDirName);
await for (var genpack in generatedDir.list()) {
if (genpack is! Directory) continue;
var libDir = new Directory(path.join(genpack.path, 'lib'));
var name = path.relative(genpack.path, from: generatedDirName);
if ((packageName != null) && packageName != name) {
await outputDir.delete(recursive: true);
throw new GenerationError(
"Tried to generate for package $name in package $packageName");
}
var copyDest = new Directory(path.join(destination.path, name));
var copyData = new SubDirIterData(copyDest);
if (_verbose) print("Copy $libDir to $copyDest");
await _copyBindingsAction(copyData, libDir);
}
await outputDir.delete(recursive: true);
}
}
}
/// Searches for .mojom.dart files under [sourceDir] and copies them to
/// [data.subdir].
_copyBindingsAction(SubDirIterData data, Directory sourceDir) async {
await for (var mojom in sourceDir.list(recursive: true)) {
if (mojom is! File) continue;
if (!isMojomDart(mojom.path)) continue;
if (_verbose) print("Found $mojom");
final relative = path.relative(mojom.path, from: sourceDir.path);
final dest = path.join(data.subdir.path, relative);
final destDirectory = new Directory(path.dirname(dest));
if (_errorOnDuplicate && _duplicateDetection.containsKey(dest)) {
String original = _duplicateDetection[dest];
throw new GenerationError(
'Conflict: Both ${original} and ${mojom.path} supply ${dest}');
}
_duplicateDetection[dest] = mojom.path;
if (_verbose || _dryRun) {
print('Copying $mojom to $dest');
}
if (!_dryRun) {
final File source = new File(mojom.path);
final File destFile = new File(dest);
if (await destFile.exists()) {
await destFile.delete();
}
if (_verbose) print("Ensuring $destDirectory exists");
await destDirectory.create(recursive: true);
await source.copy(dest);
await markFileReadOnly(dest);
}
}
}
/// Searches for .mojom files under [mojomDirectory], generates .mojom.dart
/// files for them, and copies them to the 'mojom' package.
_generateAction(_GenerateIterData data, Directory mojomDirectory) async {
if (_verbose) print(
"generateAction: $mojomDirectory, packageRoot: ${data.packageRoot}");
await _generateForMojomDir(mojomDirectory, data.packageRoot);
}
/// Iterates recurisvley over all subdirectories called "mojom" under
/// |packages| and applies bindings generation to them, placing the results
/// in the right place under |packages|.
_generateDirIter(Directory packages, _GenerateIterData data) async {
await subDirIter(packages, data, (d, s) async {
await subDirIter(s, d, _generateAction,
filter: (d) => path.basename(d.path) == "mojom");
});
}
/// Fetch mojoms for the package in |packageDirectory| if it has a .mojoms
/// file.
_fetchAction(SubDirIterData _, Directory packageDirectory) async {
var packageUri = packageDirectory.uri;
// TODO(zra): Also look in realpath(packageDirectory).parent for .mojoms
// file.
var mojomsPath = path.join(packageDirectory.path, '.mojoms');
var mojomsFile = new File(mojomsPath);
if (!await mojomsFile.exists()) return;
if (_verbose) print("Found .mojoms file: $mojomsPath");
await _fetchFromDotMojoms(packageUri, packageDirectory.parent, mojomsFile);
}
}
/// Given the location of the Mojo SDK and a root directory from which to begin
/// a search. Find .mojoms files, and generate bindings for the containing
/// packages.
class TreeGenerator {
MojomGenerator _generator;
Directory _mojoSdk;
Directory _rootDir;
List<String> _skip;
bool _verbose;
bool _profile;
bool _dryRun;
int errors;
TreeGenerator(Directory mojoSdk, Directory rootDir, List<String> skip,
{bool verbose: false, bool profile: false, bool dryRun: false}) {
_mojoSdk = mojoSdk;
_rootDir = rootDir;
_skip = skip;
_verbose = verbose;
_profile = profile;
_dryRun = dryRun;
_generator = new MojomGenerator(_mojoSdk,
verbose: _verbose, profile: _profile, dryRun: _dryRun);
errors = 0;
}
findAndGenerate() async {
Set<String> alreadySeen = new Set<String>();
await for (var entry in _rootDir.list(recursive: true)) {
if (entry is! File) continue;
if (!isDotMojoms(entry.path)) continue;
if (alreadySeen.contains(entry.path)) continue;
if (_shouldSkip(entry)) continue;
alreadySeen.add(entry.path);
await _runGenerate(entry.parent);
}
if (_profile) {
_generator.printProfile();
}
}
bool _shouldSkip(File f) {
if (_skip == null) return false;
var match =
_skip.firstWhere((p) => f.path.startsWith(p), orElse: () => null);
return match != null;
}
_runGenerate(Directory package) async {
try {
if (_verbose) print('Generating bindings for $package');
await _generator.generateForPackage(package);
if (_verbose) print('Done generating bindings for $package');
} on GenerationError catch (e) {
stderr.writeln('Bindings generation failed for package $package: $e');
errors += 1;
}
}
}
/// Given the root of a directory tree to check, and the root of a directory
/// tree containing the canonical generated bindings, checks that the files
/// match, and recommends actions to take in case they don't. The canonical
/// directory should contain a subdirectory for each package that might be
/// encountered while traversing the directory tree being checked.
class TreeChecker {
bool _verbose;
bool _profile;
Directory _mojoSdk;
Directory _root;
Directory _canonical;
List<String> _skip;
int _errors;
TreeChecker(this._mojoSdk, this._root, this._canonical, this._skip,
{bool verbose: false, bool profile: false})
: _verbose = verbose,
_profile = profile,
_errors = 0;
check() async {
Set<String> alreadySeen = new Set<String>();
await for (var entry in _root.list(recursive: true)) {
if (entry is! File) continue;
if (!isDotMojoms(entry.path)) continue;
if (entry.path.startsWith(_canonical.path)) continue;
String realpath = await entry.resolveSymbolicLinks();
if (alreadySeen.contains(realpath)) continue;
alreadySeen.add(realpath);
if (_shouldSkip(entry)) continue;
var parent = entry.parent;
if (_verbose) print("Checking package at: ${parent.path}");
await _checkAll(parent);
await _checkSame(parent);
}
if (_errors > 1) {
String dart = makeRelative(Platform.executable);
String scriptPath = makeRelative(path.fromUri(Platform.script));
String mojoSdk = makeRelative(_mojoSdk.path);
String root = makeRelative(_root.path);
stderr.writeln('It looks like there were multiple problems. '
'You can run the following command to regenerate bindings for your '
'whole tree:\n'
'\t$dart $scriptPath tree -r $root -m $mojoSdk');
}
}
int get errors => _errors;
// Check that the files are the same.
_checkSame(Directory package) async {
Directory libDir = new Directory(path.join(package.path, 'lib'));
Set<String> alreadySeen = new Set<String>();
await for (var entry in libDir.list(recursive: true)) {
if (entry is! File) continue;
if (!isMojomDart(entry.path)) continue;
String realpath = await entry.resolveSymbolicLinks();
if (alreadySeen.contains(realpath)) continue;
alreadySeen.add(realpath);
String relPath = path.relative(entry.path, from: package.parent.path);
File canonicalFile = new File(path.join(_canonical.path, relPath));
if (!await canonicalFile.exists()) {
if (_verbose) print("No canonical file for $entry");
continue;
}
if (_verbose) print("Comparing $entry with $canonicalFile");
int fileComparison = await compareFiles(entry, canonicalFile);
if (fileComparison != 0) {
String entryPath = makeRelative(entry.path);
String canonicalPath = makeRelative(canonicalFile.path);
if (fileComparison > 0) {
stderr.writeln('The generated file:\n\t$entryPath\n'
'is newer thanthe canonical file\n\t$canonicalPath\n,'
'and they are different. Regenerate canonical files?');
} else {
String dart = makeRelative(Platform.executable);
String packagePath = makeRelative(package.path);
String scriptPath = makeRelative(path.fromUri(Platform.script));
String mojoSdk = makeRelative(_mojoSdk.path);
stderr.writeln('For the package: $packagePath\n'
'The generated file:\n\t$entryPath\n'
'is older than the canonical file:\n\t$canonicalPath\n'
'and they are different. Regenerate by running:\n'
'\t$dart $scriptPath single -p $packagePath -m $mojoSdk');
}
_errors++;
return;
}
}
}
// Check that every .mojom.dart in the canonical package is also in the
// package we are checking.
_checkAll(Directory package) async {
String packageName = path.relative(package.path, from: package.parent.path);
String canonicalPackagePath =
path.join(_canonical.path, packageName, 'lib');
Directory canonicalPackage = new Directory(canonicalPackagePath);
if (!await canonicalPackage.exists()) return;
Set<String> alreadySeen = new Set<String>();
await for (var entry in canonicalPackage.list(recursive: true)) {
if (entry is! File) continue;
if (!isMojomDart(entry.path)) continue;
String realpath = await entry.resolveSymbolicLinks();
if (alreadySeen.contains(realpath)) continue;
alreadySeen.add(realpath);
String relPath = path.relative(entry.path, from: canonicalPackage.path);
File genFile = new File(path.join(package.path, 'lib', relPath));
if (_verbose) print("Checking that $genFile exists");
if (!await genFile.exists()) {
String dart = makeRelative(Platform.executable);
String genFilePath = makeRelative(genFile.path);
String packagePath = makeRelative(package.path);
String scriptPath = makeRelative(path.fromUri(Platform.script));
String mojoSdk = makeRelative(_mojoSdk.path);
stderr.writeln('The generated file:\n\t$genFilePath\n'
'is needed but does not exist. Make sure that the .mojom to '
'generate it is listed in the .mojoms file for package '
'$packageName, and run the command\n'
'\t$dart $scriptPath single -p $packagePath -m $mojoSdk');
_errors++;
}
}
}
bool _shouldSkip(File f) {
if (_skip == null) return false;
var match =
_skip.firstWhere((p) => f.path.startsWith(p), orElse: () => null);
return match != null;
}
}