Files
MWClash/setup.dart
chen08209 676f2d058a Add windows server mode start process verify
Add linux deb dependencies

Add backup recovery strategy select

Support custom text scaling

Optimize the display of different text scale

Optimize windows setup experience

Optimize startTun performance

Optimize android tv experience

Optimize default option

Optimize computed text size

Optimize hyperOS freeform window

Add developer mode

Update core

Optimize more details
2025-05-01 00:02:29 +08:00

574 lines
14 KiB
Dart
Executable File

// ignore_for_file: avoid_print
import 'dart:convert';
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:path/path.dart';
import 'package:crypto/crypto.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<BuildItem> 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<void> exec(
List<String> executable, {
String? name,
Map<String, String>? 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 Future<String> calcSha256(String filePath) async {
final file = File(filePath);
if (!await file.exists()) {
throw "File not exists";
}
final stream = file.openRead();
return sha256.convert(await stream.reduce((a, b) => a + b)).toString();
}
static Future<List<String>> 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();
final List<String> corePaths = [];
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,
);
corePaths.add(outPath);
final Map<String, String> 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,
);
}
return corePaths;
}
static buildHelper(Target target, String token) async {
await exec(
[
"cargo",
"build",
"--release",
"--features",
"windows-service",
],
environment: {
"TOKEN": token,
},
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<String> getExecutable(String 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',
);
argParser.addOption(
"env",
valueHelp: [
"pre",
"stable",
].join(','),
help: 'The $name build env',
);
}
@override
String get description => "build $name application";
@override
String get name => target.name;
List<Arch> 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" : "aarch64";
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 = '',
required String env,
}) 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 --build-dart-define=APP_ENV=$env",
),
);
}
Future<String?> 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<void> 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 env = argResults?["env"] ?? "pre";
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";
}
final corePaths = await Build.buildCore(
target: target,
arch: arch,
mode: mode,
);
if (out != "app") {
return;
}
switch (target) {
case Target.windows:
final token = target != Target.android
? await Build.calcSha256(corePaths.first)
: null;
Build.buildHelper(target, token!);
_buildDistributor(
target: target,
targets: "exe,zip",
args:
" --description $archName --build-dart-define=CORE_SHA256=$token",
env: env,
);
return;
case Target.linux:
final targetMap = {
Arch.arm64: "linux-arm64",
Arch.amd64: "linux-x64",
};
final targets = [
"deb",
if (arch == Arch.amd64) "appimage",
if (arch == Arch.amd64) "rpm",
].join(",");
final defaultTarget = targetMap[arch];
await _getLinuxDependencies(arch!);
_buildDistributor(
target: target,
targets: targets,
args:
" --description $archName --build-target-platform $defaultTarget",
env: env,
);
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:
",split-per-abi --build-target-platform ${defaultTargets.join(",")}",
env: env,
);
return;
case Target.macos:
await _getMacosDependencies();
_buildDistributor(
target: target,
targets: "dmg",
args: " --description $archName",
env: env,
);
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);
}