chore: remove updater_tools, I believe they're unused. (#198)

* chore: remove updater_tools, I believe they're unused.

* chore: remove CI for updater_tools
This commit is contained in:
Eric Seidel
2024-07-25 09:37:49 -07:00
committed by GitHub
parent 933e231636
commit af228bee0a
27 changed files with 1 additions and 1559 deletions
+1 -36
View File
@@ -1,3 +1,4 @@
# cspell:words dorny codecov
name: ci
on:
@@ -18,7 +19,6 @@ jobs:
pull-requests: read
outputs:
needs_dart_build: ${{ steps.needs_dart_build.outputs.changes }}
needs_flutter_build: ${{ steps.needs_flutter_build.outputs.changes }}
needs_rust_build: ${{ steps.needs_rust_build.outputs.changes }}
@@ -28,16 +28,6 @@ jobs:
- name: 📚 Git Checkout
uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
name: Build Detection
id: needs_dart_build
with:
filters: |
updater_tools:
- ./.github/workflows/main.yaml
- ./.github/actions/flutter_package/action.yaml
- updater_tools/**
- uses: dorny/paths-filter@v3
name: Build Detection
id: needs_flutter_build
@@ -82,30 +72,6 @@ jobs:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
working_directory: ${{ matrix.crate }}
build_dart_packages:
needs: changes
if: ${{ needs.changes.outputs.needs_dart_build != '[]' }}
strategy:
matrix:
package: ${{ fromJSON(needs.changes.outputs.needs_dart_build) }}
runs-on: ubuntu-latest
name: 🎯 Build ${{ matrix.package }}
steps:
- name: 📚 Git Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: 🐦 Build ${{ matrix.package }}
uses: ./.github/actions/dart_package
with:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
coverage_excludes: "**/*.g.dart"
working_directory: ${{ matrix.package }}
build_flutter_packages:
needs: changes
if: ${{ needs.changes.outputs.needs_flutter_build != '[]' }}
@@ -135,7 +101,6 @@ jobs:
needs:
[
semantic_pull_request,
build_dart_packages,
build_flutter_packages,
build_rust_crates,
]
-9
View File
@@ -1,9 +0,0 @@
# See https://www.dartlang.org/guides/libraries/private-files
bin/
# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock
-56
View File
@@ -1,56 +0,0 @@
# Updater Tools
[![License: MIT][license_badge]][license_link]
Tools to create artifacts used by the Updater.
## Usage
### package_patch
The `package_patch` command accepts two archives (a base/release and a patch)
produced by `flutter build` (only aabs are currently supported) and creates
patch artifacts for every architecture contained in both archives.
This command accepts the following arguments:
- `archive-type`: The type of the archives to process. Currently, only `aab`
is supported.
- `release`: The path to the base/release archive.
- `patch`: The path to the patch archive.
- `patch-executable`: The path to the `patch` executable.
- `output`: The path to the directory where the patch artifacts will be created.
Sample usage:
```
dart run updater_tools package_patch \
--archive-type=aab \
--release=release.aab \
--patch=patch.aab \
--patch-executable=path/to/patch \
--output=patch_output
```
If `release.aab` contains the default architectures produced by `flutter build`
(`arm64-v8a`, `armeabi-v7a`, and `x86_64`), this will produce the following in
the `patch_output` directory:
```
patch_output/
├── arm64-v8a.zip
│ ├── dlc.vmcode
│ └── hash
├── armeabi-v7a.zip
│ ├── dlc.vmcode
│ └── hash
└── x86_64.zip
├── dlc.vmcode
└── hash
```
In each .zip:
- dlc.vmcode: the bidiff file produced by the `patch` executable
- hash: the sha256 digest of the fully constituted (aka pre-diff) patch file
(libapp.so on Android).
-4
View File
@@ -1,4 +0,0 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml
analyzer:
exclude:
- lib/version.dart
-29
View File
@@ -1,29 +0,0 @@
import 'dart:io';
import 'package:scoped_deps/scoped_deps.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/process.dart';
import 'package:updater_tools/src/updater_tools_command_runner.dart';
Future<void> main(List<String> args) async {
await _flushThenExit(
await runScoped(
() async => UpdaterToolsCommandRunner().run(args),
values: {
loggerRef,
processManagerRef,
},
),
);
}
/// Flushes the stdout and stderr streams, then exits the program with the given
/// status code.
///
/// This returns a Future that will never complete, since the program will have
/// exited already. This is useful to prevent Future chains from proceeding
/// after you've decided to exit.
Future<void> _flushThenExit(int status) {
return Future.wait<void>([stdout.close(), stderr.close()])
.then<void>((_) => exit(status));
}
-7
View File
@@ -1,7 +0,0 @@
/// {@template archive_type}
/// The type of archive we are creating a patch for.
/// {@endtemplate}
enum ArchiveType {
/// Android App Bundle
aab,
}
@@ -1 +0,0 @@
export 'package_patch_command.dart';
@@ -1,93 +0,0 @@
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:updater_tools/src/commands/updater_tool_command.dart';
import 'package:updater_tools/src/extensions/arg_results.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/packager/patch_packager.dart';
/// The arg name to specify the path to the release binary.
const releaseCliArg = 'release';
/// The arg name to specify the path to the patch binary.
const patchCliArg = 'patch';
/// The arg name to specify the path to the patch executable.
const patchExecutableCliArg = 'patch-executable';
/// The arg name to specify the output file.
const outputCliArg = 'output';
/// {@template diff_command}
/// A wrapper around the patch executable
/// {@endtemplate}
class DiffCommand extends UpdaterToolCommand {
/// {@macro diff_command}
DiffCommand([MakePatchPackager? makePatchPackager])
: _makePatchPackagerOverride = makePatchPackager,
super() {
argParser
..addOption(
releaseCliArg,
abbr: 'r',
mandatory: true,
help: 'The path to the release artifact which will be patched',
)
..addOption(
patchCliArg,
abbr: 'p',
mandatory: true,
help: 'The path to the patch artifact which will be packaged',
)
..addOption(
patchExecutableCliArg,
mandatory: true,
help:
'''The path to the patch executable that creates a binary diff between two files''',
)
..addOption(
outputCliArg,
abbr: 'o',
mandatory: true,
help: '''
Where to write the packaged patch archives.
This should be a directory, and will contain patch archives for each architecture.''',
);
}
final MakePatchPackager? _makePatchPackagerOverride;
@override
String get name => 'diff';
@override
String get description =>
'''Outputs a binary diff of the provided release and patch files, using release as a base.''';
@override
Future<int> run() async {
final File releaseFile;
final File patchFile;
final File patchExecutable;
try {
releaseFile = results.asExistingFile(releaseCliArg);
patchFile = results.asExistingFile(patchCliArg);
patchExecutable = results.asExistingFile(patchExecutableCliArg);
} catch (e) {
logger.err('$e');
return ExitCode.usage.code;
}
final patchPackager = (_makePatchPackagerOverride ?? PatchPackager.new)(
patchExecutable: patchExecutable,
);
await patchPackager.makeDiff(
base: releaseFile,
patch: patchFile,
outFile: File(results[outputCliArg] as String),
);
return ExitCode.success.code;
}
}
@@ -1,116 +0,0 @@
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:updater_tools/src/artifact_type.dart';
import 'package:updater_tools/src/commands/updater_tool_command.dart';
import 'package:updater_tools/src/extensions/arg_results.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/packager/patch_packager.dart';
/// The arg name to specify the release and patch archive type.
const archiveTypeCliArg = 'archive-type';
/// The arg name to specify the path to the release archive.
const releaseCliArg = 'release';
/// The arg name to specify the path to the patch archive.
const patchCliArg = 'patch';
/// The arg name to specify the path to the patch executable.
const patchExecutableCliArg = 'patch-executable';
/// The arg name to specify the output directory.
const outputCliArg = 'output';
/// {@template package_patch_command}
/// A command to package patch artifacts.
/// {@endtemplate}
class PackagePatchCommand extends UpdaterToolCommand {
/// {@macro package_patch_command}
PackagePatchCommand([MakePatchPackager? makePatchPackager])
: _makePatchPackagerOverride = makePatchPackager,
super() {
argParser
..addOption(
archiveTypeCliArg,
help: 'The format of release and patch. These *must* be the same.',
allowed: ArchiveType.values.asNameMap().keys,
mandatory: true,
)
..addOption(
releaseCliArg,
abbr: 'r',
mandatory: true,
help: 'The path to the release artifact which will be patched',
)
..addOption(
patchCliArg,
abbr: 'p',
mandatory: true,
help: 'The path to the patch artifact which will be packaged',
)
..addOption(
patchExecutableCliArg,
mandatory: true,
help:
'''The path to the patch executable that creates a binary diff between two files''',
)
..addOption(
outputCliArg,
abbr: 'o',
mandatory: true,
help: '''
Where to write the packaged patch archives.
This should be a directory, and will contain patch archives for each architecture.''',
);
}
final MakePatchPackager? _makePatchPackagerOverride;
@override
String get description =>
'''A command that turns two app archives (.aab, .xcarchive, etc.) into patch artifacts.''';
@override
String get name => 'package_patch';
@override
Future<int> run() async {
final outputDirectory = Directory(results[outputCliArg] as String);
final archiveType = ArchiveType.values.byName(
results[archiveTypeCliArg] as String,
);
final File releaseFile;
final File patchFile;
final File patchExecutable;
try {
releaseFile = results.asExistingFile(releaseCliArg);
patchFile = results.asExistingFile(patchCliArg);
patchExecutable = results.asExistingFile(patchExecutableCliArg);
} catch (e) {
logger.err('$e');
return ExitCode.usage.code;
}
if (outputDirectory.existsSync()) {
logger.info('${outputDirectory.path} already exists. Deleting...');
outputDirectory.deleteSync(recursive: true);
}
final packager = (_makePatchPackagerOverride ?? PatchPackager.new)(
patchExecutable: patchExecutable,
);
await packager.packagePatch(
releaseArchive: releaseFile,
patchArchive: patchFile,
archiveType: archiveType,
outputDirectory: outputDirectory,
);
logger.info('Patch packaged to ${outputDirectory.path}');
return ExitCode.success.code;
}
}
@@ -1,15 +0,0 @@
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:meta/meta.dart';
/// {@template updater_tool_command}
/// A base class for updater tool commands.
/// {@endtemplate}
abstract class UpdaterToolCommand extends Command<int> {
/// [ArgResults] used for testing purposes only.
@visibleForTesting
ArgResults? testArgResults;
/// [ArgResults] for the current command.
ArgResults get results => testArgResults ?? argResults!;
}
@@ -1,32 +0,0 @@
import 'dart:io';
import 'dart:isolate';
import 'package:archive/archive_io.dart';
import 'package:path/path.dart' as p;
/// Functions for archiving directories.
extension DirectoryArchive on Directory {
/// Copies this directory to a temporary directory and zips it.
Future<File> zipToTempFile() async {
final tempDir = await Directory.systemTemp.createTemp();
final outFile = File(p.join(tempDir.path, '${p.basename(path)}.zip'));
await Isolate.run(() {
ZipFileEncoder().zipDirectory(this, filename: outFile.path);
});
return outFile;
}
}
/// Functions for unarchiving files.
extension FileArchive on File {
/// Extracts this zip file to the [outputDirectory] directory in a separate
/// isolate.
Future<void> extractZip({required Directory outputDirectory}) async {
await Isolate.run(() async {
final inputStream = InputFileStream(path);
final archive = ZipDecoder().decodeBuffer(inputStream);
await extractArchiveToDisk(archive, outputDirectory.path);
inputStream.closeSync();
});
}
}
@@ -1,21 +0,0 @@
import 'dart:io';
import 'package:args/args.dart';
/// Extension methods for validating options provided to [ArgResults].
extension ArgResultsValidation on ArgResults {
/// Returns the value of the option named [name] as a [File]. If the file
/// does not exist, an [ArgumentError] is thrown.
File asExistingFile(String name) {
final file = File(this[name] as String);
if (!file.existsSync()) {
throw ArgumentError.value(
file.path,
name,
'The $name file does not exist',
);
}
return file;
}
}
@@ -1 +0,0 @@
export 'archive.dart';
-8
View File
@@ -1,8 +0,0 @@
import 'package:mason_logger/mason_logger.dart';
import 'package:scoped_deps/scoped_deps.dart';
/// A reference to a [Logger] instance.
final loggerRef = create(Logger.new);
/// The [Logger] instance available in the current zone.
Logger get logger => read(loggerRef);
@@ -1,187 +0,0 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:updater_tools/src/artifact_type.dart';
import 'package:updater_tools/src/extensions/extensions.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/process.dart';
/// {@template packaging_exception}
/// An exception thrown when creating a patch package fails.
/// {@endtemplate}
class PackagingException implements Exception {
/// {@macro packaging_exception}
const PackagingException(this.message);
/// The message describing the exception.
final String message;
@override
String toString() => 'PackagingException: $message';
}
/// Function signature for the [PatchPackager] constructor.
typedef MakePatchPackager = PatchPackager Function({
required File patchExecutable,
});
/// {@template patch_packager}
/// Creates and packages patch artifacts.
/// {@endtemplate }
class PatchPackager {
/// {@macro patch_packager}
PatchPackager({
required File patchExecutable,
}) : _patchExecutable = patchExecutable {
if (!patchExecutable.existsSync()) {
throw FileSystemException(
'Patch executable does not exist',
patchExecutable.path,
);
}
}
final File _patchExecutable;
/// Create and package a patch of [patchArchive] onto [releaseArchive].
Future<Directory> packagePatch({
required File releaseArchive,
required File patchArchive,
required ArchiveType archiveType,
required Directory outputDirectory,
}) async {
final directory = switch (archiveType) {
ArchiveType.aab => await _packageAndroidAabPatch(
releaseAab: releaseArchive,
patchAab: patchArchive,
),
};
return directory.renameSync(outputDirectory.path);
}
/// Create and package a patch of [patchAab] onto [releaseAab]. The returned
/// directory will contain a zip file for each architecture in the release
/// aab.
///
/// If a libapp.so exists for an architecture in [releaseAab] but not in
/// [patchAab], a [PackagingException] will be thrown.
Future<Directory> _packageAndroidAabPatch({
required File releaseAab,
required File patchAab,
}) async {
// Extract the release and patch aabs to temporary directories.
//
// temp_dir
// └── release
// └── [release aab contents]
// └── patch
// └── [patch aab contents]
final extractionDir = Directory.systemTemp.createTempSync();
final extractedReleaseDir =
Directory(p.join(extractionDir.path, 'release'));
await releaseAab.extractZip(outputDirectory: extractedReleaseDir);
final extractedPatchDir = Directory(p.join(extractionDir.path, 'patch'));
await patchAab.extractZip(outputDirectory: extractedPatchDir);
// The base/lib directory in the extracted aab contains a directory for
// each architecture in the aab. Each of these directories contains a
// libapp.so file.
final releaseArchsDir = Directory(
p.join(extractedReleaseDir.path, 'base', 'lib'),
);
final outDir = Directory.systemTemp.createTempSync();
// For every architecture in the release aab, create a diff and zip it.
// If a libapp.so exists for an architecture in the release aab but not in
// the patch aab, throw an exception.
for (final archDir in releaseArchsDir.listSync().whereType<Directory>()) {
final archName = p.basename(archDir.path);
logger.detail('Creating diff for $archName');
// Get the elf files for the release and patch aabs.
final relativeArchPath =
p.relative(archDir.path, from: extractedReleaseDir.path);
final releaseElf = File(p.join(archDir.path, 'libapp.so'));
final patchElf = File(
p.join(extractedPatchDir.path, relativeArchPath, 'libapp.so'),
);
// If the release aab is missing a libapp.so, this is likely not a Flutter
// app. Throw an exception.
if (!releaseElf.existsSync()) {
throw PackagingException('Release aab missing libapp.so for $archName');
}
// Make sure the patch aab has a libapp.so for this architecture.
if (!patchElf.existsSync()) {
throw PackagingException('Patch aab missing libapp.so for $archName');
}
// Create a diff file in an output directory named [archName].
final diffArchDir = Directory(p.join(outDir.path, archName))
..createSync(recursive: true);
final diffFile = File(p.join(diffArchDir.path, 'dlc.vmcode'));
await makeDiff(
base: releaseElf,
patch: patchElf,
outFile: diffFile,
);
logger.detail('Diff file created at ${diffFile.path}');
// Write the hash of the pre-diffed patch elf to a file.
final hash = sha256.convert(await patchElf.readAsBytes()).toString();
File(p.join(diffArchDir.path, 'hash'))
..createSync(recursive: true)
..writeAsStringSync(hash);
// Zip the directory containing the diff file and move it to the output
// directory.
final zippedDiff = await diffArchDir.zipToTempFile();
final zipTargetPath = p.join(outDir.path, '$archName.zip');
logger.detail('Moving packaged patch to $zipTargetPath');
zippedDiff.renameSync(zipTargetPath);
// Clean up.
diffArchDir.deleteSync(recursive: true);
}
return outDir;
}
/// Create a binary diff between [base] and [patch]. Returns the path to the
/// diff file.
Future<void> makeDiff({
required File base,
required File patch,
required File outFile,
}) async {
logger.detail('Creating diff between ${base.path} and ${patch.path}');
final args = [
_patchExecutable.path,
base.path,
patch.path,
outFile.path,
];
final result = await processManager.run(args);
if (result.exitCode != ExitCode.success.code) {
throw ProcessException(
args.first,
args.sublist(1),
'Failed to create diff',
result.exitCode,
);
}
if (!outFile.existsSync()) {
throw FileSystemException(
'patch completed successfully but diff file does not exist',
outFile.path,
);
}
}
}
-9
View File
@@ -1,9 +0,0 @@
import 'package:process/process.dart';
import 'package:scoped_deps/scoped_deps.dart';
/// A reference to a [ProcessManager] instance.
final ScopedRef<ProcessManager> processManagerRef =
create(LocalProcessManager.new);
/// The [ProcessManager] instance available in the current zone.
ProcessManager get processManager => read(processManagerRef);
-7
View File
@@ -1,7 +0,0 @@
/// {@template updater_tools}
/// Tools to create artifacts used by the Updater
/// {@endtemplate}
class UpdaterTools {
/// {@macro updater_tools}
const UpdaterTools();
}
@@ -1,103 +0,0 @@
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:updater_tools/src/commands/commands.dart';
import 'package:updater_tools/src/commands/diff_command.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/version.dart';
/// The name of this executable.
const executableName = 'updater_tools';
/// The description of this executable.
const description = 'Tools to create artifacts to be consumed by the Updater.';
/// {@template updater_tools_command_runner}
/// A [CommandRunner] for the CLI.
/// {@endtemplate}
class UpdaterToolsCommandRunner extends CommandRunner<int> {
/// {@macro updater_tools_command_runner}
UpdaterToolsCommandRunner() : super(executableName, description) {
// Add root options and flags
argParser
..addFlag(
'version',
abbr: 'v',
negatable: false,
help: 'Print the current version.',
)
..addFlag(
'verbose',
help: 'Noisy logging, including all shell commands executed.',
);
addCommand(DiffCommand());
addCommand(PackagePatchCommand());
}
@override
void printUsage() => logger.info(usage);
@override
Future<int> run(Iterable<String> args) async {
try {
final topLevelResults = parse(args);
if (topLevelResults['verbose'] == true) {
logger.level = Level.verbose;
}
return await runCommand(topLevelResults) ?? ExitCode.success.code;
} on FormatException catch (e, stackTrace) {
// On format errors, show the commands error message, root usage and
// exit with an error code
logger
..err(e.message)
..err('$stackTrace')
..info('')
..info(usage);
return ExitCode.usage.code;
} on UsageException catch (e) {
// On usage errors, show the commands usage message and
// exit with an error code
logger
..err(e.message)
..info('')
..info(e.usage);
return ExitCode.usage.code;
}
}
@override
Future<int?> runCommand(ArgResults topLevelResults) async {
// Verbose logs
logger
..detail('Argument information:')
..detail(' Top level options:');
for (final option in topLevelResults.options) {
if (topLevelResults.wasParsed(option)) {
logger.detail(' - $option: ${topLevelResults[option]}');
}
}
if (topLevelResults.command != null) {
final commandResult = topLevelResults.command!;
logger
..detail(' Command: ${commandResult.name}')
..detail(' Command options:');
for (final option in commandResult.options) {
if (commandResult.wasParsed(option)) {
logger.detail(' - $option: ${commandResult[option]}');
}
}
}
// Run the command or show version
final int? exitCode;
if (topLevelResults['version'] == true) {
logger.info(packageVersion);
exitCode = ExitCode.success.code;
} else {
exitCode = await super.runCommand(topLevelResults);
}
return exitCode;
}
}
-4
View File
@@ -1,4 +0,0 @@
/// Tools to create artifacts used by the Updater
library;
export 'src/updater_tools.dart';
-2
View File
@@ -1,2 +0,0 @@
// Generated code. Do not modify.
const packageVersion = '0.0.1';
-29
View File
@@ -1,29 +0,0 @@
name: updater_tools
description: Tools to create artifacts used by the Updater
version: 0.0.1
publish_to: none
environment:
sdk: "^3.3.0"
dependencies:
archive: ^3.5.1
args: ^2.4.2
collection: ^1.18.0
crypto: ^3.0.3
mason_logger: ^0.2.12
meta: ^1.15.0
path: ^1.9.0
process: ^5.0.2
scoped_deps: ^0.1.0+1
dev_dependencies:
build_runner: ^2.4.8
build_verify: ^3.1.0
build_version: ^2.1.1
mocktail: ^1.0.3
test: ^1.25.2
very_good_analysis: ^5.1.0
executables:
updater_tools:
@@ -1,51 +0,0 @@
import 'dart:io';
import 'package:test/test.dart';
EqualsFileSystemEntity equalsFileSystemEntity(FileSystemEntity expected) =>
EqualsFileSystemEntity(expected);
/// {@template equals_file_system_entity}
/// Matches a [FileSystemEntity] that has the same type and path as [expected].
/// {@endtemplate}
class EqualsFileSystemEntity extends Matcher {
/// {@macro equals_file_system_entity}
EqualsFileSystemEntity(this.expected);
/// The [FileSystemEntity] to compare against.
final FileSystemEntity expected;
@override
bool matches(Object? actual, Map<dynamic, dynamic> matchState) {
return actual is FileSystemEntity &&
actual.runtimeType == expected.runtimeType &&
actual.path == expected.path;
}
@override
Description describeMismatch(
dynamic actual,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
if (actual is! FileSystemEntity) {
mismatchDescription.add('is not a FileSystemEntity');
} else if (actual.runtimeType != expected.runtimeType) {
mismatchDescription.add('is not a ${expected.runtimeType}');
} else if (actual.path != expected.path) {
mismatchDescription.add(
'does not have path ${expected.path} (actual ${actual.path})',
);
}
return mismatchDescription;
}
@override
Description describe(Description description) {
return description.add(
'''is a FileSystemEntity of type ${expected.runtimeType} and path ${expected.path}''',
);
}
}
@@ -1,28 +0,0 @@
import 'package:collection/collection.dart';
import 'package:test/test.dart';
HasPrefix<T> hasPrefix<T>(Iterable<T> expected) => HasPrefix<T>(expected);
/// {@template has_prefix}
/// Matches an [Iterable] that starts with the expected elements.
/// {@endtemplate}
class HasPrefix<T> extends TypeMatcher<Iterable<T>> {
/// {@macro has_prefix}
HasPrefix(this.expected);
final Iterable<T> expected;
@override
bool matches(dynamic actual, Map<dynamic, dynamic> matchState) {
return actual is Iterable<T> &&
actual.length >= expected.length &&
IterableZip([actual, expected]).every(
(pair) => pair.first == pair.last,
);
}
@override
Description describe(Description description) {
return description.add('starts with $expected');
}
}
@@ -1,2 +0,0 @@
export 'equals_file_system_entity.dart';
export 'has_prefix.dart';
@@ -1,174 +0,0 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:scoped_deps/scoped_deps.dart';
import 'package:test/test.dart';
import 'package:updater_tools/src/commands/diff_command.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/packager/patch_packager.dart';
import '../../matchers/matchers.dart';
class _MockArgResults extends Mock implements ArgResults {}
class _MockLogger extends Mock implements Logger {}
class _MockPatchPackager extends Mock implements PatchPackager {}
void main() {
group(DiffCommand, () {
late ArgResults argResults;
late Logger logger;
late PatchPackager patchPackager;
late DiffCommand command;
late File releaseFile;
late File patchFile;
late File patchExecutable;
late File outputFile;
R runWithOverrides<R>(R Function() body) {
return runScoped(
body,
values: {
loggerRef.overrideWith(() => logger),
},
);
}
setUpAll(() {
registerFallbackValue(Directory(''));
registerFallbackValue(File(''));
});
setUp(() {
argResults = _MockArgResults();
logger = _MockLogger();
patchPackager = _MockPatchPackager();
final tempDir = Directory.systemTemp.createTempSync();
releaseFile = File(p.join(tempDir.path, 'release'))
..createSync(recursive: true);
patchFile = File(p.join(tempDir.path, 'patch'))
..createSync(recursive: true);
patchExecutable = File(p.join(tempDir.path, 'patch.exe'))
..createSync(recursive: true);
outputFile = File(p.join(tempDir.path, 'output'));
when(() => argResults[releaseCliArg]).thenReturn(releaseFile.path);
when(() => argResults[patchCliArg]).thenReturn(patchFile.path);
when(() => argResults[patchExecutableCliArg])
.thenReturn(patchExecutable.path);
when(() => argResults[outputCliArg]).thenReturn(outputFile.path);
command = DiffCommand(
({required File patchExecutable}) => patchPackager,
)..testArgResults = argResults;
});
test('has a non-empty name', () {
expect(command.name, isNotEmpty);
});
test('has a non-empty description', () {
expect(command.description, isNotEmpty);
});
group('arg validation', () {
group('when release file does not exist', () {
setUp(() {
releaseFile.deleteSync();
});
test('logs error and exits with code 64', () async {
expect(
await runWithOverrides(command.run),
equals(ExitCode.usage.code),
);
verify(
() => logger.err(
any(that: contains('The release file does not exist')),
),
);
});
});
group('when patch file does not exist', () {
setUp(() {
patchFile.deleteSync();
});
test('logs error and exits with code 64', () async {
expect(
await runWithOverrides(command.run),
equals(ExitCode.usage.code),
);
verify(
() => logger.err(
any(that: contains('The patch file does not exist')),
),
);
});
});
group('when patch executable does not exist', () {
setUp(() {
patchExecutable.deleteSync();
});
test('logs error and exits with code 64', () async {
expect(
await runWithOverrides(command.run),
equals(ExitCode.usage.code),
);
verify(
() => logger.err(
any(that: contains('The patch-executable file does not exist')),
),
);
});
});
});
group('when args are valid', () {
setUp(() {
when(
() => patchPackager.makeDiff(
base: any(named: 'base'),
patch: any(named: 'patch'),
outFile: any(named: 'outFile'),
),
).thenAnswer((_) async {});
});
test('forwards values to patchPackager', () {
expect(
runWithOverrides(command.run),
completion(ExitCode.success.code),
);
verify(
() => patchPackager.makeDiff(
base: any(
named: 'base',
that: equalsFileSystemEntity(releaseFile),
),
patch: any(
named: 'patch',
that: equalsFileSystemEntity(patchFile),
),
outFile: any(
named: 'outFile',
that: equalsFileSystemEntity(outputFile),
),
),
).called(1);
});
});
});
}
@@ -1,195 +0,0 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:scoped_deps/scoped_deps.dart';
import 'package:test/test.dart';
import 'package:updater_tools/src/artifact_type.dart';
import 'package:updater_tools/src/commands/commands.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/packager/patch_packager.dart';
import '../../matchers/equals_file_system_entity.dart';
class _MockArgResults extends Mock implements ArgResults {}
class _MockLogger extends Mock implements Logger {}
class _MockPatchPackager extends Mock implements PatchPackager {}
void main() {
group(PackagePatchCommand, () {
late ArgResults argResults;
late Logger logger;
late PatchPackager patchPackager;
late PackagePatchCommand command;
late Directory outputDirectory;
late File releaseBundle;
late File patchBundle;
late File patchExecutable;
R runWithOverrides<R>(R Function() body) {
return runScoped(
body,
values: {
loggerRef.overrideWith(() => logger),
},
);
}
setUpAll(() {
registerFallbackValue(ArchiveType.aab);
registerFallbackValue(Directory(''));
registerFallbackValue(File(''));
});
setUp(() {
argResults = _MockArgResults();
logger = _MockLogger();
patchPackager = _MockPatchPackager();
final tempDir = Directory.systemTemp.createTempSync();
releaseBundle = File(p.join(tempDir.path, 'release'))
..createSync(recursive: true);
patchBundle = File(p.join(tempDir.path, 'patch'))
..createSync(recursive: true);
patchExecutable = File(p.join(tempDir.path, 'patch.exe'))
..createSync(recursive: true);
outputDirectory = Directory(p.join(tempDir.path, 'output'));
when(() => argResults[releaseCliArg]).thenReturn(releaseBundle.path);
when(() => argResults[patchCliArg]).thenReturn(patchBundle.path);
when(() => argResults[patchExecutableCliArg])
.thenReturn(patchExecutable.path);
when(() => argResults[outputCliArg]).thenReturn(outputDirectory.path);
when(() => argResults[archiveTypeCliArg])
.thenReturn(ArchiveType.aab.name);
when(
() => patchPackager.packagePatch(
releaseArchive: any(named: 'releaseArchive'),
patchArchive: any(named: 'patchArchive'),
archiveType: any(named: 'archiveType'),
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer(
(invocation) async =>
(invocation.namedArguments[#outputDirectory] as Directory)
..createSync(recursive: true),
);
command = PackagePatchCommand(
({required File patchExecutable}) => patchPackager,
)..testArgResults = argResults;
});
test('has a non-empty name', () {
expect(command.name, isNotEmpty);
});
test('has a non-empty description', () {
expect(command.description, isNotEmpty);
});
group('arg validation', () {
group('when release file does not exist', () {
setUp(() {
releaseBundle.deleteSync();
});
test('logs error and exits with code 64', () async {
expect(
await runWithOverrides(command.run),
equals(ExitCode.usage.code),
);
verify(
() => logger.err(
any(that: contains('The release file does not exist')),
),
);
});
});
group('when patch file does not exist', () {
setUp(() {
patchBundle.deleteSync();
});
test('logs error and exits with code 64', () async {
expect(
await runWithOverrides(command.run),
equals(ExitCode.usage.code),
);
verify(
() => logger.err(
any(that: contains('The patch file does not exist')),
),
);
});
});
group('when patch executable does not exist', () {
setUp(() {
patchExecutable.deleteSync();
});
test('logs error and exits with code 64', () async {
expect(
await runWithOverrides(command.run),
equals(ExitCode.usage.code),
);
verify(
() => logger.err(
any(that: contains('The patch-executable file does not exist')),
),
);
});
});
});
group('run', () {
group('when output directory already exists', () {
test('deletes the existing output directory', () {
outputDirectory.createSync(recursive: true);
File(p.join(outputDirectory.path, 'file'))
..createSync(recursive: true)
..writeAsStringSync('should be deleted');
expect(outputDirectory.listSync(), hasLength(1));
expect(() => runWithOverrides(command.run), returnsNormally);
expect(outputDirectory.existsSync(), isTrue);
expect(outputDirectory.listSync(), isEmpty);
});
});
test('forwards args to patch packager', () async {
expect(await runWithOverrides(command.run), ExitCode.success.code);
verify(
() => patchPackager.packagePatch(
releaseArchive: any(
named: 'releaseArchive',
that: equalsFileSystemEntity(releaseBundle),
),
patchArchive: any(
named: 'patchArchive',
that: equalsFileSystemEntity(patchBundle),
),
archiveType: ArchiveType.aab,
outputDirectory: any(
named: 'outputDirectory',
that: equalsFileSystemEntity(outputDirectory),
),
),
).called(1);
});
});
});
}
@@ -1,340 +0,0 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:process/process.dart';
import 'package:scoped_deps/scoped_deps.dart';
import 'package:test/test.dart';
import 'package:updater_tools/src/artifact_type.dart';
import 'package:updater_tools/src/extensions/archive.dart';
import 'package:updater_tools/src/logger.dart';
import 'package:updater_tools/src/packager/patch_packager.dart';
import 'package:updater_tools/src/process.dart';
import '../../matchers/has_prefix.dart';
class _MockLogger extends Mock implements Logger {}
class _MockProcessManager extends Mock implements ProcessManager {}
void main() {
group(PackagingException, () {
group('toString()', () {
test('returns a string with the message', () {
const message = 'message';
const exception = PackagingException(message);
expect(exception.toString(), equals('PackagingException: $message'));
});
});
});
group(PatchPackager, () {
const dlcVmcodeContents = 'dlc.vmcode contents';
const libappSoContents = 'libapp.so contents';
late Logger logger;
late ProcessManager processManager;
late PatchPackager packager;
R runWithOverrides<R>(R Function() body) {
return runScoped(
body,
values: {
loggerRef.overrideWith(() => logger),
processManagerRef.overrideWith(() => processManager),
},
);
}
Future<File> createAab({
required String name,
required List<String> archs,
}) async {
final tempDir = Directory.systemTemp.createTempSync();
final libDir = Directory(p.join(tempDir.path, 'base', 'lib'))
..createSync(recursive: true);
for (final arch in archs) {
File(p.join(libDir.path, arch, 'libapp.so'))
..createSync(recursive: true)
..writeAsStringSync(libappSoContents);
}
final zippedAab = await tempDir.zipToTempFile();
return zippedAab.renameSync('$name.aab');
}
late File patchExecutable;
setUp(() {
final tempDir = Directory.systemTemp.createTempSync();
patchExecutable = File(p.join(tempDir.path, 'patch'))
..createSync(recursive: true);
logger = _MockLogger();
processManager = _MockProcessManager();
// `patch` is invoked like:
// ```
// $ patch baseSnapshot patchSnapshot outputFile
// ````
when(
() => processManager.run(any(that: hasPrefix([patchExecutable.path]))),
).thenAnswer(
(invocation) async {
final commandParts =
invocation.positionalArguments.first as List<Object>;
final outputFilePath = commandParts.last as String;
File(outputFilePath)
..createSync()
..writeAsStringSync(dlcVmcodeContents);
return ProcessResult(0, ExitCode.success.code, '', '');
},
);
packager = PatchPackager(patchExecutable: patchExecutable);
});
group('initialization', () {
test('throws exception when patchExecutable does not exist', () {
final patchExecutable = File('nonexistent');
expect(
() => PatchPackager(patchExecutable: patchExecutable),
throwsA(
isA<FileSystemException>()
.having(
(e) => e.message,
'message',
'Patch executable does not exist',
)
.having(
(e) => e.path,
'path',
patchExecutable.path,
),
),
);
});
});
group('packagePatch with .aabs', () {
group('when release aab has arch folder missing libapp.so', () {
const archName = 'arm64';
late File releaseAab;
late File patchAab;
setUp(() async {
final tempDir = Directory.systemTemp.createTempSync();
// Create a directory for an arch named 'arm64' with an incorrectly-
// named .so file.
File(
p.join(tempDir.path, 'base', 'lib', archName, 'not_a_libapp.so'),
).createSync(recursive: true);
releaseAab = await tempDir.zipToTempFile();
patchAab = await createAab(name: 'patch', archs: [archName]);
});
test('throws a PackagingException', () async {
await expectLater(
() => runWithOverrides(
() => packager.packagePatch(
releaseArchive: releaseAab,
patchArchive: patchAab,
archiveType: ArchiveType.aab,
outputDirectory: Directory.systemTemp,
),
),
throwsA(
isA<PackagingException>().having(
(e) => e.message,
'message',
'Release aab missing libapp.so for $archName',
),
),
);
});
});
group('when patch aab is missing archs present in release aab', () {
test('throws a PackagingException', () async {
final outDir = Directory(
p.join(
Directory.systemTemp.createTempSync().path,
'out',
),
);
final releaseAab = await createAab(
name: 'release',
archs: ['arch1', 'arch2'],
);
final patchAab = await createAab(name: 'patch', archs: ['arch1']);
await expectLater(
() => runWithOverrides(
() => packager.packagePatch(
releaseArchive: releaseAab,
patchArchive: patchAab,
archiveType: ArchiveType.aab,
outputDirectory: outDir,
),
),
throwsA(
isA<PackagingException>().having(
(e) => e.message,
'message',
'Patch aab missing libapp.so for arch2',
),
),
);
});
});
test('outputs one zipped patch file per arch found in the release aab',
() async {
final outDir = Directory(
p.join(
Directory.systemTemp.createTempSync().path,
'out',
),
);
expect(outDir.existsSync(), isFalse);
const archs = ['arm64-v8a', 'armeabi-v7a', 'x86_64'];
final releaseAab = await createAab(name: 'release', archs: archs);
final patchAab = await createAab(name: 'patch', archs: archs);
await runWithOverrides(
() => packager.packagePatch(
releaseArchive: releaseAab,
patchArchive: patchAab,
archiveType: ArchiveType.aab,
outputDirectory: outDir,
),
);
// The outputDirectory should have been created.
expect(outDir.existsSync(), isTrue);
// The outputDirectory should contain a zip file for each arch.
final outDirContents = outDir.listSync().whereType<File>();
expect(outDirContents, hasLength(3));
expect(
outDirContents.map((f) => p.basename(f.path)),
containsAll(archs.map((a) => '$a.zip')),
);
// Each zip file should decompress to a directory with the given arch
// name containing the dlc.vmcode file produced by the patch executable.
for (final zipFile in outDirContents) {
final tempDir = Directory.systemTemp.createTempSync();
await zipFile.extractZip(outputDirectory: tempDir);
final extractedContents = tempDir.listSync();
// Contents of the zip file should be:
// 1. A dlc.vmcode file (a bidiff produced by the patch executable).
// 2. A hash file that is the sha256 hash of the patch libapp.so.
expect(extractedContents, hasLength(2));
expect(
extractedContents.map((e) => p.basename(e.path)),
containsAll(['dlc.vmcode', 'hash']),
);
final patchFile = extractedContents
.firstWhere((e) => p.basename(e.path) == 'dlc.vmcode');
expect(patchFile, isA<File>());
expect(p.basename(patchFile.path), equals('dlc.vmcode'));
expect(
(patchFile as File).readAsStringSync(),
equals(dlcVmcodeContents),
);
// Contents of the hash file should be the sha256 hash of the patch
// libapp.so.
final hashFile =
extractedContents.firstWhere((e) => p.basename(e.path) == 'hash');
expect(hashFile, isA<File>());
expect(
(hashFile as File).readAsStringSync(),
equals(sha256.convert(libappSoContents.codeUnits).toString()),
);
}
});
});
group('when patch executable fails', () {
late File releaseAab;
late File patchAab;
setUp(() async {
releaseAab = await createAab(name: 'release', archs: ['arch1']);
patchAab = await createAab(name: 'patch', archs: ['arch1']);
});
group('when process exits with nonzero code', () {
setUp(() {
when(
() => processManager.run(
any(that: hasPrefix([patchExecutable.path])),
),
).thenAnswer(
(_) async => ProcessResult(0, ExitCode.software.code, '', ''),
);
});
test('throws ProcessException', () async {
await expectLater(
() => runWithOverrides(
() => packager.packagePatch(
releaseArchive: releaseAab,
patchArchive: patchAab,
archiveType: ArchiveType.aab,
outputDirectory: Directory.systemTemp,
),
),
throwsA(
isA<ProcessException>().having(
(e) => e.message,
'message',
'Failed to create diff',
),
),
);
});
});
group('when output file is not created', () {
setUp(() {
when(
() => processManager.run(
any(that: hasPrefix([patchExecutable.path])),
),
).thenAnswer(
// Don't create the output file specified by the command.
(_) async => ProcessResult(0, ExitCode.success.code, '', ''),
);
});
test('throws FileSystemException', () async {
await expectLater(
() => runWithOverrides(
() => packager.packagePatch(
releaseArchive: releaseAab,
patchArchive: patchAab,
archiveType: ArchiveType.aab,
outputDirectory: Directory.systemTemp,
),
),
throwsA(
isA<FileSystemException>().having(
(e) => e.message,
'message',
'patch completed successfully but diff file does not exist',
),
),
);
});
});
});
});
}