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:
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -1,4 +0,0 @@
|
||||
include: package:very_good_analysis/analysis_options.5.1.0.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/version.dart
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/// Tools to create artifacts used by the Updater
|
||||
library;
|
||||
|
||||
export 'src/updater_tools.dart';
|
||||
@@ -1,2 +0,0 @@
|
||||
// Generated code. Do not modify.
|
||||
const packageVersion = '0.0.1';
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user