diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index abc71dbf..4b01ad2c 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -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, ] diff --git a/updater_tools/.gitignore b/updater_tools/.gitignore deleted file mode 100644 index b139716a..00000000 --- a/updater_tools/.gitignore +++ /dev/null @@ -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 \ No newline at end of file diff --git a/updater_tools/README.md b/updater_tools/README.md deleted file mode 100644 index b835fea1..00000000 --- a/updater_tools/README.md +++ /dev/null @@ -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). diff --git a/updater_tools/analysis_options.yaml b/updater_tools/analysis_options.yaml deleted file mode 100644 index f46d765b..00000000 --- a/updater_tools/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -include: package:very_good_analysis/analysis_options.5.1.0.yaml -analyzer: - exclude: - - lib/version.dart diff --git a/updater_tools/bin/updater_tools.dart b/updater_tools/bin/updater_tools.dart deleted file mode 100644 index 91e96385..00000000 --- a/updater_tools/bin/updater_tools.dart +++ /dev/null @@ -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 main(List 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 _flushThenExit(int status) { - return Future.wait([stdout.close(), stderr.close()]) - .then((_) => exit(status)); -} diff --git a/updater_tools/lib/src/artifact_type.dart b/updater_tools/lib/src/artifact_type.dart deleted file mode 100644 index 321f7a52..00000000 --- a/updater_tools/lib/src/artifact_type.dart +++ /dev/null @@ -1,7 +0,0 @@ -/// {@template archive_type} -/// The type of archive we are creating a patch for. -/// {@endtemplate} -enum ArchiveType { - /// Android App Bundle - aab, -} diff --git a/updater_tools/lib/src/commands/commands.dart b/updater_tools/lib/src/commands/commands.dart deleted file mode 100644 index 4a58453f..00000000 --- a/updater_tools/lib/src/commands/commands.dart +++ /dev/null @@ -1 +0,0 @@ -export 'package_patch_command.dart'; diff --git a/updater_tools/lib/src/commands/diff_command.dart b/updater_tools/lib/src/commands/diff_command.dart deleted file mode 100644 index 9b90ff9a..00000000 --- a/updater_tools/lib/src/commands/diff_command.dart +++ /dev/null @@ -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 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; - } -} diff --git a/updater_tools/lib/src/commands/package_patch_command.dart b/updater_tools/lib/src/commands/package_patch_command.dart deleted file mode 100644 index f6ae38f5..00000000 --- a/updater_tools/lib/src/commands/package_patch_command.dart +++ /dev/null @@ -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 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; - } -} diff --git a/updater_tools/lib/src/commands/updater_tool_command.dart b/updater_tools/lib/src/commands/updater_tool_command.dart deleted file mode 100644 index 0dd54e03..00000000 --- a/updater_tools/lib/src/commands/updater_tool_command.dart +++ /dev/null @@ -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 { - /// [ArgResults] used for testing purposes only. - @visibleForTesting - ArgResults? testArgResults; - - /// [ArgResults] for the current command. - ArgResults get results => testArgResults ?? argResults!; -} diff --git a/updater_tools/lib/src/extensions/archive.dart b/updater_tools/lib/src/extensions/archive.dart deleted file mode 100644 index 84a157da..00000000 --- a/updater_tools/lib/src/extensions/archive.dart +++ /dev/null @@ -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 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 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(); - }); - } -} diff --git a/updater_tools/lib/src/extensions/arg_results.dart b/updater_tools/lib/src/extensions/arg_results.dart deleted file mode 100644 index 645ce23e..00000000 --- a/updater_tools/lib/src/extensions/arg_results.dart +++ /dev/null @@ -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; - } -} diff --git a/updater_tools/lib/src/extensions/extensions.dart b/updater_tools/lib/src/extensions/extensions.dart deleted file mode 100644 index 69ad8ff8..00000000 --- a/updater_tools/lib/src/extensions/extensions.dart +++ /dev/null @@ -1 +0,0 @@ -export 'archive.dart'; diff --git a/updater_tools/lib/src/logger.dart b/updater_tools/lib/src/logger.dart deleted file mode 100644 index b711b3cc..00000000 --- a/updater_tools/lib/src/logger.dart +++ /dev/null @@ -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); diff --git a/updater_tools/lib/src/packager/patch_packager.dart b/updater_tools/lib/src/packager/patch_packager.dart deleted file mode 100644 index e4396912..00000000 --- a/updater_tools/lib/src/packager/patch_packager.dart +++ /dev/null @@ -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 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 _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()) { - 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 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, - ); - } - } -} diff --git a/updater_tools/lib/src/process.dart b/updater_tools/lib/src/process.dart deleted file mode 100644 index 71febd31..00000000 --- a/updater_tools/lib/src/process.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:process/process.dart'; -import 'package:scoped_deps/scoped_deps.dart'; - -/// A reference to a [ProcessManager] instance. -final ScopedRef processManagerRef = - create(LocalProcessManager.new); - -/// The [ProcessManager] instance available in the current zone. -ProcessManager get processManager => read(processManagerRef); diff --git a/updater_tools/lib/src/updater_tools.dart b/updater_tools/lib/src/updater_tools.dart deleted file mode 100644 index f8be80de..00000000 --- a/updater_tools/lib/src/updater_tools.dart +++ /dev/null @@ -1,7 +0,0 @@ -/// {@template updater_tools} -/// Tools to create artifacts used by the Updater -/// {@endtemplate} -class UpdaterTools { - /// {@macro updater_tools} - const UpdaterTools(); -} diff --git a/updater_tools/lib/src/updater_tools_command_runner.dart b/updater_tools/lib/src/updater_tools_command_runner.dart deleted file mode 100644 index 583d1222..00000000 --- a/updater_tools/lib/src/updater_tools_command_runner.dart +++ /dev/null @@ -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 { - /// {@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 run(Iterable 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 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; - } -} diff --git a/updater_tools/lib/updater_tools.dart b/updater_tools/lib/updater_tools.dart deleted file mode 100644 index 0750491a..00000000 --- a/updater_tools/lib/updater_tools.dart +++ /dev/null @@ -1,4 +0,0 @@ -/// Tools to create artifacts used by the Updater -library; - -export 'src/updater_tools.dart'; diff --git a/updater_tools/lib/version.dart b/updater_tools/lib/version.dart deleted file mode 100644 index 67a7647b..00000000 --- a/updater_tools/lib/version.dart +++ /dev/null @@ -1,2 +0,0 @@ -// Generated code. Do not modify. -const packageVersion = '0.0.1'; diff --git a/updater_tools/pubspec.yaml b/updater_tools/pubspec.yaml deleted file mode 100644 index 039db0db..00000000 --- a/updater_tools/pubspec.yaml +++ /dev/null @@ -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: diff --git a/updater_tools/test/matchers/equals_file_system_entity.dart b/updater_tools/test/matchers/equals_file_system_entity.dart deleted file mode 100644 index 33285d08..00000000 --- a/updater_tools/test/matchers/equals_file_system_entity.dart +++ /dev/null @@ -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 matchState) { - return actual is FileSystemEntity && - actual.runtimeType == expected.runtimeType && - actual.path == expected.path; - } - - @override - Description describeMismatch( - dynamic actual, - Description mismatchDescription, - Map 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}''', - ); - } -} diff --git a/updater_tools/test/matchers/has_prefix.dart b/updater_tools/test/matchers/has_prefix.dart deleted file mode 100644 index a9c5c5c1..00000000 --- a/updater_tools/test/matchers/has_prefix.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:test/test.dart'; - -HasPrefix hasPrefix(Iterable expected) => HasPrefix(expected); - -/// {@template has_prefix} -/// Matches an [Iterable] that starts with the expected elements. -/// {@endtemplate} -class HasPrefix extends TypeMatcher> { - /// {@macro has_prefix} - HasPrefix(this.expected); - - final Iterable expected; - - @override - bool matches(dynamic actual, Map matchState) { - return actual is Iterable && - actual.length >= expected.length && - IterableZip([actual, expected]).every( - (pair) => pair.first == pair.last, - ); - } - - @override - Description describe(Description description) { - return description.add('starts with $expected'); - } -} diff --git a/updater_tools/test/matchers/matchers.dart b/updater_tools/test/matchers/matchers.dart deleted file mode 100644 index 4d597c13..00000000 --- a/updater_tools/test/matchers/matchers.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'equals_file_system_entity.dart'; -export 'has_prefix.dart'; diff --git a/updater_tools/test/src/commands/diff_command_test.dart b/updater_tools/test/src/commands/diff_command_test.dart deleted file mode 100644 index 63b995b7..00000000 --- a/updater_tools/test/src/commands/diff_command_test.dart +++ /dev/null @@ -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 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); - }); - }); - }); -} diff --git a/updater_tools/test/src/commands/package_patch_command_test.dart b/updater_tools/test/src/commands/package_patch_command_test.dart deleted file mode 100644 index 923a9ee2..00000000 --- a/updater_tools/test/src/commands/package_patch_command_test.dart +++ /dev/null @@ -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 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); - }); - }); - }); -} diff --git a/updater_tools/test/src/packager/patch_packager_test.dart b/updater_tools/test/src/packager/patch_packager_test.dart deleted file mode 100644 index a78e3041..00000000 --- a/updater_tools/test/src/packager/patch_packager_test.dart +++ /dev/null @@ -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 Function() body) { - return runScoped( - body, - values: { - loggerRef.overrideWith(() => logger), - processManagerRef.overrideWith(() => processManager), - }, - ); - } - - Future createAab({ - required String name, - required List 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; - 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() - .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().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().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(); - 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()); - 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()); - 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().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().having( - (e) => e.message, - 'message', - 'patch completed successfully but diff file does not exist', - ), - ), - ); - }); - }); - }); - }); -}