// ignore_for_file: avoid_print import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:path/path.dart'; enum Target { windows, linux, android, macos, } extension TargetExt on Target { String get os { if (this == Target.macos) { return "darwin"; } return name; } bool get same { if (this == Target.android) { return true; } if (Platform.isWindows && this == Target.windows) { return true; } if (Platform.isLinux && this == Target.linux) { return true; } if (Platform.isMacOS && this == Target.macos) { return true; } return false; } String get dynamicLibExtensionName { final String extensionName; switch (this) { case Target.android || Target.linux: extensionName = ".so"; break; case Target.windows: extensionName = ".dll"; break; case Target.macos: extensionName = ".dylib"; break; } return extensionName; } String get executableExtensionName { final String extensionName; switch (this) { case Target.windows: extensionName = ".exe"; break; default: extensionName = ""; break; } return extensionName; } } enum Mode { core, lib } enum Arch { amd64, arm64, arm } class BuildItem { Target target; Arch? arch; String? archName; BuildItem({ required this.target, this.arch, this.archName, }); @override String toString() { return 'BuildLibItem{target: $target, arch: $arch, archName: $archName}'; } } class Build { static List get buildItems => [ BuildItem( target: Target.macos, arch: Arch.arm64, ), BuildItem( target: Target.macos, arch: Arch.amd64, ), BuildItem( target: Target.linux, arch: Arch.arm64, ), BuildItem( target: Target.linux, arch: Arch.amd64, ), BuildItem( target: Target.windows, arch: Arch.amd64, ), BuildItem( target: Target.windows, arch: Arch.arm64, ), BuildItem( target: Target.android, arch: Arch.arm, archName: 'armeabi-v7a', ), BuildItem( target: Target.android, arch: Arch.arm64, archName: 'arm64-v8a', ), BuildItem( target: Target.android, arch: Arch.amd64, archName: 'x86_64', ), ]; static String get appName => "FlClash"; static String get coreName => "FlClashCore"; static String get libName => "libclash"; static String get outDir => join(current, libName); static String get _coreDir => join(current, "core"); static String get _servicesDir => join(current, "services", "helper"); static String get distPath => join(current, "dist"); static String _getCc(BuildItem buildItem) { final environment = Platform.environment; if (buildItem.target == Target.android) { final ndk = environment["ANDROID_NDK"]; assert(ndk != null); final prebuiltDir = Directory(join(ndk!, "toolchains", "llvm", "prebuilt")); final prebuiltDirList = prebuiltDir.listSync(); final map = { "armeabi-v7a": "armv7a-linux-androideabi21-clang", "arm64-v8a": "aarch64-linux-android21-clang", "x86": "i686-linux-android21-clang", "x86_64": "x86_64-linux-android21-clang" }; return join( prebuiltDirList.first.path, "bin", map[buildItem.archName], ); } return "gcc"; } static get tags => "with_gvisor"; static Future exec( List executable, { String? name, Map? environment, String? workingDirectory, bool runInShell = true, }) async { if (name != null) print("run $name"); final process = await Process.start( executable[0], executable.sublist(1), environment: environment, workingDirectory: workingDirectory, runInShell: runInShell, ); process.stdout.listen((data) { print(utf8.decode(data)); }); process.stderr.listen((data) { print(utf8.decode(data)); }); final exitCode = await process.exitCode; if (exitCode != 0 && name != null) throw "$name error"; } static buildCore({ required Mode mode, required Target target, Arch? arch, }) async { final isLib = mode == Mode.lib; final items = buildItems.where( (element) { return element.target == target && (arch == null ? true : element.arch == arch); }, ).toList(); for (final item in items) { final outFileDir = join( outDir, item.target.name, item.archName, ); final file = File(outFileDir); if (file.existsSync()) { file.deleteSync(recursive: true); } final fileName = isLib ? "$libName${item.target.dynamicLibExtensionName}" : "$coreName${item.target.executableExtensionName}"; final outPath = join( outFileDir, fileName, ); final Map env = {}; env["GOOS"] = item.target.os; if (item.arch != null) { env["GOARCH"] = item.arch!.name; } if (isLib) { env["CGO_ENABLED"] = "1"; env["CC"] = _getCc(item); env["CFLAGS"] = "-O3 -Werror"; } else { env["CGO_ENABLED"] = "0"; } final execLines = [ "go", "build", "-ldflags=-w -s", "-tags=$tags", if (isLib) "-buildmode=c-shared", "-o", outPath, ]; await exec( execLines, name: "build core", environment: env, workingDirectory: _coreDir, ); } } static buildHelper(Target target) async { await exec( [ "cargo", "build", "--release", "--features", "windows-service", ], name: "build helper", workingDirectory: _servicesDir, ); final outPath = join( _servicesDir, "target", "release", "helper${target.executableExtensionName}", ); final targetPath = join(outDir, target.name, "FlClashHelperService${target.executableExtensionName}"); await File(outPath).copy(targetPath); } static List getExecutable(String command) { print(command); return command.split(" "); } static getDistributor() async { final distributorDir = join( current, "plugins", "flutter_distributor", "packages", "flutter_distributor", ); await exec( name: "clean distributor", Build.getExecutable("flutter clean"), workingDirectory: distributorDir, ); await exec( name: "upgrade distributor", Build.getExecutable("flutter pub upgrade"), workingDirectory: distributorDir, ); await exec( name: "get distributor", Build.getExecutable("dart pub global activate -s path $distributorDir"), ); } static copyFile(String sourceFilePath, String destinationFilePath) { final sourceFile = File(sourceFilePath); if (!sourceFile.existsSync()) { throw "SourceFilePath not exists"; } final destinationFile = File(destinationFilePath); final destinationDirectory = destinationFile.parent; if (!destinationDirectory.existsSync()) { destinationDirectory.createSync(recursive: true); } try { sourceFile.copySync(destinationFilePath); print("File copied successfully!"); } catch (e) { print("Failed to copy file: $e"); } } } class BuildCommand extends Command { Target target; BuildCommand({ required this.target, }) { if (target == Target.android || target == Target.linux) { argParser.addOption( "arch", valueHelp: arches.map((e) => e.name).join(','), help: 'The $name build desc', ); } else { argParser.addOption( "arch", help: 'The $name build archName', ); } argParser.addOption( "out", valueHelp: [ if (target.same) "app", "core", ].join(','), help: 'The $name build arch', ); } @override String get description => "build $name application"; @override String get name => target.name; List get arches => Build.buildItems .where((element) => element.target == target && element.arch != null) .map((e) => e.arch!) .toList(); _getLinuxDependencies(Arch arch) async { await Build.exec( Build.getExecutable("sudo apt update -y"), ); await Build.exec( Build.getExecutable("sudo apt install -y ninja-build libgtk-3-dev"), ); await Build.exec( Build.getExecutable("sudo apt install -y libayatana-appindicator3-dev"), ); await Build.exec( Build.getExecutable("sudo apt-get install -y libkeybinder-3.0-dev"), ); await Build.exec( Build.getExecutable("sudo apt install -y locate"), ); if (arch == Arch.amd64) { await Build.exec( Build.getExecutable("sudo apt install -y rpm patchelf"), ); await Build.exec( Build.getExecutable("sudo apt install -y libfuse2"), ); final downloadName = arch == Arch.amd64 ? "x86_64" : "aarch_64"; await Build.exec( Build.getExecutable( "wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$downloadName.AppImage", ), ); await Build.exec( Build.getExecutable( "chmod +x appimagetool", ), ); } await Build.exec( Build.getExecutable( "sudo mv appimagetool /usr/local/bin/", ), ); } _getMacosDependencies() async { await Build.exec( Build.getExecutable("npm install -g appdmg"), ); } _buildDistributor({ required Target target, required String targets, String args = '', }) async { await Build.getDistributor(); await Build.exec( name: name, Build.getExecutable( "flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose $args", ), ); } Future get systemArch async { if (Platform.isWindows) { return Platform.environment["PROCESSOR_ARCHITECTURE"]; } else if (Platform.isLinux || Platform.isMacOS) { final result = await Process.run('uname', ['-m']); return result.stdout.toString().trim(); } return null; } @override Future run() async { final mode = target == Target.android ? Mode.lib : Mode.core; final String out = argResults?["out"] ?? (target.same ? "app" : "core"); final archName = argResults?["arch"]; final currentArches = arches.where((element) => element.name == archName).toList(); final arch = currentArches.isEmpty ? null : currentArches.first; if (arch == null && target != Target.android) { throw "Invalid arch parameter"; } await Build.buildCore( target: target, arch: arch, mode: mode, ); if (target == Target.windows) { await Build.buildHelper(target); } if (out != "app") { return; } switch (target) { case Target.windows: _buildDistributor( target: target, targets: "exe,zip", args: "--description $archName", ); return; case Target.linux: final targetMap = { Arch.arm64: "linux-arm64", Arch.amd64: "linux-x64", }; final targets = [ "deb", if (arch == Arch.amd64) ...[ "appimage", "rpm", ], ].join(","); final defaultTarget = targetMap[arch]; await _getLinuxDependencies(arch!); _buildDistributor( target: target, targets: targets, args: "--description $archName --build-target-platform $defaultTarget", ); return; case Target.android: final targetMap = { Arch.arm: "android-arm", Arch.arm64: "android-arm64", Arch.amd64: "android-x64", }; final defaultArches = [Arch.arm, Arch.arm64, Arch.amd64]; final defaultTargets = defaultArches .where((element) => arch == null ? true : element == arch) .map((e) => targetMap[e]) .toList(); _buildDistributor( target: target, targets: "apk", args: "--flutter-build-args split-per-abi --build-target-platform ${defaultTargets.join(",")}", ); return; case Target.macos: await _getMacosDependencies(); _buildDistributor( target: target, targets: "dmg", args: "--description $archName", ); return; } } } main(args) async { final runner = CommandRunner("setup", "build Application"); runner.addCommand(BuildCommand(target: Target.android)); runner.addCommand(BuildCommand(target: Target.linux)); runner.addCommand(BuildCommand(target: Target.windows)); runner.addCommand(BuildCommand(target: Target.macos)); runner.run(args); }