Files
FlClash/setup.dart
T
chen08209 45015f856b Support custom overwrite
Support run on demand

Optimize windows ipc

Optimize windows arm64

Optimize build

Optimize some details

Update core
2026-05-29 14:35:56 +08:00

409 lines
10 KiB
Dart
Executable File

import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:path/path.dart' as p;
const _allTargets = <String, String>{
'android': 'apk',
'linux': 'deb', // appimage + rpm added for amd64 only
'macos': 'dmg',
'windows': 'exe,zip',
};
const _androidFlutterTarget = {
'arm': 'android-arm',
'arm64': 'android-arm64',
'amd64': 'android-x64',
};
const _hostPlatform = {
'linux': 'linux',
'macos': 'macos',
'windows': 'windows',
};
Future<void> main(List<String> args) async {
final parser = createSetupArgParser();
if (args.contains('--help') || args.contains('-h')) {
_showHelp(parser);
exit(0);
}
final results = parser.parse(args);
final rest = results.rest;
final hostOs = Platform.operatingSystem;
final host = _hostPlatform[hostOs];
if (host == null) {
stderr.writeln('Unsupported host platform: $hostOs');
exit(1);
}
final platform = rest.isNotEmpty ? rest.first : host;
if (platform != host && platform != 'android') {
stderr.writeln(
'Cannot build "$platform" on $hostOs. Allowed: $host, android',
);
_showHelp(parser);
exit(1);
}
final env = results['env'] as String;
final rootDir = Directory.current.path;
final arch = _detectArch();
final targets = _getTargets(platform, arch, results['targets']);
final androidArch = results['arch'] as String?;
final verbose = results['verbose'] as bool;
final exitCode = await _package(
platform,
env,
targets,
rootDir,
arch,
androidArch: androidArch,
verbose: verbose,
);
exit(exitCode);
}
ArgParser createSetupArgParser() {
return ArgParser()
..addOption(
'env',
defaultsTo: 'pre',
allowed: ['pre', 'stable'],
help: 'Application environment',
)
..addOption(
'targets',
valueHelp: 'exe,zip,dmg,apk,...',
help: 'Package targets (default: all for platform)',
)
..addOption(
'arch',
valueHelp: 'arm,arm64,amd64',
allowed: ['arm', 'arm64', 'amd64'],
help: 'Target architecture (Android only)',
)
..addFlag(
'verbose',
abbr: 'v',
negatable: false,
help: 'Enable verbose Flutter build output',
);
}
List<String> createFlutterBuildArgs({
required String platform,
required bool verbose,
}) {
final flutterBuildArgs = <String>[
if (verbose) 'verbose',
'dart-define-from-file=env.json',
];
if (platform == 'android') {
flutterBuildArgs.add('split-per-abi');
}
return flutterBuildArgs;
}
String _getTargets(String platform, String arch, String? customTargets) {
if (customTargets != null) return customTargets;
if (platform == 'linux' && arch == 'amd64') return 'deb,appimage,rpm';
return _allTargets[platform]!;
}
void _showHelp(ArgParser parser) {
stderr.writeln('Usage: dart setup.dart [platform] [options]');
stderr.writeln('Platform: current host platform (default) or android');
stderr.writeln();
stderr.writeln('Default package targets:');
_allTargets.forEach((p, t) => stderr.writeln(' $p: $t'));
stderr.writeln();
stderr.writeln(parser.usage);
}
Future<int> _package(
String platform,
String env,
String targets,
String rootDir,
String arch, {
String? androidArch,
required bool verbose,
}) async {
final distributorDir = p.join(
rootDir,
'plugins',
'flutter_distributor',
'packages',
'flutter_distributor',
);
final activateResult = await Process.run('dart', [
'pub',
'global',
'activate',
'-s',
'path',
distributorDir,
]);
if (activateResult.exitCode != 0) {
stderr.write(activateResult.stderr);
return activateResult.exitCode;
}
final coreSha256 = platform == 'windows' ? await _buildGoCore(rootDir) : null;
final file = File(p.join(rootDir, 'env.json'));
await file.writeAsString(
jsonEncode({'APP_ENV': env, 'CORE_SHA256': ?coreSha256}),
);
final flutterBuildArgs = createFlutterBuildArgs(
platform: platform,
verbose: verbose,
);
final descriptionArgs = <String>[];
if (platform != 'android') {
descriptionArgs.addAll(['--description', arch]);
}
final depExit = await _ensureDependencies(platform, arch);
if (depExit != 0) return depExit;
final process = await Process.start(
'flutter_distributor',
[
'package',
'--skip-clean',
'--platform',
platform,
'--targets',
targets,
if (androidArch != null)
'--build-target-platform=${_androidFlutterTarget[androidArch]!}',
if (flutterBuildArgs.isNotEmpty)
'--flutter-build-args=${flutterBuildArgs.join(',')}',
...descriptionArgs,
],
includeParentEnvironment: true,
environment: {'ANDROID_ARCH': ?androidArch},
runInShell: Platform.isWindows,
);
process.stdout.listen((data) {
stdout.write(utf8.decode(data));
});
process.stderr.listen((data) {
stderr.write(utf8.decode(data));
});
final exitCode = await process.exitCode;
return exitCode;
}
Future<String?> _buildGoCore(String rootDir) async {
final buildToolDir = p.join(
rootDir,
'plugins',
'setup',
'buildkit',
'build_tool',
);
final result = await Process.run('dart', [
'run',
'build_tool',
'windows',
'--root-dir',
rootDir,
], workingDirectory: buildToolDir);
if (result.exitCode != 0) {
stderr.write(result.stderr);
return null;
}
final shaFile = File(p.join(rootDir, 'core_sha256.json'));
if (!shaFile.existsSync()) return null;
final content =
jsonDecode(shaFile.readAsStringSync()) as Map<String, dynamic>;
return content['CORE_SHA256'] as String?;
}
String _detectArch() {
if (Platform.isWindows) {
final pa = Platform.environment['PROCESSOR_ARCHITECTURE'] ?? 'AMD64';
return pa.toUpperCase() == 'ARM64' ? 'arm64' : 'amd64';
}
final result = Process.runSync('uname', ['-m']);
final machine = (result.stdout as String).trim();
if (machine == 'aarch64') return 'arm64';
if (machine == 'x86_64') return 'amd64';
return machine;
}
Future<bool> _hasCommand(String cmd) async {
final which = Platform.isWindows ? 'where' : 'command';
final args = Platform.isWindows ? [cmd] : ['-v', cmd];
final result = await Process.run(which, args);
return result.exitCode == 0;
}
Future<int> _ensureDependencies(String platform, String arch) async {
switch (platform) {
case 'macos':
return _ensureMacosDependencies();
case 'linux':
return _ensureLinuxDependencies(arch);
default:
return 0;
}
}
Future<int> _ensureMacosDependencies() async {
if (await _hasCommand('appdmg')) {
stdout.writeln('appdmg already installed, skipping.');
return 0;
}
stdout.writeln('Installing appdmg (DMG creator)...');
final result = await Process.run('npm', ['install', '-g', 'appdmg']);
if (result.exitCode != 0) {
stderr.write(result.stderr);
}
return result.exitCode;
}
Future<int> _ensureLinuxDependencies(String arch) async {
final pkgGroups = <List<String>>[
['ninja-build', 'libgtk-3-dev'],
['libayatana-appindicator3-dev'],
['libkeybinder-3.0-dev'],
['locate'],
];
if (arch == 'amd64') {
pkgGroups.addAll([
['rpm', 'patchelf'],
['libfuse2'],
]);
}
final missingGroups = <List<String>>[];
for (final group in pkgGroups) {
final missingPkgs = <String>[];
for (final pkg in group) {
if (!await _isDebianPackageInstalled(pkg)) {
missingPkgs.add(pkg);
}
}
if (missingPkgs.isNotEmpty) {
missingGroups.add(missingPkgs);
}
}
if (missingGroups.isEmpty) {
stdout.writeln('All Linux build dependencies already installed, skipping.');
} else {
stdout.writeln('Updating apt package lists...');
final updateExit = await _runLinuxDependencyCommand([
'apt-get',
'update',
'-y',
]);
if (updateExit != 0) {
stderr.writeln(
'apt-get update exited with $updateExit; continuing and verifying '
'dependency installation directly.',
);
}
for (final missingPkgs in missingGroups) {
stdout.writeln(
'Installing Linux build dependencies: ${missingPkgs.join(', ')}...',
);
final installExit = await _installLinuxPackages(missingPkgs);
if (installExit != 0) return installExit;
}
}
if (arch == 'amd64') {
const appimagetool = '/usr/local/bin/appimagetool';
if (File(appimagetool).existsSync()) {
stdout.writeln('appimagetool already installed, skipping.');
return 0;
}
stdout.writeln('Downloading appimagetool...');
final downloadName = arch == 'amd64' ? 'x86_64' : 'aarch64';
final dlResult = await Process.run('wget', [
'-O',
appimagetool,
'https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$downloadName.AppImage',
]);
if (dlResult.exitCode != 0) {
stderr.write(dlResult.stderr);
return dlResult.exitCode;
}
await Process.run('chmod', ['+x', appimagetool]);
}
return 0;
}
Future<bool> _isDebianPackageInstalled(String pkg) async {
final result = await Process.run('dpkg', ['-s', pkg]);
return result.exitCode == 0 &&
(result.stdout as String).contains('Status: install ok installed');
}
Future<bool> _areDebianPackagesInstalled(List<String> pkgs) async {
for (final pkg in pkgs) {
if (!await _isDebianPackageInstalled(pkg)) {
return false;
}
}
return true;
}
Future<int> _installLinuxPackages(List<String> pkgs) async {
final exitCode = await _runLinuxDependencyCommand([
'apt-get',
'install',
'-y',
...pkgs,
]);
if (exitCode == 0) return 0;
if (await _areDebianPackagesInstalled(pkgs)) {
stderr.writeln(
'apt-get install exited with $exitCode, but all requested packages are '
'installed; continuing.',
);
return 0;
}
return exitCode;
}
Future<int> _runLinuxDependencyCommand(List<String> command) async {
final sudoCommand = [
'env',
'DEBIAN_FRONTEND=noninteractive',
'NEEDRESTART_MODE=a',
...command,
];
stdout.writeln('exec: sudo ${sudoCommand.join(' ')}');
final result = await Process.start('sudo', sudoCommand);
result.stdout.listen((data) {
stdout.write(utf8.decode(data));
});
result.stderr.listen((data) {
stderr.write(utf8.decode(data));
});
final exitCode = await result.exitCode;
if (exitCode != 0) {
stderr.writeln('Linux dependency command failed with exit code $exitCode.');
}
return exitCode;
}