Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support to use flutter-pi as custom embedder #32

Merged
merged 15 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
455 changes: 43 additions & 412 deletions lib/commands/bootstrap/bootstarp_command.dart

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions lib/configs/embedder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// Flutter embedders which we are supporting in snapp_cli
enum FlutterEmbedder {
/// Flutter official embedder for Linux
flutter('Flutter Linux', 'Flutter Linux Official'),

/// Flutter-pi embedder: [https://github.com/ardera/flutter-pi]
flutterPi('Flutter-pi', 'Flutter-pi Embedder');

const FlutterEmbedder(this.label, this.sdkName);

final String label;
final String sdkName;
}
52 changes: 52 additions & 0 deletions lib/service/custom_device_builder/custom_device_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:snapp_cli/configs/embedder.dart';
import 'package:snapp_cli/host_runner/host_runner_platform.dart';
import 'package:snapp_cli/service/custom_device_builder/src/flutter.dart';
import 'package:snapp_cli/service/custom_device_builder/src/flutter_pi.dart';
import 'package:snapp_cli/service/setup_device/device_config_context.dart';
import 'package:snapp_cli/service/setup_device/device_setup.dart';
// ignore: implementation_imports
import 'package:flutter_tools/src/custom_devices/custom_device_config.dart';
import 'package:snapp_cli/snapp_cli.dart';

abstract class CustomDeviceBuilder {
const CustomDeviceBuilder({
required this.flutterSdkManager,
required this.hostPlatform,
});

factory CustomDeviceBuilder.create({
required FlutterEmbedder embedder,
required FlutterSdkManager flutterSdkManager,
required HostRunnerPlatform hostPlatform,
}) {
switch (embedder) {
case FlutterEmbedder.flutter:
return FlutterCustomDeviceBuilder(
flutterSdkManager: flutterSdkManager,
hostPlatform: hostPlatform,
);

case FlutterEmbedder.flutterPi:
return FlutterPiCustomDeviceBuilder(
flutterSdkManager: flutterSdkManager,
hostPlatform: hostPlatform,
);
}
}

final FlutterSdkManager flutterSdkManager;
final HostRunnerPlatform hostPlatform;

Future<CustomDeviceConfig> buildDevice(
final DeviceConfigContext context,
);

bool isContextValid(DeviceConfigContext context) {
return context.id != null &&
context.label != null &&
context.targetIp != null &&
context.username != null &&
context.remoteHasSshConnection &&
context.appExecuterPath != null;
}
}
141 changes: 141 additions & 0 deletions lib/service/custom_device_builder/src/flutter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import 'package:snapp_cli/host_runner/host_runner_platform.dart';
import 'package:snapp_cli/service/custom_device_builder/custom_device_builder.dart';
import 'package:snapp_cli/service/setup_device/device_setup.dart';
// ignore: implementation_imports
import 'package:flutter_tools/src/custom_devices/custom_device_config.dart';

class FlutterCustomDeviceBuilder extends CustomDeviceBuilder {
const FlutterCustomDeviceBuilder({
required super.flutterSdkManager,
required super.hostPlatform,
});

@override
Future<CustomDeviceConfig> buildDevice(
final DeviceConfigContext context,
) async {
if (!isContextValid(context)) {
logger.err('Device context: $context');
throw Exception("Device setup did not produce a valid configuration.");
}

/// path to the icu data file on the host machine
final hostIcuDataPath = flutterSdkManager.icuDataPath;

/// path to the build artifacts on the remote machine
const hostBuildClonePath = 'snapp_embedded';

/// path to the icu data file on the remote machine
const hostIcuDataClone = '$hostBuildClonePath/engine';

final ipv6 = context.ipv6!;

final sshTarget = context.sshTarget!;

final formattedLoopbackIp = context.formattedLoopbackIp!;

final remoteAppExecuter = context.appExecuterPath!;

return CustomDeviceConfig(
id: context.id!,
label: context.formattedLabel,
sdkNameAndVersion: context.sdkName!,
enabled: true,

// host-platform specific, filled out later
pingCommand: hostPlatform.pingCommand(
ipv6: ipv6,
pingTarget: context.targetIp!.address,
),
pingSuccessRegex: hostPlatform.pingSuccessRegex,
postBuildCommand: const <String>[],

// just install to /tmp/${appName} by default
// returns the command runner for the current platform
// for example:
// on windows it returns "powershell -c"
// on linux and macOS it returns "bash -c"
installCommand: hostPlatform.commandRunner(
<String>[
// create the necessary directories in the remote machine
hostPlatform
.sshCommand(
ipv6: ipv6,
sshTarget: sshTarget,
command: 'mkdir -p /tmp/\${appName}/$hostIcuDataClone',
)
.asString,

// copy the current project files from host to the remote
hostPlatform
.scpCommand(
ipv6: ipv6,
source: '${hostPlatform.currentSourcePath}*',
dest: '$sshTarget:/tmp/\${appName}',
)
.asString,

// copy the build artifacts from host to the remote
hostPlatform
.scpCommand(
ipv6: ipv6,
source: r'${localPath}',
dest: '$sshTarget:/tmp/\${appName}/$hostBuildClonePath',
)
.asString,

// copy the icu data file from host to the remote
hostPlatform
.scpCommand(
ipv6: ipv6,
source: hostIcuDataPath,
dest: '$sshTarget:/tmp/\${appName}/$hostIcuDataClone',
lastCommand: true,
)
.asString,
],
),
// just uninstall app by removing the /tmp/${appName} directory on the remote
uninstallCommand: hostPlatform.sshCommand(
ipv6: ipv6,
sshTarget: sshTarget,
command: r'rm -rf "/tmp/${appName}"',
lastCommand: true,
),

// run the app on the remote
runDebugCommand: hostPlatform.sshMultiCommand(
ipv6: ipv6,
sshTarget: sshTarget,
commands: <String>[
'cd /tmp/\${appName} ;',
'$remoteAppExecuter build linux --debug ;',
// remove remote build artifacts
'rm -rf "/tmp/\${appName}/build/flutter_assets/*" ;',
'rm -rf "/tmp/\${appName}/build/linux/arm64/debug/bundle/data/flutter_assets/*" ;',
'rm -rf "/tmp/\${appName}/build/linux/arm64/debug/bundle/data/icudtl.dat" ;',
// and replace them by host build artifacts
'cp /tmp/\${appName}/$hostBuildClonePath/flutter_assets/* /tmp/\${appName}/build/flutter_assets ;',
'cp /tmp/\${appName}/$hostBuildClonePath/flutter_assets/* /tmp/\${appName}/build/linux/arm64/debug/bundle/data/flutter_assets ;',
'cp /tmp/\${appName}/$hostIcuDataClone/icudtl.dat /tmp/\${appName}/build/linux/arm64/debug/bundle/data ;',
// finally run the app
r'DISPLAY=:0 /tmp/\${appName}/build/linux/arm64/debug/bundle/\${appName} ;'
],
),
forwardPortCommand: <String>[
'ssh',
'-o',
'BatchMode=yes',
'-o',
'ExitOnForwardFailure=yes',
if (ipv6) '-6',
'-L',
'$formattedLoopbackIp:\${hostPort}:$formattedLoopbackIp:\${devicePort}',
sshTarget,
"echo 'Port forwarding success'; read",
],
forwardPortSuccessRegex: RegExp('Port forwarding success'),
screenshotCommand: null,
);
}
}
108 changes: 108 additions & 0 deletions lib/service/custom_device_builder/src/flutter_pi.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'package:snapp_cli/host_runner/host_runner_platform.dart';
import 'package:snapp_cli/service/custom_device_builder/custom_device_builder.dart';
import 'package:snapp_cli/service/setup_device/device_setup.dart';
// ignore: implementation_imports
import 'package:flutter_tools/src/custom_devices/custom_device_config.dart';

class FlutterPiCustomDeviceBuilder extends CustomDeviceBuilder {
const FlutterPiCustomDeviceBuilder({
required super.flutterSdkManager,
required super.hostPlatform,
});

@override
Future<CustomDeviceConfig> buildDevice(
final DeviceConfigContext context,
) async {
if (!isContextValid(context)) {
logger.err('Device context: $context');
throw Exception("Device setup did not produce a valid configuration.");
}

final ipv6 = context.ipv6!;

final sshTarget = context.sshTarget!;

final formattedLoopbackIp = context.formattedLoopbackIp!;

final remoteAppExecuter = context.appExecuterPath!;

return CustomDeviceConfig(
id: context.id!,
label: context.formattedLabel,
sdkNameAndVersion: context.sdkName!,
enabled: true,

// host-platform specific, filled out later
pingCommand: hostPlatform.pingCommand(
ipv6: ipv6,
pingTarget: context.targetIp!.address,
),
pingSuccessRegex: hostPlatform.pingSuccessRegex,
postBuildCommand: const <String>[],
installCommand: hostPlatform.commandRunner(
<String>[
// Copy bundle folder to remote
hostPlatform
.scpCommand(
ipv6: ipv6,
source: r'${localPath}',
dest: '$sshTarget:/tmp/\${appName}',
)
.asString,

// Build using flutterpi_tool to be able to use libflutter_engine.so and icudtl.dat
'flutterpi_tool build --arch=arm64 --debug ;',

// Copy libflutter_engine.so to remote
hostPlatform
.scpCommand(
ipv6: ipv6,
source: r'${localPath}/libflutter_engine.so',
dest: '$sshTarget:/tmp/\${appName}',
)
.asString,

// Copy icudtl.dat to remote
hostPlatform
.scpCommand(
ipv6: ipv6,
source: r'${localPath}/icudtl.dat',
dest: '$sshTarget:/tmp/\${appName}',
lastCommand: true,
)
.asString,
],
),
// just uninstall app by removing the /tmp/${appName} directory on the remote
uninstallCommand: hostPlatform.sshCommand(
ipv6: ipv6,
sshTarget: sshTarget,
command:
'PID=\$(ps aux | grep \'$remoteAppExecuter /tmp/\${appName}\' | grep -v grep | awk \'{print \$2}\'); [ -n "\$PID" ] && kill \$PID && echo "Process $remoteAppExecuter /tmp/\${appName} (PID: \$PID) has been killed." || echo "Process not found.";',
lastCommand: true,
),
// run the app on the remote
runDebugCommand: hostPlatform.sshCommand(
ipv6: ipv6,
sshTarget: sshTarget,
lastCommand: true,
command: '$remoteAppExecuter /tmp/\${appName} &',
),
forwardPortCommand: <String>[
'ssh',
'-o',
'BatchMode=yes',
'-o',
'ExitOnForwardFailure=yes',
if (ipv6) '-6',
'-L',
'$formattedLoopbackIp:\${hostPort}:$formattedLoopbackIp:\${devicePort}',
sshTarget,
"echo 'Port forwarding success'; read",
],
forwardPortSuccessRegex: RegExp('Port forwarding success'),
screenshotCommand: null,
);
}
}
70 changes: 70 additions & 0 deletions lib/service/dependency_installer/dependency_installer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:snapp_cli/configs/embedder.dart';
import 'package:snapp_cli/flutter_sdk.dart';
import 'package:snapp_cli/service/dependency_installer/src/flutter_pi_dependency_installer.dart';
import 'package:snapp_cli/service/remote_controller_service.dart';

abstract class DependencyInstaller {
const DependencyInstaller({
required this.flutterSdkManager,
required this.remoteControllerService,
});

final FlutterSdkManager flutterSdkManager;
final RemoteControllerService remoteControllerService;

factory DependencyInstaller.create(
FlutterEmbedder embedder, {
required FlutterSdkManager flutterSdkManager,
required RemoteControllerService remoteControllerService,
}) {
switch (embedder) {
case FlutterEmbedder.flutter:
return NoOpDependencyInstaller(
flutterSdkManager: flutterSdkManager,
remoteControllerService: remoteControllerService,
);
case FlutterEmbedder.flutterPi:
return FlutterPiDependencyInstaller(
flutterSdkManager: flutterSdkManager,
remoteControllerService: remoteControllerService,
);
}
}

Future<bool> install() async {
final isHostDependenciesInstalled = await installDependenciesOnHost();
if (!isHostDependenciesInstalled) {
return false;
}

final isRemoteDependenciesInstalled = await installDependenciesOnRemote();
if (!isRemoteDependenciesInstalled) {
return false;
}

return true;
}

/// Install host dependencies that is required to run the app on the remote device.
Future<bool> installDependenciesOnHost();

/// Install dependencies on the remote device.
Future<bool> installDependenciesOnRemote();
}

class NoOpDependencyInstaller extends DependencyInstaller {
const NoOpDependencyInstaller({
required super.flutterSdkManager,
required super.remoteControllerService,
});

@override
Future<bool> installDependenciesOnHost() async {
return true;
}

@override
Future<bool> installDependenciesOnRemote() async {
return true;
}
}
Loading