import Foundation @main struct Build {} #if canImport(PackagePlugin) import PackagePlugin extension Build: CommandPlugin { func performCommand(context _: PluginContext, arguments: [String]) throws { try Build.performCommand(arguments: arguments) } } #if canImport(XcodeProjectPlugin) import XcodeProjectPlugin extension Build: XcodeCommandPlugin { func performCommand(context _: XcodePluginContext, arguments: [String]) throws { try Build.performCommand(arguments: arguments) } } #endif #else extension Build { static func main() throws { try performCommand(arguments: Array(CommandLine.arguments.dropFirst())) } } #endif extension Build { static var ffmpegConfiguers = [String]() static func performCommand(arguments: [String]) throws { print(arguments) if arguments.contains("h") || arguments.contains("-h") || arguments.contains("--help") { printHelp() return } if Utility.shell("which brew") == nil { print(""" You need to run the script first /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" """) return } if Utility.shell("which pkg-config") == nil { Utility.shell("brew install pkg-config") } let path = URL.currentDirectory + ".Script" if !FileManager.default.fileExists(atPath: path.path) { try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true) } FileManager.default.changeCurrentDirectoryPath(path.path) var librarys = [Library]() var isFFmpegDebug = false for argument in arguments { if argument == "notRecompile" { BaseBuild.notRecompile = true } else if argument == "gitCloneAll" { BaseBuild.gitCloneAll = true } else if argument == "disableGPL" { BaseBuild.disableGPL = true } else if argument == "enable-debug" { isFFmpegDebug = true } else if argument.hasPrefix("platforms=") { let values = String(argument.suffix(argument.count - "platforms=".count)) let platforms = values.split(separator: ",").compactMap { PlatformType(rawValue: String($0)) } if !platforms.isEmpty { BaseBuild.platforms = platforms } } else if argument.hasPrefix("enable-") { let value = String(argument.suffix(argument.count - "enable-".count)) if let library = Library(rawValue: value) { librarys.append(library) } else { print("argument \(argument) wrong") return } } else if argument.hasPrefix("--"), argument != "--disable-sandbox", argument != "--allow-writing-to-directory" { Build.ffmpegConfiguers.append(argument) } } if isFFmpegDebug { Build.ffmpegConfiguers.append("--enable-debug") Build.ffmpegConfiguers.append("--enable-debug=3") Build.ffmpegConfiguers.append("--disable-stripping") } else { Build.ffmpegConfiguers.append("--disable-debug") Build.ffmpegConfiguers.append("--enable-stripping") } if librarys.isEmpty { librarys.append(contentsOf: [.libshaderc, .vulkan, .lcms2, .libplacebo, .libdav1d, .gmp, .nettle, .gnutls, .readline, .libsmbclient, .libsrt, .libzvbi, .libfreetype, .libfribidi, .libharfbuzz, .libass, .libfontconfig, .libbluray, .FFmpeg, .libmpv]) } if BaseBuild.disableGPL { librarys.removeAll { $0 == .readline || $0 == .libsmbclient } } else { Build.ffmpegConfiguers.append("--enable-gpl") } for library in librarys { try library.build.buildALL() } } static func printHelp() { print(""" Usage: swift package BuildFFmpeg [OPTION]... Default Build: swift package --disable-sandbox BuildFFmpeg enable-libshaderc enable-vulkan enable-lcms2 enable-libdav1d enable-libplacebo enable-gmp enable-nettle enable-gnutls enbale-readline enable-libsmbclient enable-libsrt enable-libzvbi enable-libfreetype enable-libfribidi enable-libharfbuzz enable-libass enable-FFmpeg enable-libmpv Options: h, -h, --help display this help and exit notRecompile If there is a library, then there is no need to recompile gitCloneAll git clone not add --depth 1 enable-debug, build ffmpeg with debug information platforms=xros deployment platform: macos,ios,isimulator,tvos,tvsimulator,xros,xrsimulator,maccatalyst,watchos,watchsimulator --xx add ffmpeg Configuers Libraries: enable-libshaderc build with libshaderc enable-vulkan depend enable-libshaderc enable-libdav1d build with libdav1d enable-libplacebo depend enable-libshaderc enable-vulkan enable-lcms2 enable-libdav1d enable-nettle depend enable-gmp enable-gnutls depend enable-gmp enable-nettle enable-libsmbclient depend enable-gmp enable-nettle enable-gnutls enbale-readline enable-libsrt depend enable-openssl or enable-gnutls enable-libfreetype build with libfreetype enable-libharfbuzz depend enable-libfreetype enable-libass depend enable-libfreetype enable-libfribidi enable-libharfbuzz enable-libfontconfig depend enable-libfreetype enable-libbluray depend enable-libfreetype enable-libfontconfig enable-libzvbi build with libzvbi enable-FFmpeg build with FFmpeg enable-libmpv depend enable-libass enable-FFmpeg enable-openssl build with openssl [no] """) } } enum Library: String, CaseIterable { case libglslang, libshaderc, vulkan, lcms2, libdovi, libdav1d, libplacebo, libfreetype, libharfbuzz, libfribidi, libass, gmp, readline, nettle, gnutls, libsmbclient, libsrt, libzvbi, libfontconfig, libbluray, FFmpeg, libmpv, openssl, libtls, boringssl, libpng, libupnp, libnfs, libsmb2 var version: String { switch self { case .FFmpeg: return "n6.1" case .libfreetype: return "VER-2-13-2" case .libfribidi: return "v1.0.12" case .libharfbuzz: return "5.3.1" case .libass: return "0.17.1-branch" case .libpng: return "v1.6.43" case .libmpv: return "v0.37.0" case .openssl: return "openssl-3.2.1" case .libsrt: return "v1.5.3" case .libsmbclient: return "samba-4.15.13" case .gnutls: return "3.8.3" case .nettle: return "nettle_3.9.1_release_20230601" case .libdav1d: return "1.1.0" case .gmp: return "v6.2.1" case .libtls: return "OPENBSD_7_3" case .libzvbi: return "v0.2.42" case .boringssl: return "master" case .libplacebo: return "v6.338.2" case .vulkan: return "v1.2.8" case .libshaderc: return "v2024.0" case .readline: return "readline-8.2" case .libglslang: return "13.1.1" case .libdovi: return "2.1.0" case .lcms2: return "lcms2.16" case .libupnp: return "release-1.14.18" case .libnfs: return "libnfs-5.0.2" case .libbluray: return "1.3.4" case .libfontconfig: return "2.14.2" case .libsmb2: return "master" } } var url: String { switch self { case .libpng: return "https://github.com/glennrp/libpng" case .libmpv: return "https://github.com/mpv-player/mpv" case .libsrt: return "https://github.com/Haivision/srt" case .libsmbclient: return "https://github.com/samba-team/samba" case .nettle: return "https://git.lysator.liu.se/nettle/nettle" case .gmp: return "https://github.com/alisw/GMP" case .libdav1d: return "https://github.com/videolan/dav1d" case .libtls: return "https://github.com/libressl/portable" case .libzvbi: return "https://github.com/zapping-vbi/zvbi" case .boringssl: return "https://github.com/google/boringssl" case .libplacebo: return "https://github.com/haasn/libplacebo" case .vulkan: return "https://github.com/KhronosGroup/MoltenVK" case .libshaderc: return "https://github.com/google/shaderc" case .readline: return "https://git.savannah.gnu.org/git/readline.git" case .libglslang: return "https://github.com/KhronosGroup/glslang" case .libdovi: return "https://github.com/quietvoid/dovi_tool" case .lcms2: return "https://github.com/mm2/Little-CMS" case .libupnp: return "https://github.com/pupnp/pupnp" case .libnfs: return "https://github.com/sahlberg/libnfs" case .libbluray: return "https://code.videolan.org/videolan/libbluray" case .libfontconfig: return "https://gitlab.freedesktop.org/fontconfig/fontconfig" case .libsmb2: return "https://github.com/sahlberg/libsmb2" default: var value = rawValue if self != .libass, value.hasPrefix("lib") { value = String(value.dropFirst(3)) } return "https://github.com/\(value)/\(value)" } } var isFFmpegDependentLibrary: Bool { switch self { case .vulkan, .libshaderc, .libglslang, .lcms2, .libplacebo, .libdav1d, .gmp, .gnutls, .libsrt, .libzvbi, .libfontconfig, .libbluray: return true case .openssl: return false case .libsmbclient: return !BaseBuild.disableGPL default: return false } } var build: BaseBuild { switch self { case .FFmpeg: return BuildFFMPEG() case .libfreetype: return BuildFreetype() case .libfribidi: return BuildFribidi() case .libharfbuzz: return BuildHarfbuzz() case .libass: return BuildASS() case .libpng: return BuildPng() case .libmpv: return BuildMPV() case .openssl: return BuildOpenSSL() case .libsrt: return BuildSRT() case .libsmbclient: return BuildSmbclient() case .gnutls: return BuildGnutls() case .libdav1d: return BuildDav1d() case .nettle: return BuildNettle() case .gmp: return BuildGmp() case .libtls: return BuildLibreSSL() case .libzvbi: return BuildZvbi() case .boringssl: return BuildBoringSSL() case .libplacebo: return BuildPlacebo() case .vulkan: return BuildVulkan() case .libshaderc: return BuildShaderc() case .libglslang: return BuildGlslang() case .readline: return BuildReadline() case .libdovi: return BuildDovi() case .lcms2: return BuildLittleCms() case .libupnp: return BuildUPnP() case .libnfs: return BuildNFS() case .libfontconfig: return BuildFontconfig() case .libbluray: return BuildBluray() case .libsmb2: return BuildSMB2() } } } class BaseBuild { static var platforms = PlatformType.allCases .filter { ![.watchos, .watchsimulator, .android].contains($0) } static var notRecompile = false static var gitCloneAll = false static var disableGPL = false let library: Library let directoryURL: URL init(library: Library) { self.library = library directoryURL = URL.currentDirectory + "\(library.rawValue)-\(library.version)" if !FileManager.default.fileExists(atPath: directoryURL.path) { var arguments = ["clone", "--recurse-submodules"] if !BaseBuild.gitCloneAll { arguments.append(contentsOf: ["--depth", "1"]) } arguments.append(contentsOf: ["--branch", library.version, library.url, directoryURL.path]) try! Utility.launch(path: "/usr/bin/git", arguments: arguments) } let patch = URL.currentDirectory + "../Plugins/BuildFFmpeg/patch/\(library.rawValue)" if FileManager.default.fileExists(atPath: patch.path) { _ = try? Utility.launch(path: "/usr/bin/git", arguments: ["stash"], currentDirectoryURL: directoryURL) let fileNames = try! FileManager.default.contentsOfDirectory(atPath: patch.path).sorted() for fileName in fileNames { _ = try? Utility.launch(path: "/usr/bin/git", arguments: ["apply", "\((patch + fileName).path)"], currentDirectoryURL: directoryURL) } } } func platforms() -> [PlatformType] { BaseBuild.platforms } func buildALL() throws { for platform in platforms() { for arch in platform.architectures { let prefix = thinDir(platform: platform, arch: arch) if FileManager.default.fileExists(atPath: (prefix + "lib").path), BaseBuild.notRecompile { continue } try? FileManager.default.removeItem(at: prefix) let buildURL = scratch(platform: platform, arch: arch) try? FileManager.default.removeItem(at: buildURL) try? FileManager.default.createDirectory(at: buildURL, withIntermediateDirectories: true, attributes: nil) try build(platform: platform, arch: arch, buildURL: buildURL) } } try createXCFramework() } func build(platform: PlatformType, arch: ArchType, buildURL: URL) throws { try? _ = Utility.launch(path: "/usr/bin/make", arguments: ["clean"], currentDirectoryURL: buildURL) try? _ = Utility.launch(path: "/usr/bin/make", arguments: ["distclean"], currentDirectoryURL: buildURL) let environ = environment(platform: platform, arch: arch) if FileManager.default.fileExists(atPath: (directoryURL + "meson.build").path) { if Utility.shell("which meson") == nil { Utility.shell("brew install meson") } let meson = Utility.shell("which meson", isOutput: true)! let crossFile = createMesonCrossFile(platform: platform, arch: arch) try Utility.launch(path: meson, arguments: ["setup", buildURL.path, "--cross-file=\(crossFile.path)"] + arguments(platform: platform, arch: arch), currentDirectoryURL: directoryURL, environment: environ) try Utility.launch(path: meson, arguments: ["compile", "--clean"], currentDirectoryURL: buildURL, environment: environ) try Utility.launch(path: meson, arguments: ["compile", "--verbose"], currentDirectoryURL: buildURL, environment: environ) try Utility.launch(path: meson, arguments: ["install"], currentDirectoryURL: buildURL, environment: environ) } else if FileManager.default.fileExists(atPath: (directoryURL + wafPath()).path) { let waf = (directoryURL + wafPath()).path try Utility.launch(path: waf, arguments: ["configure"] + arguments(platform: platform, arch: arch), currentDirectoryURL: directoryURL, environment: environ) var arguments = [String]() arguments.append(contentsOf: wafBuildArg()) try Utility.launch(path: waf, arguments: arguments, currentDirectoryURL: directoryURL, environment: environ) arguments = ["install"] arguments.append(contentsOf: wafInstallArg()) try Utility.launch(path: waf, arguments: arguments, currentDirectoryURL: directoryURL, environment: environ) } else { try configure(buildURL: buildURL, environ: environ, platform: platform, arch: arch) try Utility.launch(path: "/usr/bin/make", arguments: ["-j8"], currentDirectoryURL: buildURL, environment: environ) try Utility.launch(path: "/usr/bin/make", arguments: ["-j8", "install"], currentDirectoryURL: buildURL, environment: environ) } } func wafPath() -> String { "waf" } func wafBuildArg() -> [String] { ["build"] } func wafInstallArg() -> [String] { [] } func configure(buildURL: URL, environ: [String: String], platform: PlatformType, arch: ArchType) throws { let makeLists = directoryURL + "CMakeLists.txt" if FileManager.default.fileExists(atPath: makeLists.path) { if Utility.shell("which cmake") == nil { Utility.shell("brew install cmake") } let cmake = Utility.shell("which cmake", isOutput: true)! let thinDirPath = thinDir(platform: platform, arch: arch).path var arguments = [ makeLists.path, "-DCMAKE_VERBOSE_MAKEFILE=0", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_OSX_SYSROOT=\(platform.sdk.lowercased())", "-DCMAKE_OSX_ARCHITECTURES=\(arch.rawValue)", "-DCMAKE_INSTALL_PREFIX=\(thinDirPath)", "-DBUILD_SHARED_LIBS=0", ] arguments.append(contentsOf: self.arguments(platform: platform, arch: arch)) try Utility.launch(path: cmake, arguments: arguments, currentDirectoryURL: buildURL, environment: environ) } else { let configure = directoryURL + "configure" if !FileManager.default.fileExists(atPath: configure.path) { var bootstrap = directoryURL + "bootstrap" if !FileManager.default.fileExists(atPath: bootstrap.path) { bootstrap = directoryURL + ".bootstrap" } if FileManager.default.fileExists(atPath: bootstrap.path) { try Utility.launch(executableURL: bootstrap, arguments: [], currentDirectoryURL: directoryURL, environment: environ) } else { let autogen = directoryURL + "autogen.sh" if FileManager.default.fileExists(atPath: autogen.path) { var environ = environ environ["NOCONFIGURE"] = "1" try Utility.launch(executableURL: autogen, arguments: [], currentDirectoryURL: directoryURL, environment: environ) } } } try Utility.launch(executableURL: configure, arguments: arguments(platform: platform, arch: arch), currentDirectoryURL: buildURL, environment: environ) } } func environment(platform: PlatformType, arch: ArchType) -> [String: String] { let cFlags = cFlags(platform: platform, arch: arch).joined(separator: " ") let ldFlags = ldFlags(platform: platform, arch: arch).joined(separator: " ") let pkgConfigPath = platform.pkgConfigPath(arch: arch) let pkgConfigPathDefault = Utility.shell("pkg-config --variable pc_path pkg-config", isOutput: true)! let path = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:" return [ "LC_CTYPE": "C", "CC": platform.cc, "CXX": platform.cc + "++", // "SDKROOT": platform.sdk.lowercased(), "CURRENT_ARCH": arch.rawValue, "CFLAGS": cFlags, // makefile can't use CPPFLAGS // "CPPFLAGS": cFlags, // 这个要加,不然cmake在编译maccatalyst 会有问题 "CXXFLAGS": cFlags, "LDFLAGS": ldFlags, // "PKG_CONFIG_PATH": pkgConfigPath, "PKG_CONFIG_LIBDIR": pkgConfigPath + pkgConfigPathDefault, "PATH": path, ] } func cFlags(platform: PlatformType, arch: ArchType) -> [String] { var cFlags = platform.cFlags(arch: arch) let librarys = flagsDependencelibrarys() for library in librarys { let path = thinDir(library: library, platform: platform, arch: arch) if FileManager.default.fileExists(atPath: path.path) { if library == .libsmbclient { cFlags.append("-I\(path.path)/include/samba-4.0") } else { cFlags.append("-I\(path.path)/include") } } } return cFlags } func ldFlags(platform: PlatformType, arch: ArchType) -> [String] { var ldFlags = platform.ldFlags(arch: arch) let librarys = flagsDependencelibrarys() for library in librarys { let path = thinDir(library: library, platform: platform, arch: arch) if FileManager.default.fileExists(atPath: path.path) { var libname = library.rawValue if libname.hasPrefix("lib") { libname = String(libname.dropFirst(3)) } ldFlags.append("-L\(path.path)/lib") ldFlags.append("-l\(libname)") if library == .nettle { ldFlags.append("-lhogweed") } else if library == .gnutls { ldFlags.append(contentsOf: ["-framework", "Security", "-framework", "CoreFoundation"]) } else if library == .libsmbclient { ldFlags.append(contentsOf: ["-lresolv", "-lpthread", "-lz", "-liconv"]) } } } return ldFlags } func flagsDependencelibrarys() -> [Library] { [] } func arguments(platform _: PlatformType, arch _: ArchType) -> [String] { [] } func frameworks() throws -> [String] { [library.rawValue] } private func createXCFramework() throws { let frameworks = try frameworks() for framework in frameworks { var arguments = ["-create-xcframework"] for platform in PlatformType.allCases { if let frameworkPath = try createFramework(framework: framework, platform: platform) { if isFramework { arguments.append("-framework") arguments.append(frameworkPath) } else { arguments.append("-library") arguments.append(frameworkPath + "/" + framework + ".a") arguments.append("-headers") arguments.append(frameworkPath + "/Headers") } } } arguments.append("-output") let XCFrameworkFile = URL.currentDirectory + ["../Sources", framework + ".xcframework"] arguments.append(XCFrameworkFile.path) if FileManager.default.fileExists(atPath: XCFrameworkFile.path) { try FileManager.default.removeItem(at: XCFrameworkFile) } try Utility.launch(path: "/usr/bin/xcodebuild", arguments: arguments) } } private func createFramework(framework: String, platform: PlatformType) throws -> String? { let frameworkDir = URL.currentDirectory + [library.rawValue, platform.rawValue, "\(framework).framework"] if !platforms().contains(platform) { if FileManager.default.fileExists(atPath: frameworkDir.path) { return frameworkDir.path } else { return nil } } try? FileManager.default.removeItem(at: frameworkDir) try FileManager.default.createDirectory(at: frameworkDir, withIntermediateDirectories: true, attributes: nil) var arguments = ["-create"] for arch in platform.architectures { let prefix = thinDir(platform: platform, arch: arch) if !FileManager.default.fileExists(atPath: prefix.path) { return nil } let libname = framework.hasPrefix("lib") || framework.hasPrefix("Lib") ? framework : "lib" + framework var libPath = prefix + ["lib", "\(libname).a"] if !FileManager.default.fileExists(atPath: libPath.path) { libPath = prefix + ["lib", "\(libname).dylib"] } arguments.append(libPath.path) var headerURL: URL = prefix + "include" + framework if !FileManager.default.fileExists(atPath: headerURL.path) { headerURL = prefix + "include" } try? FileManager.default.copyItem(at: headerURL, to: frameworkDir + "Headers") } arguments.append("-output") var output = (frameworkDir + framework).path if !isFramework { output += ".a" } arguments.append(output) try Utility.launch(path: "/usr/bin/lipo", arguments: arguments) try FileManager.default.createDirectory(at: frameworkDir + "Modules", withIntermediateDirectories: true, attributes: nil) var modulemap = """ framework module \(framework) [system] { umbrella "." """ for header in frameworkExcludeHeaders(framework) { modulemap += """ exclude header "\(header).h" """ } modulemap += """ export * } """ FileManager.default.createFile(atPath: frameworkDir.path + "/Modules/module.modulemap", contents: modulemap.data(using: .utf8), attributes: nil) createPlist(path: frameworkDir.path + "/Info.plist", name: framework, minVersion: platform.minVersion, platform: platform.sdk) return frameworkDir.path } var isFramework: Bool { true } func thinDir(library: Library, platform: PlatformType, arch: ArchType) -> URL { URL.currentDirectory + [library.rawValue, platform.rawValue, "thin", arch.rawValue] } func thinDir(platform: PlatformType, arch: ArchType) -> URL { thinDir(library: library, platform: platform, arch: arch) } func scratch(platform: PlatformType, arch: ArchType) -> URL { URL.currentDirectory + [library.rawValue, platform.rawValue, "scratch", arch.rawValue] } func frameworkExcludeHeaders(_: String) -> [String] { [] } private func createPlist(path: String, name: String, minVersion: String, platform: String) { let identifier = "com.kintan.ksplayer." + name let content = """ CFBundleDevelopmentRegion en CFBundleExecutable \(name) CFBundleIdentifier \(identifier) CFBundleInfoDictionaryVersion 6.0 CFBundleName \(name) CFBundlePackageType FMWK CFBundleShortVersionString 87.88.520 CFBundleVersion 87.88.520 CFBundleSignature ???? MinimumOSVersion \(minVersion) CFBundleSupportedPlatforms \(platform) NSPrincipalClass """ FileManager.default.createFile(atPath: path, contents: content.data(using: .utf8), attributes: nil) } private func createMesonCrossFile(platform: PlatformType, arch: ArchType) -> URL { let url = scratch(platform: platform, arch: arch) let crossFile = url + "crossFile.meson" let prefix = thinDir(platform: platform, arch: arch) let cFlags = cFlags(platform: platform, arch: arch).map { "'" + $0 + "'" }.joined(separator: ", ") let ldFlags = ldFlags(platform: platform, arch: arch).map { "'" + $0 + "'" }.joined(separator: ", ") let content = """ [binaries] c = '/usr/bin/clang' cpp = '/usr/bin/clang++' objc = '/usr/bin/clang' objcpp = '/usr/bin/clang++' ar = '\(platform.xcrunFind(tool: "ar"))' strip = '\(platform.xcrunFind(tool: "strip"))' pkgconfig = 'pkg-config' [properties] has_function_printf = true has_function_hfkerhisadf = false [host_machine] system = 'darwin' subsystem = '\(platform.mesonSubSystem)' kernel = 'xnu' cpu_family = '\(arch.cpuFamily)' cpu = '\(arch.targetCpu)' endian = 'little' [built-in options] default_library = 'static' buildtype = 'release' prefix = '\(prefix.path)' c_args = [\(cFlags)] cpp_args = [\(cFlags)] objc_args = [\(cFlags)] objcpp_args = [\(cFlags)] c_link_args = [\(ldFlags)] cpp_link_args = [\(ldFlags)] objc_link_args = [\(ldFlags)] objcpp_link_args = [\(ldFlags)] """ FileManager.default.createFile(atPath: crossFile.path, contents: content.data(using: .utf8), attributes: nil) return crossFile } } enum PlatformType: String, CaseIterable { case macos, ios, isimulator, tvos, tvsimulator, xros, xrsimulator, maccatalyst, watchos, watchsimulator, android var minVersion: String { switch self { case .ios, .isimulator: return "13.0" case .tvos, .tvsimulator: return "13.0" case .macos: return "10.15" case .maccatalyst: return "14.0" case .watchos, .watchsimulator: return "6.0" case .xros, .xrsimulator: return "1.0" case .android: return "24" } } var name: String { switch self { case .ios, .tvos, .macos, .android: return rawValue case .tvsimulator: return "tvossim" case .isimulator: return "iossim" case .maccatalyst: return "maccat" case .watchos: return "watchos" case .watchsimulator: return "watchossim" case .xros: return "visionos" case .xrsimulator: return "visionossim" } } var frameworkName: String { switch self { case .ios: return "ios-arm64" case .maccatalyst: return "ios-arm64_x86_64-maccatalyst" case .isimulator: return "ios-arm64_x86_64-simulator" case .macos: return "macos-arm64_x86_64" case .tvos: return "tvos-arm64_arm64e" case .tvsimulator: return "tvos-arm64_x86_64-simulator" case .watchos: return "watchos-arm64" case .watchsimulator: return "watchossim" case .xros: return "xros-arm64" case .xrsimulator: return "xros-arm64_x86_64-simulator" case .android: return "android" } } var architectures: [ArchType] { switch self { case .ios, .xros, .watchos, .android: return [.arm64] case .tvos: return [.arm64, .arm64e] case .isimulator, .tvsimulator, .watchsimulator: return [.arm64, .x86_64] case .xrsimulator: return [.arm64] case .macos: #if arch(x86_64) return [.x86_64, .arm64] #else return [.arm64, .x86_64] #endif case .maccatalyst: return [.arm64, .x86_64] } } var mesonSubSystem: String { switch self { case .isimulator: return "ios-simulator" case .tvsimulator: return "tvos-simulator" case .xrsimulator: return "xros-simulator" case .watchsimulator: return "watchos-simulator" default: return rawValue } } var cc: String { if self == .android { return androidToolchainPath + "/bin/aarch64-linux-android\(minVersion)-clang" } else { return "/usr/bin/clang" } } func host(arch: ArchType) -> String { switch self { case .macos: return "\(arch.targetCpu)-apple-darwin" case .ios, .tvos, .watchos, .xros: return "\(arch.targetCpu)-\(rawValue)-darwin" case .isimulator, .maccatalyst: return PlatformType.ios.host(arch: arch) case .tvsimulator: return PlatformType.tvos.host(arch: arch) case .watchsimulator: return PlatformType.watchos.host(arch: arch) case .xrsimulator: return PlatformType.xros.host(arch: arch) case .android: return "aarch64-linux-android" } } func deploymentTarget(arch: ArchType) -> String { switch self { case .ios, .tvos, .watchos, .macos, .xros: return "\(arch.targetCpu)-apple-\(rawValue)\(minVersion)" case .maccatalyst: return "\(arch.targetCpu)-apple-ios\(minVersion)-macabi" case .isimulator: return PlatformType.ios.deploymentTarget(arch: arch) + "-simulator" case .tvsimulator: return PlatformType.tvos.deploymentTarget(arch: arch) + "-simulator" case .watchsimulator: return PlatformType.watchos.deploymentTarget(arch: arch) + "-simulator" case .xrsimulator: return PlatformType.xros.deploymentTarget(arch: arch) + "-simulator" case .android: return "" } } private var osVersionMin: String { switch self { case .ios, .tvos, .watchos: return "-m\(rawValue)-version-min=\(minVersion)" case .macos: return "-mmacosx-version-min=\(minVersion)" case .isimulator: return "-mios-simulator-version-min=\(minVersion)" case .tvsimulator: return "-mtvos-simulator-version-min=\(minVersion)" case .watchsimulator: return "-mwatchos-simulator-version-min=\(minVersion)" case .maccatalyst, .xros, .xrsimulator, .android: return "" } } var sdk: String { switch self { case .ios: return "iPhoneOS" case .isimulator: return "iPhoneSimulator" case .tvos: return "AppleTVOS" case .tvsimulator: return "AppleTVSimulator" case .watchos: return "WatchOS" case .watchsimulator: return "WatchSimulator" case .xros: return "XROS" case .xrsimulator: return "XRSimulator" case .macos, .maccatalyst: return "MacOSX" case .android: return "" } } func ldFlags(arch: ArchType) -> [String] { // ldFlags的关键参数要跟cFlags保持一致,不然会在ld的时候不通过。 if self == .android { return [ // "-march=armv8-a", // "-L\(toolchainPath)/sysroot/usr/lib/\(host(arch: arch))/\(minVersion)", // "-L\(toolchainPath)/lib", ] } let isysroot = isysroot var ldFlags = ["-arch", arch.rawValue, "-isysroot", isysroot, "-target", deploymentTarget(arch: arch)] if self == .maccatalyst { ldFlags.append("-iframework") ldFlags.append("\(isysroot)/System/iOSSupport/System/Library/Frameworks") } return ldFlags } func cFlags(arch: ArchType) -> [String] { var cflags = ldFlags(arch: arch) cflags.append(osVersionMin) // 不能同时有强符合和弱符号出现 cflags.append("-fno-common") // if self == .android { // cflags.append("-fstrict-aliasing") // cflags.append("-DANDROID_NDK") // cflags.append("-fPIC") // cflags.append("-DANDROID") // } return cflags } var isysroot: String { if self == .android { return androidToolchainPath + "/sysroot" } return xcrunFind(tool: "--show-sdk-path") } var androidToolchainPath: String { let root = ProcessInfo.processInfo.environment["ANDROID_NDK_HOME"] ?? "" // let toolchain = "darwin-arm64" let toolchain = "darwin-x86_64" let toolchainPath = "\(root)/toolchains/llvm/prebuilt/\(toolchain)" return toolchainPath } func xcrunFind(tool: String) -> String { try! Utility.launch(path: "/usr/bin/xcrun", arguments: ["--sdk", sdk.lowercased(), "--find", tool], isOutput: true) } func pkgConfigPath(arch: ArchType) -> String { var pkgConfigPath = "" for lib in Library.allCases { let path = URL.currentDirectory + [lib.rawValue, rawValue, "thin", arch.rawValue] if FileManager.default.fileExists(atPath: path.path) { pkgConfigPath += "\(path.path)/lib/pkgconfig:" } } return pkgConfigPath } } enum ArchType: String, CaseIterable { // swiftlint:disable identifier_name // arm64e 还没ABI。所以第三方库是无法使用的。 case arm64, x86_64, arm64e // swiftlint:enable identifier_name var executable: Bool { guard let architecture = Bundle.main.executableArchitectures?.first?.intValue else { return false } // NSBundleExecutableArchitectureARM64 if architecture == 0x0100_000C, self == .arm64 || self == .arm64e { return true } else if architecture == NSBundleExecutableArchitectureX86_64, self == .x86_64 { return true } return false } var cpuFamily: String { switch self { case .arm64, .arm64e: return "aarch64" case .x86_64: return "x86_64" } } var targetCpu: String { switch self { case .arm64, .arm64e: return "arm64" case .x86_64: return "x86_64" } } } enum Utility { @discardableResult static func shell(_ command: String, isOutput: Bool = false, currentDirectoryURL: URL? = nil, environment: [String: String] = [:]) -> String? { do { return try launch(executableURL: URL(fileURLWithPath: "/bin/zsh"), arguments: ["-c", command], isOutput: isOutput, currentDirectoryURL: currentDirectoryURL, environment: environment) } catch { print(error.localizedDescription) return nil } } @discardableResult static func launch(path: String, arguments: [String], isOutput: Bool = false, currentDirectoryURL: URL? = nil, environment: [String: String] = [:]) throws -> String { try launch(executableURL: URL(fileURLWithPath: path), arguments: arguments, isOutput: isOutput, currentDirectoryURL: currentDirectoryURL, environment: environment) } @discardableResult static func launch(executableURL: URL, arguments: [String], isOutput: Bool = false, currentDirectoryURL: URL? = nil, environment: [String: String] = [:]) throws -> String { #if os(macOS) let task = Process() var environment = environment if environment["PATH"] == nil { environment["PATH"] = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin" } task.environment = environment var standardOutput: FileHandle? var log = executableURL.path + " " + arguments.joined(separator: " ") + "\n environment: " + environment.description if isOutput { let pipe = Pipe() task.standardOutput = pipe standardOutput = pipe.fileHandleForReading } else if var logURL = currentDirectoryURL { logURL = logURL.appendingPathExtension("log") log += " logFile: \(logURL)" if !FileManager.default.fileExists(atPath: logURL.path) { FileManager.default.createFile(atPath: logURL.path, contents: nil) } let standardOutput = try FileHandle(forWritingTo: logURL) if #available(macOS 10.15.4, *) { try standardOutput.seekToEnd() } task.standardOutput = standardOutput } print(log) task.arguments = arguments task.currentDirectoryURL = currentDirectoryURL task.executableURL = executableURL try task.run() task.waitUntilExit() if task.terminationStatus == 0 { if isOutput, let standardOutput { let data = standardOutput.readDataToEndOfFile() let result = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .newlines) ?? "" print(result) return result } else { return "" } } else { throw NSError(domain: "fail", code: Int(task.terminationStatus)) } #else return "" #endif } } extension URL { static var currentDirectory: URL { URL(fileURLWithPath: FileManager.default.currentDirectoryPath) } static func + (left: URL, right: String) -> URL { var url = left url.appendPathComponent(right) return url } static func + (left: URL, right: [String]) -> URL { var url = left for item in right { url.appendPathComponent(item) } return url } } class BuildUPnP: BaseBuild { init() { super.init(library: .libupnp) } } class BuildNFS: BaseBuild { override var isFramework: Bool { false } init() { super.init(library: .libnfs) } } class BuildSMB2: BaseBuild { override var isFramework: Bool { false } init() { super.init(library: .libsmb2) } }