Skip to content

Commit

Permalink
feat: add support for releasing, previewing, and patching linux apps (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanoltman authored Jan 30, 2025
1 parent 307ebb5 commit 34ebc6e
Show file tree
Hide file tree
Showing 32 changed files with 2,123 additions and 93 deletions.
2 changes: 1 addition & 1 deletion bin/internal/flutter.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5c1dcc19ebcee3565c65262dd95970186e4d81cc
3d75b30b181d1d4ce66c426c64aca2498529f2e0
1 change: 1 addition & 0 deletions packages/shorebird_cli/bin/shorebird.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Command: shorebird ${args.join(' ')}
idevicesyslogRef,
iosDeployRef,
javaRef,
linuxRef,
loggerRef,
networkCheckerRef,
openRef,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export 'android_archive_differ.dart';
export 'apple_archive_differ.dart';
export 'file_set_diff.dart';
export 'linux_bundle_differ.dart';
export 'plist.dart';
export 'windows_archive_differ.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart';

/// {@template linux_bundle_differ}
/// Finds differences between two Linux bundles.
/// {@endtemplate}
class LinuxBundleDiffer extends ArchiveDiffer {
/// {@macro linux_bundle_differ}
const LinuxBundleDiffer();

bool _isDirectoryPath(String path) {
return path.endsWith('/');
}

@override
bool isAssetFilePath(String filePath) =>
!_isDirectoryPath(filePath) &&
p.split(filePath).any((s) => s == 'flutter_assets');

@override
bool isDartFilePath(String filePath) => p.basename(filePath) == 'libapp.so';

@override
bool isNativeFilePath(String filePath) {
// TODO: implement isNativeFilePath
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import 'dart:io';

import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart';

/// {@template windows_archive_differ}
Expand Down Expand Up @@ -43,17 +40,4 @@ class WindowsArchiveDiffer extends ArchiveDiffer {
// See https://github.com/shorebirdtech/shorebird/issues/2794
return false;
}

@override
Future<FileSetDiff> changedFiles(
String oldArchivePath,
String newArchivePath,
) async {
final oldPathHashes = await fileHashes(File(oldArchivePath));
final newPathHashes = await fileHashes(File(newArchivePath));
return FileSetDiff.fromPathHashes(
oldPathHashes: oldPathHashes,
newPathHashes: newPathHashes,
);
}
}
43 changes: 43 additions & 0 deletions packages/shorebird_cli/lib/src/artifact_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,49 @@ class ArtifactBuilder {
});
}

/// Builds a Linux desktop application by running `flutter build linux
/// --release` with Shorebird's fork of Flutter.
Future<void> buildLinuxApp({
String? target,
List<String> args = const [],
String? base64PublicKey,
DetailProgress? buildProgress,
}) async {
await _runShorebirdBuildCommand(() async {
const executable = 'flutter';
final arguments = [
'build',
'linux',
'--release',
if (target != null) '--target=$target',
...args,
];

final buildProcess = await process.start(
executable,
arguments,
environment: base64PublicKey?.toPublicKeyEnv(),
);

buildProcess.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
logger.detail(line);
});

final stderrLines = await buildProcess.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.toList();
final stdErr = stderrLines.join('\n');
final exitCode = await buildProcess.exitCode;
if (exitCode != ExitCode.success.code) {
throw ArtifactBuildException('Failed to build: $stdErr');
}
});
}

/// Builds a macOS app using `flutter build macos`. Runs `flutter pub get`
/// with the system installation of Flutter to reset
/// `.dart_tool/package_config.json` after the build completes or fails.
Expand Down
15 changes: 15 additions & 0 deletions packages/shorebird_cli/lib/src/artifact_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,21 @@ class ArtifactManager {
.firstWhereOrNull((directory) => directory.path.endsWith('.app'));
}

/// Returns the build/ subdirectory containing the compiled Linux bundle.
Directory get linuxBundleDirectory {
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
return Directory(
p.join(
projectRoot.path,
'build',
'linux',
'x64',
'release',
'bundle',
),
);
}

/// Returns the build/ subdirectory containing the compiled Windows exe.
Directory getWindowsReleaseDirectory() {
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
Expand Down
30 changes: 30 additions & 0 deletions packages/shorebird_cli/lib/src/code_push_client_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,36 @@ aar artifact already exists, continuing...''',
return thinnedArchiveDirectory;
}

/// Zips and uploads a Linux release bundle.
Future<void> createLinuxReleaseArtifacts({
required String appId,
required int releaseId,
required Directory bundle,
}) async {
final createArtifactProgress = logger.progress('Uploading artifacts');
final zippedBundle = await Directory(bundle.path).zipToTempFile();
try {
await codePushClient.createReleaseArtifact(
appId: appId,
releaseId: releaseId,
artifactPath: zippedBundle.path,
arch: primaryLinuxReleaseArtifactArch,
platform: ReleasePlatform.linux,
hash: sha256.convert(await zippedBundle.readAsBytes()).toString(),
canSideload: true,
podfileLockHash: null,
);
} catch (error) {
_handleErrorAndExit(
error,
progress: createArtifactProgress,
message: 'Error uploading bundle: $error',
);
}

createArtifactProgress.complete();
}

/// Registers and uploads macOS release artifacts to the Shorebird server.
Future<void> createMacosReleaseArtifacts({
required String appId,
Expand Down
151 changes: 151 additions & 0 deletions packages/shorebird_cli/lib/src/commands/patch/linux_patcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import 'dart:io';

import 'package:crypto/crypto.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/archive/archive.dart';
import 'package:shorebird_cli/src/archive_analysis/linux_bundle_differ.dart';
import 'package:shorebird_cli/src/artifact_builder.dart';
import 'package:shorebird_cli/src/artifact_manager.dart';
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/code_signer.dart';
import 'package:shorebird_cli/src/commands/commands.dart';
import 'package:shorebird_cli/src/common_arguments.dart';
import 'package:shorebird_cli/src/extensions/arg_results.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/patch_diff_checker.dart';
import 'package:shorebird_cli/src/platform/platform.dart';
import 'package:shorebird_cli/src/release_type.dart';
import 'package:shorebird_cli/src/shorebird_flutter.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/src/base/process.dart';
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';

/// {@template linux_patcher}
/// Functions to create a linux patch.
/// {@endtemplate}
class LinuxPatcher extends Patcher {
/// {@macro linux_patcher}
LinuxPatcher({
required super.argParser,
required super.argResults,
required super.flavor,
required super.target,
});

@override
Future<void> assertPreconditions() async {}

@override
Future<DiffStatus> assertUnpatchableDiffs({
required ReleaseArtifact releaseArtifact,
required File releaseArchive,
required File patchArchive,
}) async {
return patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
localArchive: patchArchive,
releaseArchive: releaseArchive,
archiveDiffer: const LinuxBundleDiffer(),
allowAssetChanges: allowAssetDiffs,
allowNativeChanges: allowNativeDiffs,
);
}

@override
Future<File> buildPatchArtifact({String? releaseVersion}) async {
final flutterVersionString = await shorebirdFlutter.getVersionAndRevision();

final buildAppBundleProgress = logger.detailProgress(
'Building Linux app with Flutter $flutterVersionString',
);

try {
await artifactBuilder.buildLinuxApp(
base64PublicKey: argResults.encodedPublicKey,
);
buildAppBundleProgress.complete();
} on Exception catch (e) {
buildAppBundleProgress.fail(e.toString());
throw ProcessExit(ExitCode.software.code);
}

return artifactManager.linuxBundleDirectory.zipToTempFile();
}

@override
Future<Map<Arch, PatchArtifactBundle>> createPatchArtifacts({
required String appId,
required int releaseId,
required File releaseArtifact,
File? supplementArtifact,
}) async {
final createDiffProgress = logger.progress('Creating patch artifacts');
final patchArtifactPath = p.join(
artifactManager.linuxBundleDirectory.path,
'lib',
'libapp.so',
);
final patchArtifact = File(patchArtifactPath);
final hash = sha256.convert(await patchArtifact.readAsBytes()).toString();

final tempDir = Directory.systemTemp.createTempSync();
final zipPath = p.join(tempDir.path, 'patch.zip');
final zipFile = releaseArtifact.copySync(zipPath);
await artifactManager.extractZip(
zipFile: zipFile,
outputDirectory: tempDir,
);

// The release artifact is the zipped directory at
// build/linux/x64/release/bundle
final appSoPath = p.join(tempDir.path, 'lib', 'libapp.so');

final privateKeyFile = argResults.file(
CommonArguments.privateKeyArg.name,
);
final hashSignature = privateKeyFile != null
? codeSigner.sign(
message: hash,
privateKeyPemFile: privateKeyFile,
)
: null;

final String diffPath;
try {
diffPath = await artifactManager.createDiff(
releaseArtifactPath: appSoPath,
patchArtifactPath: patchArtifactPath,
);
} on Exception catch (error) {
createDiffProgress.fail('$error');
throw ProcessExit(ExitCode.software.code);
}

createDiffProgress.complete();

return {
Arch.x86_64: PatchArtifactBundle(
arch: Arch.x86_64.arch,
path: diffPath,
hash: hash,
size: File(diffPath).lengthSync(),
hashSignature: hashSignature,
),
};
}

@override
Future<String> extractReleaseVersionFromArtifact(File artifact) async {
final outputDirectory = Directory.systemTemp.createTempSync();
await artifactManager.extractZip(
zipFile: artifact,
outputDirectory: outputDirectory,
);
return linux.versionFromLinuxBundle(bundleRoot: outputDirectory);
}

@override
String get primaryReleaseArtifactArch => 'bundle';

@override
ReleaseType get releaseType => ReleaseType.linux;
}
1 change: 1 addition & 0 deletions packages/shorebird_cli/lib/src/commands/patch/patch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export 'aar_patcher.dart';
export 'android_patcher.dart';
export 'ios_framework_patcher.dart';
export 'ios_patcher.dart';
export 'linux_patcher.dart';
export 'macos_patcher.dart';
export 'patch_command.dart';
export 'patcher.dart';
Expand Down
11 changes: 10 additions & 1 deletion packages/shorebird_cli/lib/src/commands/patch/patch_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
return ExitCode.usage.code;
}

if (results.releaseTypes.contains(ReleaseType.linux)) {
logger.warn(linuxBetaWarning);
}

if (results.releaseTypes.contains(ReleaseType.macos)) {
logger.warn(macosBetaWarning);
}
Expand Down Expand Up @@ -257,7 +261,12 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
target: target,
);
case ReleaseType.linux:
throw UnimplementedError();
return LinuxPatcher(
argParser: argParser,
argResults: results,
flavor: flavor,
target: target,
);
case ReleaseType.macos:
return MacosPatcher(
argParser: argParser,
Expand Down
Loading

0 comments on commit 34ebc6e

Please sign in to comment.