blob: ad4f3dfabb4cf80e8878bb544f43711030e24e81 [file]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../base/error_handling_io.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
/// A service for creating and parsing [Depfile]s.
class DepfileService {
DepfileService({required Logger logger, required FileSystem fileSystem})
: _logger = logger,
_fileSystem = fileSystem;
final Logger _logger;
final FileSystem _fileSystem;
static final _separatorExpr = RegExp(r'([^\\]) ');
static final _escapeExpr = RegExp(r'\\(.)');
/// Given an [depfile] File, write the depfile contents.
///
/// If both [depfile] and [Depfile.outputs] are empty,
/// ensures the file does not exist.
/// This can be overridden with the [writeEmpty] parameter when
/// both static and runtime dependencies exist and it is not desired
/// to force a rerun due to no depfile.
///
/// If [filterOutputs] is true, files that are listed as both inputs and
/// outputs in the depfile are filtered out from the outputs list before
/// writing to avoid circular dependency cycles in external systems like Xcode.
void writeToFile(
Depfile depfile,
File output, {
bool writeEmpty = false,
bool filterOutputs = false,
}) {
if (depfile.inputs.isEmpty && depfile.outputs.isEmpty && !writeEmpty) {
ErrorHandlingFileSystem.deleteIfExists(output);
return;
}
final buffer = StringBuffer();
final List<File> outputsToWrite;
if (filterOutputs) {
final Set<String> inputPaths = depfile.inputs.map((File f) => f.absolute.path).toSet();
outputsToWrite = depfile.outputs
.where((File file) => !inputPaths.contains(file.absolute.path))
.toList();
} else {
outputsToWrite = depfile.outputs;
}
_writeFilesToBuffer(outputsToWrite, buffer);
buffer.write(': ');
_writeFilesToBuffer(depfile.inputs, buffer);
output.writeAsStringSync(buffer.toString());
}
/// Parse the depfile contents from [file].
///
/// If the syntax is invalid, returns an empty [Depfile].
///
/// If [baseDirectory] is provided, any relative paths in the depfile
/// will be resolved relative to it.
Depfile parse(File file, [Directory? baseDirectory]) {
final String contents = file.readAsStringSync();
final List<String> colonSeparated = contents.split(': ');
if (colonSeparated.length != 2) {
_logger.printError('Invalid depfile: ${file.path}');
return const Depfile(<File>[], <File>[]);
}
final List<File> inputs = _processList(colonSeparated[1].trim(), baseDirectory);
final List<File> outputs = _processList(colonSeparated[0].trim(), baseDirectory);
return Depfile(inputs, outputs);
}
/// Parse the output of dart2js's used dependencies.
///
/// The [file] contains a list of newline separated file URIs. The output
/// file must be manually specified.
Depfile parseDart2js(File file, File output) {
final inputs = <File>[
for (final String rawUri in file.readAsLinesSync())
if (rawUri.trim().isNotEmpty)
if (Uri.tryParse(rawUri) case final Uri fileUri when fileUri.scheme == 'file')
_fileSystem.file(fileUri),
];
return Depfile(inputs, <File>[output]);
}
void _writeFilesToBuffer(List<File> files, StringBuffer buffer) {
final backslash = _fileSystem.path.style.separator == r'\';
for (final outputFile in files) {
String path = _fileSystem.path.normalize(outputFile.path);
if (backslash) {
// Backslashes in a depfile have to be escaped if the platform separator is a backslash.
path = path.replaceAll(r'\', r'\\');
} else {
// Convert all path separators to forward slashes.
path = path.replaceAll(r'\', r'/');
}
// Escape spaces.
path = path.replaceAll(r' ', r'\ ');
buffer.write(' $path');
}
}
List<File> _processList(String rawText, [Directory? baseDirectory]) {
return rawText
// Put every file on right-hand side on the separate line
.replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n')
.split('\n')
// Expand escape sequences, so that '\ ', for example, becomes ' '
.map<String>(
(String path) =>
path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)!).trim(),
)
.where((String path) => path.isNotEmpty)
// The tool doesn't write duplicates to these lists. This call is an attempt to
// be resilient to the outputs of other tools which write or user edits to depfiles.
.toSet()
// Normalize the path before creating a file object.
.map((String path) {
if (baseDirectory != null && _fileSystem.path.isRelative(path)) {
return _fileSystem.file(
_fileSystem.path.normalize(_fileSystem.path.join(baseDirectory.path, path)),
);
}
return _fileSystem.file(_fileSystem.path.normalize(path));
})
.toList();
}
}
/// A class for representing depfile formats.
class Depfile {
/// Create a [Depfile] from a list of [inputs] and [outputs].
const Depfile(this.inputs, this.outputs);
/// The input files for this depfile.
final List<File> inputs;
/// The output files for this depfile.
final List<File> outputs;
}