Skip to content

Commit

Permalink
feat: add updater_tools with package_patch command (#163)
Browse files Browse the repository at this point in the history
* Initial updater_tools buildout

* cont'd

* update readme

* use cli arg name variables

* coverage

* pr feedback

* coverage

* comments

* Add libapp.so hash to patch package
  • Loading branch information
bryanoltman authored May 17, 2024
1 parent 5707bd8 commit 43b21ee
Show file tree
Hide file tree
Showing 21 changed files with 1,214 additions and 13 deletions.
49 changes: 48 additions & 1 deletion updater_tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,51 @@ Tools to create artifacts used by the Updater.

## Usage

Coming soon!
### 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).
3 changes: 3 additions & 0 deletions updater_tools/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml
analyzer:
exclude:
- lib/version.dart
29 changes: 29 additions & 0 deletions updater_tools/bin/updater_tools.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 changes: 7 additions & 0 deletions updater_tools/lib/src/artifact_type.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// {@template archive_type}
/// The type of archive we are creating a patch for.
/// {@endtemplate}
enum ArchiveType {
/// Android App Bundle
aab,
}
1 change: 1 addition & 0 deletions updater_tools/lib/src/commands/commands.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'package_patch_command.dart';
141 changes: 141 additions & 0 deletions updater_tools/lib/src/commands/package_patch_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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/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';

/// Function signature for the [PatchPackager] constructor.
typedef MakePatchPackager = PatchPackager Function({
required File patchExecutable,
});

/// {@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 releaseFile = File(results[releaseCliArg] as String);
final patchFile = File(results[patchCliArg] as String);
final patchExecutable = File(results[patchExecutableCliArg] as String);
final outputDirectory = Directory(results[outputCliArg] as String);
final archiveType = ArchiveType.values.byName(
results[archiveTypeCliArg] as String,
);

try {
_assertCliArgsValid();
} 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;
}

/// Verifies that CLI arguments point to existing files. Throws an
/// [ArgumentError] if any of the args are not valid.
void _assertCliArgsValid() {
final releaseFilePath = results[releaseCliArg] as String;
final patchFilePath = results[patchCliArg] as String;
final patchExecutablePath = results[patchExecutableCliArg] as String;

_verifyFileExists(releaseFilePath, releaseCliArg);
_verifyFileExists(patchFilePath, patchCliArg);
_verifyFileExists(patchExecutablePath, patchExecutableCliArg);
}

/// Throws an [ArgumentError] if a file at [path] does not exist.
void _verifyFileExists(String path, String name) {
if (!File(path).existsSync()) {
throw ArgumentError.value(
path,
name,
'The $name file does not exist',
);
}
}
}
15 changes: 15 additions & 0 deletions updater_tools/lib/src/commands/updater_tool_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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!;
}
32 changes: 32 additions & 0 deletions updater_tools/lib/src/extensions/archive.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 change: 1 addition & 0 deletions updater_tools/lib/src/extensions/extensions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'archive.dart';
8 changes: 8 additions & 0 deletions updater_tools/lib/src/logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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);
Loading

0 comments on commit 43b21ee

Please sign in to comment.