blob: e9c73d4964b80fd29905f7b7d5f61861ea993bda [file] [log] [blame]
// 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:developer' as dev;
import 'dart:io';
import 'package:mojom/src/utils.dart';
import 'package:path/path.dart' as path;
part 'mojom_finder.dart';
class MojomGenerator {
static dev.Counter _genMs;
final bool _errorOnDuplicate;
final bool _dryRun;
final bool _force;
final Directory _mojoSdk;
Map<String, String> _duplicateDetection;
int _generationMs;
MojomGenerator(this._mojoSdk,
{bool errorOnDuplicate: true, bool profile: false, bool dryRun: false,
bool force: false})
: _errorOnDuplicate = errorOnDuplicate,
_dryRun = dryRun,
_force = force,
_generationMs = 0,
_duplicateDetection = new Map<String, String>() {
if (_genMs == null) {
_genMs = new dev.Counter("mojom generation",
"Time spent waiting for bindings generation script in ms.");
dev.Metrics.register(_genMs);
}
}
generate(PackageInfo info) async {
// Count the .mojom files, and find the modification time of the most
// recently modified one.
int mojomCount = info.mojomFiles.length;
DateTime newestMojomTime;
for (File mojom in info.mojomFiles) {
DateTime mojomTime = await getModificationTime(mojom);
if ((newestMojomTime == null) || newestMojomTime.isBefore(mojomTime)) {
newestMojomTime = mojomTime;
}
}
// Count the .mojom.dart files, and find the modification time of the
// least recently modified one.
List mojomDartInfo = await _findOldestMojomDart(info.packageDir);
DateTime oldestMojomDartTime = null;
int mojomDartCount = 0;
if (mojomDartInfo != null) {
oldestMojomDartTime = mojomDartInfo[0];
mojomDartCount = mojomDartInfo[1];
}
// If we don't have enough .mojom.dart files, or if a .mojom file is
// newer than the oldest .mojom.dart file, then regenerate.
if (_force || (mojomDartCount < mojomCount) ||
_shouldRegenerate(newestMojomTime, oldestMojomDartTime)) {
for (File mojom in info.mojomFiles) {
await _generateForMojom(
mojom, info.importDir, info.packageDir, info.name);
}
// Delete any .mojom.dart files that are still older than mojomTime.
await _deleteOldMojomDart(info.packageDir, newestMojomTime);
}
}
/// Under [package]/lib, returns the oldest modification time for a
/// .mojom.dart file.
_findOldestMojomDart(Directory package) async {
int mojomDartCount = 0;
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, followLinks: false)) {
if (file is! File) continue;
if (!isMojomDart(file.path)) continue;
DateTime modTime = (await file.stat()).modified;
if ((oldestModTime == null) || oldestModTime.isAfter(modTime)) {
oldestModTime = modTime;
}
mojomDartCount++;
}
return [oldestModTime, mojomDartCount];
}
// Delete .mojom.dart files under [package] that are [olderThanThis].
_deleteOldMojomDart(Directory package, DateTime olderThanThis) async {
Directory libDir = new Directory(path.join(package.path, 'lib'));
if (!await libDir.exists()) {
return;
}
await for (var file in libDir.list(recursive: true, followLinks: false)) {
if (file is! File) continue;
if (!isMojomDart(file.path)) continue;
DateTime modTime = (await file.stat()).modified;
if (modTime.isBefore(olderThanThis)) {
log.warning("Deleting stale .mojom.dart: $file");
await file.delete();
}
}
}
/// If the .mojoms file or the newest .mojom is newer than the oldest
/// .mojom.dart, then regenerate everything.
bool _shouldRegenerate(DateTime mojomTime, DateTime mojomDartTime) {
return (mojomTime == null) ||
(mojomDartTime == null) ||
mojomTime.isAfter(mojomDartTime);
}
_runBindingsGeneration(String script, List<String> arguments) async {
var result;
var stopwatch = new Stopwatch()..start();
result = await Process.run(script, arguments);
stopwatch.stop();
_genMs.value += stopwatch.elapsedMilliseconds;
return result;
}
// This is a hack until we can express import paths in .mojom files.
// This checks the mojom path for '/mojo/services/' and if found assumes
// this mojom needs //mojo/services as an import path when generating
// bindings.
String _sniffForMojoServicesInclude(String mojomPath) {
List<String> pathComponents = path.split(mojomPath);
while (pathComponents.length > 2) {
int last = pathComponents.length;
if ((pathComponents[last - 1] == 'services') &&
(pathComponents[last - 2] == 'mojo')) {
return path.joinAll(pathComponents);
}
// Remove the last element and try again.
pathComponents.removeLast();
}
return null;
}
_generateForMojom(File mojom, Directory importDir, Directory destination,
String packageName) async {
if (!isMojom(mojom.path)) return;
log.info("_generateForMojom($mojom)");
final script = path.join(
_mojoSdk.path, 'tools', 'bindings', 'mojom_bindings_generator.py');
final sdkInc = path.normalize(path.join(_mojoSdk.path, '..', '..'));
final outputDir = await Directory.systemTemp.createTemp();
try {
final output = outputDir.path;
final servicesPath = _sniffForMojoServicesInclude(mojom.path);
final arguments = [
'--use_bundled_pylibs',
'-g',
'dart',
'-o',
output,
'-I',
sdkInc,
'-I',
importDir.path,
'--no-gen-imports'
];
if (servicesPath != null) {
arguments.add('-I');
arguments.add(servicesPath);
}
arguments.add(mojom.path);
log.info('Generating $mojom');
log.info('$script ${arguments.join(" ")}');
log.info('dryRun = $_dryRun');
if (!_dryRun) {
final result = await _runBindingsGeneration(script, arguments);
if (result.exitCode != 0) {
log.info("bindings generation result = ${result.exitCode}");
await outputDir.delete(recursive: true);
throw new GenerationError("$script failed:\n"
"code: ${result.exitCode}\n"
"stderr: ${result.stderr}\n"
"stdout: ${result.stdout}");
} else {
log.info("bindings generation result = 0");
}
// Generated .mojom.dart is under $output/dart-gen/$PACKAGE/lib/$X
// Move $X to |destination|/lib/$X.
// Throw an exception if $PACKGE != [packageName].
final generatedDirName = path.join(output, 'dart-gen');
final generatedDir = new Directory(generatedDirName);
log.info("generatedDir= $generatedDir");
assert(await generatedDir.exists());
await for (var genpack in generatedDir.list()) {
if (genpack is! Directory) continue;
log.info("genpack = $genpack");
var libDir = new Directory(path.join(genpack.path, 'lib'));
var name = path.relative(genpack.path, from: generatedDirName);
log.info("Found generated lib dir: $libDir");
if (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, 'lib'));
log.info("Copy $libDir to $copyDest");
await _copyBindings(copyDest, libDir);
}
}
} finally {
await outputDir.delete(recursive: true);
}
}
/// Searches for .mojom.dart files under [sourceDir] and copies them to
/// [destDir].
_copyBindings(Directory destDir, Directory sourceDir) async {
var sourceList = sourceDir.list(recursive: true, followLinks: false);
await for (var mojom in sourceList) {
if (mojom is! File) continue;
if (!isMojomDart(mojom.path)) continue;
log.info("Found $mojom");
final relative = path.relative(mojom.path, from: sourceDir.path);
final dest = path.join(destDir.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;
log.info('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();
}
log.info("Ensuring $destDirectory exists");
await destDirectory.create(recursive: true);
await source.copy(dest);
await markFileReadOnly(dest);
}
}
}
}
/// Given the location of the Mojo SDK and a root directory from which to begin
/// a search. Find .mojom files, and generate bindings for the relevant
/// packages.
class TreeGenerator {
final MojomGenerator _generator;
final Directory _mojoSdk;
final Directory _mojomRootDir;
final Directory _dartRootDir;
final List<String> _skip;
final bool _dryRun;
Set<String> _processedPackages;
int errors;
TreeGenerator(
Directory mojoSdk, this._mojomRootDir, this._dartRootDir, this._skip,
{bool dryRun: false, bool force: false})
: _mojoSdk = mojoSdk,
_dryRun = dryRun,
_generator = new MojomGenerator(mojoSdk, dryRun: dryRun, force: force),
_processedPackages = new Set<String>(),
errors = 0;
generate() async {
var mojomFinder = new MojomFinder(_mojomRootDir, _dartRootDir, _skip);
List<PackageInfo> packageInfos = await mojomFinder.find();
for (PackageInfo info in packageInfos) {
await _runGenerate(info);
}
}
_runGenerate(PackageInfo info) async {
try {
log.info('Generating bindings for ${info.name}');
await _generator.generate(info);
log.info('Done generating bindings for ${info.name}');
} on GenerationError catch (e) {
log.severe('Bindings generation failed for package ${info.name}: $e');
errors += 1;
}
}
bool _shouldSkip(File f) => containsPrefix(f.path, _skip);
}
/// 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 {
final Directory _mojoSdk;
final Directory _mojomRootDir;
final Directory _dartRootDir;
final Directory _canonical;
final List<String> _skip;
int _errors;
TreeChecker(this._mojoSdk, this._mojomRootDir, this._dartRootDir,
this._canonical, this._skip)
: _errors = 0;
check() async {
// Generate missing .mojoms files if needed.
var mojomFinder = new MojomFinder(_mojomRootDir, _dartRootDir, _skip);
List<PackageInfo> packageInfos = await mojomFinder.find();
for (PackageInfo info in packageInfos) {
log.info("Checking package at ${info.packageDir}");
await _checkAll(info.packageDir);
await _checkSame(info.packageDir);
}
// If there were multiple mismatches, explain how to regenerate the bindings
// for the whole tree.
if (_errors > 1) {
String dart = makeRelative(Platform.executable);
String packRoot = (Platform.packageRoot == "")
? ""
: "-p " + makeRelative(Platform.packageRoot);
String scriptPath = makeRelative(path.fromUri(Platform.script));
String mojoSdk = makeRelative(_mojoSdk.path);
String root = makeRelative(_mojomRootDir.path);
String dartRoot = makeRelative(_dartRootDir.path);
String skips = _skip.map((s) => "-s " + makeRelative(s)).join(" ");
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 $packRoot $scriptPath gen -m $mojoSdk -r $root -o $dartRoot '
'$skips');
}
}
int get errors => _errors;
// Check that the files are the same.
_checkSame(Directory package) async {
Directory libDir = new Directory(path.join(package.path, 'lib'));
await for (var entry in libDir.list(recursive: true, followLinks: false)) {
if (entry is! File) continue;
if (!isMojomDart(entry.path)) continue;
String relPath = path.relative(entry.path, from: package.parent.path);
File canonicalFile = new File(path.join(_canonical.path, relPath));
if (!await canonicalFile.exists()) {
log.info("No canonical file for $entry");
continue;
}
log.info("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 packRoot = (Platform.packageRoot == "")
? ""
: "-p " + makeRelative(Platform.packageRoot);
String root = makeRelative(_mojomRootDir.path);
String packagePath = makeRelative(package.path);
String scriptPath = makeRelative(path.fromUri(Platform.script));
String mojoSdk = makeRelative(_mojoSdk.path);
String skips = _skip.map((s) => "-s " + makeRelative(s)).join(" ");
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 $packRoot $scriptPath single -m $mojoSdk -r $root '
'-p $packagePath $skips');
}
_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;
var canonicalPackages =
canonicalPackage.list(recursive: true, followLinks: false);
await for (var entry in canonicalPackages) {
if (entry is! File) continue;
if (!isMojomDart(entry.path)) continue;
String relPath = path.relative(entry.path, from: canonicalPackage.path);
File genFile = new File(path.join(package.path, 'lib', relPath));
log.info("Checking that $genFile exists");
if (!await genFile.exists()) {
String dart = makeRelative(Platform.executable);
String packRoot = (Platform.packageRoot == "")
? ""
: "-p " + makeRelative(Platform.packageRoot);
String root = makeRelative(_mojomRootDir.path);
String genFilePath = makeRelative(genFile.path);
String packagePath = makeRelative(package.path);
String scriptPath = makeRelative(path.fromUri(Platform.script));
String mojoSdk = makeRelative(_mojoSdk.path);
String skips = _skip.map((s) => "-s " + makeRelative(s)).join(" ");
stderr.writeln('The generated file:\n\t$genFilePath\n'
'is needed but does not exist. Run the command\n'
'\t$dart $packRoot $scriptPath single -m $mojoSdk -r $root '
'-p $packagePath $skips');
_errors++;
return;
}
}
}
bool _shouldSkip(File f) => containsPrefix(f.path, _skip);
}