import Foundation enum Build { static func performCommand(_ options: ArgumentOptions) throws { 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") } if Utility.shell("which wget") == nil { Utility.shell("brew install wget") } let path = URL.currentDirectory + "dist" if !FileManager.default.fileExists(atPath: path.path) { try? FileManager.default.createDirectory(at: path, withIntermediateDirectories: false, attributes: nil) } try? Utility.removeFiles(extensions: [".swift"], currentDirectoryURL: URL.currentDirectory + ["dist", "release"]) FileManager.default.changeCurrentDirectoryPath(path.path) BaseBuild.options = options if !options.platforms.isEmpty { BaseBuild.platforms = options.platforms } } } class ArgumentOptions { private let arguments: [String] var enableDebug: Bool = false var enableSplitPlatform: Bool = false var enableGPL: Bool = false var platforms : [PlatformType] = [] var releaseVersion: String = "0.0.0" init() { self.arguments = [] } init(arguments: [String]) { self.arguments = arguments } func contains(_ argument: String) -> Bool { return self.arguments.firstIndex(of: argument) != nil } static func parse(_ arguments: [String]) throws -> ArgumentOptions { let options = ArgumentOptions(arguments: Array(arguments.dropFirst())) for argument in arguments { switch argument { case "enable-debug": options.enableDebug = true case "enable-gpl": options.enableGPL = true case "enable-split-platform": options.enableSplitPlatform = true default: if argument.hasPrefix("version=") { let version = String(argument.suffix(argument.count - "version=".count)) options.releaseVersion = version } if argument.hasPrefix("platform=") { let values = String(argument.suffix(argument.count - "platform=".count)) for val in values.split(separator: ",") { let platformStr = val.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() switch platformStr { case "ios": options.platforms += [PlatformType.ios, PlatformType.isimulator] case "tvos": options.platforms += [PlatformType.tvos, PlatformType.tvsimulator] case "xros": options.platforms += [PlatformType.xros, PlatformType.xrsimulator] default: guard let other = PlatformType(rawValue: platformStr) else { throw NSError(domain: "unknown platform: \(val)", code: 1) } if !options.platforms.contains(other) { options.platforms += [other] } } } } } } return options } } class BaseBuild { static let defaultPath = "/Library/Frameworks/Python.framework/Versions/Current/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" static var platforms = PlatformType.allCases static var options = ArgumentOptions() static let splitPlatformGroups = [ PlatformType.macos.rawValue: [PlatformType.macos, PlatformType.maccatalyst], PlatformType.ios.rawValue: [PlatformType.ios, PlatformType.isimulator], PlatformType.tvos.rawValue: [PlatformType.tvos, PlatformType.tvsimulator], PlatformType.xros.rawValue: [PlatformType.xros, PlatformType.xrsimulator] ] let library: Library let directoryURL: URL let xcframeworkDirectoryURL: URL var pullLatestVersion = false; init(library: Library) { self.library = library directoryURL = URL.currentDirectory + "\(library.rawValue)-\(library.version)" xcframeworkDirectoryURL = URL.currentDirectory + ["release", "xcframework"] } func beforeBuild() throws { if FileManager.default.fileExists(atPath: directoryURL.path) { return } // pull code from git if pullLatestVersion { try! Utility.launch(path: "/usr/bin/git", arguments: ["-c", "advice.detachedHead=false", "clone", "--recursive", "--depth", "1", library.url, directoryURL.path]) } else { try! Utility.launch(path: "/usr/bin/git", arguments: ["-c", "advice.detachedHead=false", "clone", "--recursive", "--depth", "1", "--branch", library.version, library.url, directoryURL.path]) } // apply patch let patch = URL.currentDirectory + "../Sources/BuildScripts/patch/\(library.rawValue)" if FileManager.default.fileExists(atPath: patch.path) { _ = try? Utility.launch(path: "/usr/bin/git", arguments: ["checkout", "."], currentDirectoryURL: directoryURL) let fileNames = try! FileManager.default.contentsOfDirectory(atPath: patch.path).sorted() for fileName in fileNames { if !fileName.hasSuffix(".patch") { continue } try! Utility.launch(path: "/usr/bin/git", arguments: ["apply", "\((patch + fileName).path)"], currentDirectoryURL: directoryURL) } } } func buildALL() throws { try beforeBuild() try? FileManager.default.removeItem(at: URL.currentDirectory + library.rawValue) try? FileManager.default.removeItem(at: directoryURL.appendingPathExtension("log")) for platform in BaseBuild.platforms { for arch in architectures(platform) { try build(platform: platform, arch: arch) } } try createXCFramework() try packageRelease() try afterBuild() } func afterBuild() throws { try generatePackageManagerFile() } func architectures(_ platform: PlatformType) -> [ArchType] { platform.architectures } func platforms() -> [PlatformType] { BaseBuild.platforms } func build(platform: PlatformType, arch: ArchType) throws { let buildURL = scratch(platform: platform, arch: arch) try? FileManager.default.createDirectory(at: buildURL, withIntermediateDirectories: true, attributes: nil) 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") } if Utility.shell("which ninja") == nil { Utility.shell("brew install ninja") } let crossFile = createMesonCrossFile(platform: platform, arch: arch) let meson = Utility.shell("which meson", isOutput: true)! 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) try Utility.launch(path: waf, arguments: wafBuildArg(), currentDirectoryURL: directoryURL, environment: environ) try Utility.launch(path: waf, arguments: ["install"] + wafInstallArg(), 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 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) } 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_SYSTEM_NAME=\(platform.cmakeSystemName)", "-DCMAKE_SYSTEM_PROCESSOR=\(arch.rawValue)", "-DCMAKE_INSTALL_PREFIX=\(thinDirPath)", "-DBUILD_SHARED_LIBS=0", "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ] 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) } } var arguments = [ "--prefix=\(thinDir(platform: platform, arch: arch).path)", ] arguments.append(contentsOf: self.arguments(platform: platform, arch: arch)) try Utility.launch(executableURL: configure, arguments: arguments, 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)! return [ "LC_CTYPE": "C", "CC": "/usr/bin/clang", "CXX": "/usr/bin/clang++", // "SDKROOT": platform.sdk.lowercased(), "CURRENT_ARCH": arch.rawValue, "CFLAGS": cFlags, // makefile can't use CPPFLAGS "CPPFLAGS": cFlags, // 这个要加,不然cmake在编译maccatalyst 会有问题 "CXXFLAGS": cFlags, "ASMFLAGS": cFlags, "LDFLAGS": ldFlags, "PKG_CONFIG_LIBDIR": pkgConfigPath + pkgConfigPathDefault, "PATH": BaseBuild.defaultPath, ] } func cFlags(platform: PlatformType, arch: ArchType) -> [String] { var cFlags = platform.cFlags(arch: arch) let librarys = flagsDependencelibrarys() for library in librarys { let path = URL.currentDirectory + [library.rawValue, platform.rawValue, "thin", arch.rawValue] if FileManager.default.fileExists(atPath: path.path) { 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 = URL.currentDirectory + [library.rawValue, platform.rawValue, "thin", arch.rawValue] 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)") } } return ldFlags } func flagsDependencelibrarys() -> [Library] { [] } func arguments(platform: PlatformType, arch: ArchType) -> [String] { return [] } func frameworks() throws -> [String] { [library.rawValue] } func createXCFramework() throws { // clean all old xcframework try? Utility.removeFiles(extensions: [".xcframework"], currentDirectoryURL: self.xcframeworkDirectoryURL) var frameworks: [String] = [] let libNames = try self.frameworks() for libName in libNames { if libName.hasPrefix("lib") { frameworks.append("Lib" + libName.dropFirst(3)) } else { frameworks.append(libName) } } for framework in frameworks { var frameworkGenerated = [PlatformType: String]() for platform in BaseBuild.platforms { if let frameworkPath = try createFramework(framework: framework, platform: platform) { frameworkGenerated[platform] = frameworkPath } } try buildXCFramework(name: framework, paths: Array(frameworkGenerated.values)) // Generate xcframework for different platforms if BaseBuild.options.enableSplitPlatform { for (group, platforms) in BaseBuild.splitPlatformGroups { var frameworkPaths: [String] = [] for platform in platforms { if let frameworkPath = frameworkGenerated[platform] { frameworkPaths.append(frameworkPath) } } try buildXCFramework(name: "\(framework)-\(group)", paths: frameworkPaths) } } } } private func buildXCFramework(name: String, paths: [String]) throws { if paths.isEmpty { return } var arguments = ["-create-xcframework"] for frameworkPath in paths { arguments.append("-framework") arguments.append(frameworkPath) } arguments.append("-output") let XCFrameworkFile = self.xcframeworkDirectoryURL + [name + ".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) } func createFramework(framework: String, platform: PlatformType) throws -> String? { let platformDir = URL.currentDirectory + [library.rawValue, platform.rawValue] if !FileManager.default.fileExists(atPath: platformDir.path) { return nil } 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") arguments.append((frameworkDir + framework).path) 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 "." """ frameworkExcludeHeaders(framework).forEach { header in modulemap += """ exclude header "\(header).h" """ } modulemap += """ export * } """ FileManager.default.createFile(atPath: frameworkDir.path + "/Modules/module.modulemap", contents: modulemap.data(using: .utf8), attributes: nil) // Setting the minimum version to 100.0 is required for uploading a static framework to the App Store after Xcode 15.4 // Fix: ITMS-90208: "Invalid Bundle. The bundle xxx.framework does not support the minimum OS Version specified in the Info.plist." // It was originally using `platform.minVersion` createPlist(path: frameworkDir.path + "/Info.plist", name: framework, minVersion: "100.0", platform: platform.sdk) try fixShallowBundles(framework: framework, platform: platform, frameworkDir: frameworkDir) return frameworkDir.path } // Fix shallow bundles for Xcode 26, only for macOS frameworks func fixShallowBundles(framework: String, platform: PlatformType, frameworkDir: URL) throws { guard platform == .macos else { return } let infoPlistPath = frameworkDir + "Info.plist" let versionsPath = frameworkDir + "Versions" // Check if this is a shallow bundle that needs fixing var isDirectory: ObjCBool = false let frameworkExists = FileManager.default.fileExists(atPath: frameworkDir.path, isDirectory: &isDirectory) let hasInfoPlist = FileManager.default.fileExists(atPath: infoPlistPath.path) let hasVersions = FileManager.default.fileExists(atPath: versionsPath.path, isDirectory: &isDirectory) && isDirectory.boolValue if frameworkExists && hasInfoPlist && !hasVersions { print("Fixing \(framework).framework bundle structure...") // Create proper bundle structure let versionAResourcesPath = frameworkDir + ["Versions", "A", "Resources"] try FileManager.default.createDirectory(at: versionAResourcesPath, withIntermediateDirectories: true, attributes: nil) // Move Info.plist to proper location let newInfoPlistPath = versionAResourcesPath + "Info.plist" try FileManager.default.moveItem(at: infoPlistPath, to: newInfoPlistPath) // Move framework binary to proper location let binaryPath = frameworkDir + framework let newBinaryPath = frameworkDir + ["Versions", "A", framework] if FileManager.default.fileExists(atPath: binaryPath.path) { try FileManager.default.moveItem(at: binaryPath, to: newBinaryPath) } // Move LICENSE if exists let licensePath = frameworkDir + "LICENSE" if FileManager.default.fileExists(atPath: licensePath.path) { let newLicensePath = frameworkDir + ["Versions", "A", "LICENSE"] try FileManager.default.moveItem(at: licensePath, to: newLicensePath) } // Create symbolic links let currentLinkPath = frameworkDir + ["Versions", "Current"] try? FileManager.default.removeItem(at: currentLinkPath) try FileManager.default.createSymbolicLink(atPath: currentLinkPath.path, withDestinationPath: "A") let binaryLinkPath = frameworkDir + framework try? FileManager.default.removeItem(at: binaryLinkPath) try FileManager.default.createSymbolicLink(atPath: binaryLinkPath.path, withDestinationPath: "Versions/Current/\(framework)") let resourcesLinkPath = frameworkDir + "Resources" try? FileManager.default.removeItem(at: resourcesLinkPath) try FileManager.default.createSymbolicLink(atPath: resourcesLinkPath.path, withDestinationPath: "Versions/Current/Resources") print("\(framework).framework structure fixed") } } 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.mpvkit." + normalizeBundleIdentifier(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) } // CFBundleIdentifier must contain only alphanumerics(a-z), dots(.), hyphens(-) private func normalizeBundleIdentifier(_ identifier: String) -> String { return identifier.replacingOccurrences(of: "_", with: "-") } 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"))' pkg-config = '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 } func packageRelease() throws { let releaseDirPath = URL.currentDirectory + ["release"] if !FileManager.default.fileExists(atPath: releaseDirPath.path) { try? FileManager.default.createDirectory(at: releaseDirPath, withIntermediateDirectories: true, attributes: nil) } let releaseLibPath = releaseDirPath + [library.rawValue] try? FileManager.default.removeItem(at: releaseLibPath) // copy static libraries for platform in BaseBuild.platforms { for arch in architectures(platform) { let thinLibPath = thinDir(platform: platform, arch: arch) + ["lib"] if !FileManager.default.fileExists(atPath: thinLibPath.path) { continue } let staticLibraries = try FileManager.default.contentsOfDirectory(atPath: thinLibPath.path).filter { $0.hasSuffix(".a") } let releaseThinLibPath = releaseDirPath + [library.rawValue, "lib", platform.rawValue, "thin", arch.rawValue, "lib"] try? FileManager.default.createDirectory(at: releaseThinLibPath, withIntermediateDirectories: true, attributes: nil) for lib in staticLibraries { let sourceURL = thinLibPath + [lib] let destinationURL = releaseThinLibPath + [lib] try FileManager.default.copyItem(at: sourceURL, to: destinationURL) } } } // copy includes guard let firstPlatform = getFirstSuccessPlatform() else { return } let firstArch = architectures(firstPlatform).first! let includePath = thinDir(platform: firstPlatform, arch: firstArch) + ["include"] let destIncludePath = releaseDirPath + [library.rawValue, "include"] try FileManager.default.copyItem(at: includePath, to: destIncludePath) // copy pkg-config file example try packagePkgConfigRelease() // zip build artifacts when there are frameworks to generate if try self.frameworks().count > 0 { let sourceLib = releaseDirPath + [library.rawValue] let destZipLibPath = releaseDirPath + [library.rawValue + "-all.zip"] try? FileManager.default.removeItem(at: destZipLibPath) try Utility.launch(path: "/usr/bin/zip", arguments: ["-qr", destZipLibPath.path, "./"], currentDirectoryURL: sourceLib) } // zip xcframeworks var frameworks: [String] = [] let libNames = try self.frameworks() for libName in libNames { if libName.hasPrefix("lib") { frameworks.append("Lib" + libName.dropFirst(3)) } else { frameworks.append(libName) } } for framework in frameworks { // clean old zip files try? FileManager.default.removeItem(at: releaseDirPath + [framework + ".xcframework.zip"]) try? FileManager.default.removeItem(at: releaseDirPath + [framework + ".xcframework.checksum.txt"]) let XCFrameworkFile = framework + ".xcframework" let zipFile = releaseDirPath + [framework + ".xcframework.zip"] let checksumFile = releaseDirPath + [framework + ".xcframework.checksum.txt"] try Utility.launch(path: "/usr/bin/zip", arguments: ["-qry", zipFile.path, XCFrameworkFile], currentDirectoryURL: self.xcframeworkDirectoryURL) Utility.shell("swift package compute-checksum \(zipFile.path) > \(checksumFile.path)") if BaseBuild.options.enableSplitPlatform { for group in BaseBuild.splitPlatformGroups.keys { let XCFrameworkName = "\(framework)-\(group)" // clean old zip files try? FileManager.default.removeItem(at: releaseDirPath + [XCFrameworkName + ".xcframework.zip"]) try? FileManager.default.removeItem(at: releaseDirPath + [XCFrameworkName + ".xcframework.checksum.txt"]) let XCFrameworkFile = XCFrameworkName + ".xcframework" let XCFrameworkPath = self.xcframeworkDirectoryURL + ["\(framework)-\(group).xcframework"] if FileManager.default.fileExists(atPath: XCFrameworkPath.path) { let zipFile = releaseDirPath + [XCFrameworkName + ".xcframework.zip"] let checksumFile = releaseDirPath + [XCFrameworkName + ".xcframework.checksum.txt"] try Utility.launch(path: "/usr/bin/zip", arguments: ["-qry", zipFile.path, XCFrameworkFile], currentDirectoryURL: self.xcframeworkDirectoryURL) Utility.shell("swift package compute-checksum \(zipFile.path) > \(checksumFile.path)") } } } } } func packagePkgConfigRelease() throws { let releaseDirPath = URL.currentDirectory + ["release"] // copy pkg-config file example for platform in BaseBuild.platforms { for arch in architectures(platform) { let thinLibPath = thinDir(platform: platform, arch: arch) + ["lib"] let pkgconfigPath = thinLibPath + ["pkgconfig"] if !FileManager.default.fileExists(atPath: pkgconfigPath.path) { continue } let destPkgConfigDir = releaseDirPath + [library.rawValue, "pkgconfig-example", platform.rawValue] let destPkgConfigPath = destPkgConfigDir + arch.rawValue try? FileManager.default.createDirectory(at: destPkgConfigDir, withIntermediateDirectories: true, attributes: nil) try FileManager.default.copyItem(at: pkgconfigPath, to: destPkgConfigPath) let pkgconfigFiles = Utility.listAllFiles(in: destPkgConfigPath) for file in pkgconfigFiles { if let data = FileManager.default.contents(atPath: file.path), var str = String(data: data, encoding: .utf8) { str = str.replacingOccurrences(of: URL.currentDirectory.path, with: "/path/to/workdir") try! str.write(toFile: file.path, atomically: true, encoding: .utf8) } } } } } func generatePackageManagerFile() throws { let releaseDirPath = URL.currentDirectory + ["release"] let template = URL.currentDirectory + ["../docs/Package.template.swift"] let packageFile = releaseDirPath + "Package.swift" if !FileManager.default.fileExists(atPath: packageFile.path) { try! FileManager.default.createDirectory(at: releaseDirPath, withIntermediateDirectories: true, attributes: nil) try! FileManager.default.copyItem(at: template, to: packageFile) } var dependencyTargetContent = "" if self is ZipBaseBuild { for target in library.targets { let tmpChecksum = FileManager.default.temporaryDirectory + "\(library.rawValue)_checksum.txt" if FileManager.default.fileExists(atPath: tmpChecksum.path) { try? FileManager.default.removeItem(at: tmpChecksum) } try! Utility.launch(path: "wget", arguments: ["-q", "-O", tmpChecksum.path, target.checksum], currentDirectoryURL: FileManager.default.temporaryDirectory) let checksum = try String(contentsOf: tmpChecksum, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) dependencyTargetContent += """ .binaryTarget( name: "\(target.name)", url: "\(target.url)", checksum: "\(checksum)" ), """ try? FileManager.default.removeItem(at: tmpChecksum) } } else { for target in library.targets { let checksumFile = releaseDirPath + [target.name + ".xcframework.checksum.txt"] let checksum = try String(contentsOf: checksumFile, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) dependencyTargetContent += """ .binaryTarget( name: "\(target.name)", url: "\(target.url)", checksum: "\(checksum)" ), """ } } if dependencyTargetContent.isEmpty { return } if let data = FileManager.default.contents(atPath: packageFile.path), var str = String(data: data, encoding: .utf8) { let placeholderChars = "//AUTO_GENERATE_TARGETS_END//" str = str.replacingOccurrences(of: """ \(placeholderChars) """, with: """ \(dependencyTargetContent) \(placeholderChars) """) try! str.write(toFile: packageFile.path, atomically: true, encoding: .utf8) } } func getFirstSuccessPlatform() -> PlatformType? { for platform in BaseBuild.platforms { let firstArch = architectures(platform).first! let thinPath = thinDir(platform: platform, arch: firstArch) if FileManager.default.fileExists(atPath: thinPath.path) { return platform } } return nil } } class CombineBaseBuild : BaseBuild { func combineFrameworkName() -> String { "\(library.rawValue)-combined.a" } func combineFrameworks(platform: PlatformType, arch: ArchType) -> [String] { let thinLibPath = thinDir(platform: platform, arch: arch) + ["lib"] let staticLibraries = try? FileManager.default.contentsOfDirectory(atPath: thinLibPath.path).filter { $0.hasSuffix(".a") } guard let staticLibraries = staticLibraries else { return [] } // order by create date descending let sortedFrameworks = staticLibraries.sorted { let file1Path = thinLibPath + [$0] let file2Path = thinLibPath + [$1] let attr1 = try? FileManager.default.attributesOfItem(atPath: file1Path.path) let attr2 = try? FileManager.default.attributesOfItem(atPath: file2Path.path) let date1 = attr1?[FileAttributeKey.creationDate] as? Date ?? Date.distantPast let date2 = attr2?[FileAttributeKey.creationDate] as? Date ?? Date.distantPast return date1 < date2 } return sortedFrameworks } override func frameworks() throws -> [String] { ["\(library.rawValue)-combined"] } override func build(platform: PlatformType, arch: ArchType) throws { try super.build(platform: platform, arch: arch) try combineStaticLibraries(platform: platform, arch: arch) } func combineStaticLibraries(platform: PlatformType, arch: ArchType) throws { let frameworks = self.combineFrameworks(platform: platform, arch: arch) if frameworks.isEmpty { return } print("Create combine static libraries...") let thinLibPath = thinDir(platform: platform, arch: arch) + ["lib"] var combinedLibName = combineFrameworkName() if !combinedLibName.hasSuffix(".a") { combinedLibName += ".a" } var paths: [String] = [] let prefix = thinDir(platform: platform, arch: arch) if !FileManager.default.fileExists(atPath: prefix.path) { throw NSError(domain: "no build for \(platform.rawValue) \(arch.rawValue)", code: 1) } for framework in frameworks { let libname = framework.hasPrefix("lib") || framework.hasPrefix("Lib") ? framework : "lib" + framework let libPath = prefix + ["lib", libname] if !FileManager.default.fileExists(atPath: libPath.path) { throw NSError(domain: "no library \(libPath.path) for \(platform.rawValue) \(arch.rawValue)", code: 1) } paths.append(libPath.path) } let outputPath = prefix + ["lib", combinedLibName] var arguments = ["-static"] arguments.append(contentsOf: ["-o", outputPath.path]) for frameworkPath in paths { arguments.append(frameworkPath) } if FileManager.default.fileExists(atPath: outputPath.path) { try? FileManager.default.removeItem(at: outputPath) } try Utility.launch(path: "/usr/bin/libtool", arguments: arguments) // move old static libraries to origin directory let backupDirectory = thinLibPath + ["bak"] try? FileManager.default.createDirectory(at: backupDirectory, withIntermediateDirectories: true, attributes: nil) for framework in frameworks { let libname = framework.hasPrefix("lib") || framework.hasPrefix("Lib") ? framework : "lib" + framework let libPath = prefix + ["lib", libname] let backupLibPath = backupDirectory + [libname] try? FileManager.default.moveItem(at: libPath, to: backupLibPath) } // create combine pkgconfig let pkgconfigPath = thinLibPath + ["pkgconfig", "\(library.rawValue).pc"] if !FileManager.default.fileExists(atPath: pkgconfigPath.path) { throw NSError(domain: "no pkgconfig \(pkgconfigPath.path) for \(platform.rawValue) \(arch.rawValue)", code: 1) } var content = try String(contentsOf: pkgconfigPath) let combinedLibname = combinedLibName.hasPrefix("lib") ? String(combinedLibName.dropFirst(3).dropLast(2)) : String(combinedLibName.dropLast(2)) content = content.replacingOccurrences( of: "-L\\$\\{libdir\\}((\\s+-l\\S+)+)", with: "-L${libdir} -l\(combinedLibname)", options: .regularExpression ) // move old pkgconfig to origin directory let backupPkgconfigPath = backupDirectory + [pkgconfigPath.lastPathComponent] try? FileManager.default.moveItem(at: pkgconfigPath, to: backupPkgconfigPath) // replace with combined pkgconfig FileManager.default.createFile(atPath: pkgconfigPath.path, contents: content.data(using: .utf8), attributes: nil) } } class ZipBaseBuild : BaseBuild { override func beforeBuild() throws { // unzip builded static library let outputFileName = "\(library.rawValue).zip" let outputFile = directoryURL + outputFileName // delete invalid downloaded files let attributes = try? FileManager.default.attributesOfItem(atPath: outputFile.path) if let fileSize = attributes?[FileAttributeKey.size] as? UInt64, fileSize <= 0 { try? FileManager.default.removeItem(atPath: directoryURL.path) } try! FileManager.default.createDirectory(atPath: directoryURL.path, withIntermediateDirectories: true, attributes: nil) if !FileManager.default.fileExists(atPath: outputFile.path) { try! Utility.launch(path: "wget", arguments: ["-O", outputFileName, library.url], currentDirectoryURL: directoryURL) try! Utility.launch(path: "/usr/bin/unzip", arguments: ["-o",outputFileName], currentDirectoryURL: directoryURL) } } override func buildALL() throws { try beforeBuild() try? FileManager.default.removeItem(at: URL.currentDirectory + library.rawValue) try? FileManager.default.removeItem(at: directoryURL.appendingPathExtension("log")) try? FileManager.default.createDirectory(atPath: (URL.currentDirectory + library.rawValue).path, withIntermediateDirectories: true, attributes: nil) for platform in BaseBuild.platforms { for arch in architectures(platform) { // restore lib let srcThinLibPath = directoryURL + ["lib"] + [platform.rawValue, "thin", arch.rawValue, "lib"] // ignore if platform not support if !FileManager.default.fileExists(atPath: srcThinLibPath.path) { continue } let destThinPath = thinDir(platform: platform, arch: arch) let destThinLibPath = destThinPath + ["lib"] try? FileManager.default.createDirectory(atPath: destThinPath.path, withIntermediateDirectories: true, attributes: nil) try? FileManager.default.copyItem(at: srcThinLibPath, to: destThinLibPath) // restore include let srcIncludePath = directoryURL + ["include"] let destIncludePath = destThinPath + ["include"] try? FileManager.default.copyItem(at: srcIncludePath, to: destIncludePath) // restore pkgconfig let srcPkgConfigPath = directoryURL + ["pkgconfig-example", platform.rawValue, arch.rawValue] let destPkgConfigPath = destThinPath + ["lib", "pkgconfig"] try? FileManager.default.copyItem(at: srcPkgConfigPath, to: destPkgConfigPath) Utility.listAllFiles(in: destPkgConfigPath).forEach { file in if let data = FileManager.default.contents(atPath: file.path), var str = String(data: data, encoding: .utf8) { str = str.replacingOccurrences(of: "/path/to/workdir", with: URL.currentDirectory.path) try! str.write(toFile: file.path, atomically: true, encoding: .utf8) } } } } try afterBuild() } override func afterBuild() throws { try super.afterBuild() } } class PackageTarget { let name: String let url : String let checksum: String init(name: String, url : String, checksum: String) { self.name = name self.url = url self.checksum = checksum } static func target( name: String, url : String, checksum: String ) -> PackageTarget { return PackageTarget(name: name, url: url, checksum: checksum) } } enum PlatformType: String, CaseIterable { case xros, xrsimulator, maccatalyst, macos, isimulator, tvsimulator, tvos, ios var minVersion: String { switch self { case .ios, .isimulator: return "14.0" case .tvos, .tvsimulator: return "14.0" case .macos: return "11.0" case .maccatalyst: // return "14.0" return "" case .xros, .xrsimulator: return "1.0" } } var name: String { switch self { case .ios, .tvos, .macos: return rawValue case .tvsimulator: return "tvossim" case .isimulator: return "iossim" case .maccatalyst: return "maccat" 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: // 保持和xcode一致:https://github.com/KhronosGroup/MoltenVK/issues/431#issuecomment-771137085 return "tvos-arm64_arm64e" case .tvsimulator: return "tvos-arm64_x86_64-simulator" case .xros: return "xros-arm64" case .xrsimulator: return "xros-arm64_x86_64-simulator" } } // xcodebuild default ARCHS = "$(ARCHS_STANDARD_64_BIT)" only build arm64e for tvos var architectures: [ArchType] { switch self { case .ios, .xros: return [.arm64] case .tvos: return [.arm64, .arm64e] case .xrsimulator: return [.arm64] case .isimulator, .tvsimulator: return [.arm64, .x86_64] case .macos: // macos 不能用arm64,不然打包release包会报错,不能通过 #if arch(x86_64) return [.x86_64, .arm64] #else return [.arm64, .x86_64] #endif case .maccatalyst: return [.arm64, .x86_64] } } func deploymentTarget(_ arch: ArchType) -> String { switch self { case .ios, .tvos, .macos, .xros: return "\(arch.targetCpu)-apple-\(rawValue)\(minVersion)" case .maccatalyst: return "\(arch.targetCpu)-apple-ios-macabi" case .isimulator: return PlatformType.ios.deploymentTarget(arch) + "-simulator" case .tvsimulator: return PlatformType.tvos.deploymentTarget(arch) + "-simulator" case .xrsimulator: return PlatformType.xros.deploymentTarget(arch) + "-simulator" } } private var osVersionMin: String { switch self { case .ios, .tvos: 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 .maccatalyst, .xros, .xrsimulator: return "" // return "-miphoneos-version-min=\(minVersion)" } } var sdk : String { switch self { case .ios: return "iPhoneOS" case .isimulator: return "iPhoneSimulator" case .tvos: return "AppleTVOS" case .tvsimulator: return "AppleTVSimulator" case .macos: return "MacOSX" case .maccatalyst: return "MacOSX" case .xros: return "XROS" case .xrsimulator: return "XRSimulator" } } var isysroot: String { xcrunFind(tool: "--show-sdk-path") } var mesonSubSystem: String { switch self { case .isimulator: return "ios-simulator" case .tvsimulator: return "tvos-simulator" case .xrsimulator: return "xros-simulator" default: return rawValue } } var cmakeSystemName: String { switch self { case .ios, .isimulator: return "iOS" case .tvos, .tvsimulator: return "tvOS" case .macos, .maccatalyst: return "Darwin" case .xros, .xrsimulator: return "visionOS" } } func host(arch: ArchType) -> String { switch self { case .ios, .isimulator, .maccatalyst: return "\(arch == .x86_64 ? "x86_64" : "arm64")-ios-darwin" case .tvos, .tvsimulator: return "\(arch == .x86_64 ? "x86_64" : "arm64")-tvos-darwin" case .xros, .xrsimulator: return "\(arch == .x86_64 ? "x86_64" : "arm64")-xros-darwin" case .macos: return "\(arch == .x86_64 ? "x86_64" : "arm64")-apple-darwin" } } func ldFlags(arch: ArchType) -> [String] { // ldFlags的关键参数要跟cFlags保持一致,不然会在ld的时候不通过。 var flags = ["-lc++", "-arch", arch.rawValue, "-isysroot", isysroot, "-target", deploymentTarget(arch), osVersionMin] // maccatalyst的vulkan库需要加载UIKit框架 if self == .maccatalyst { flags += ["-iframework", "\(isysroot)/System/iOSSupport/System/Library/Frameworks"] } return flags } func cFlags(arch: ArchType) -> [String] { var cflags = ["-arch", arch.rawValue, "-isysroot", isysroot, "-target", deploymentTarget(arch), osVersionMin] // if self == .macos || self == .maccatalyst { // 不能同时有强符合和弱符号出现 // cflags.append("-fno-common") // } if self == .tvos || self == .tvsimulator { cflags.append("-DHAVE_FORK=0") } return cflags } 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 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 { 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" } } static var hostArch : ArchType { #if arch(arm64) return .arm64 #else return .x86_64 #endif } } 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/bash"), 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 { if !path.hasPrefix("/") { let execPath = Utility.shell("which \(path)", isOutput: true)! if execPath.isEmpty { throw NSError(domain: "[\(path)] not found", code: 1) } return try launch(executableURL: URL(fileURLWithPath: execPath), arguments: arguments, isOutput: isOutput, currentDirectoryURL: currentDirectoryURL, environment: environment) } else { return 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 { let task = Process() var environment = environment // for homebrew 1.12 if ProcessInfo.processInfo.environment.keys.contains("HOME") { environment["HOME"] = ProcessInfo.processInfo.environment["HOME"] } if !environment.keys.contains("PATH") { environment["PATH"] = BaseBuild.defaultPath } task.environment = environment var outputFileHandle: FileHandle? var logURL: URL? var outputBuffer = Data() let outputPipe = Pipe() let errorPipe = Pipe() task.standardOutput = outputPipe task.standardError = errorPipe if let curURL = currentDirectoryURL { // output to file logURL = curURL.appendingPathExtension("log") if !FileManager.default.fileExists(atPath: logURL!.path) { FileManager.default.createFile(atPath: logURL!.path, contents: nil) } outputFileHandle = try FileHandle(forWritingTo: logURL!) outputFileHandle?.seekToEndOfFile() } outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in let data = fileHandle.availableData if !data.isEmpty { outputBuffer.append(data) if let outputString = String(data: data, encoding: .utf8) { if isOutput { print(outputString.trimmingCharacters(in: .newlines)) } // Write to file simultaneously. outputFileHandle?.write(data) } } else { // Close the read capability processing program and clean up resources. fileHandle.readabilityHandler = nil fileHandle.closeFile() } } errorPipe.fileHandleForReading.readabilityHandler = { fileHandle in let data = fileHandle.availableData if !data.isEmpty { if let outputString = String(data: data, encoding: .utf8) { print(outputString.trimmingCharacters(in: .newlines)) // Write to file simultaneously. outputFileHandle?.write(data) } } else { // Close the read capability processing program and clean up resources. fileHandle.readabilityHandler = nil fileHandle.closeFile() } } task.arguments = arguments var log = executableURL.path + " " + arguments.joined(separator: " ") + " environment: " + environment.description if let currentDirectoryURL { log += " url: \(currentDirectoryURL)" } print(log) outputFileHandle?.write("\(log)\n".data(using: .utf8)!) task.currentDirectoryURL = currentDirectoryURL task.executableURL = executableURL try task.run() task.waitUntilExit() if task.terminationStatus == 0 { if isOutput { let result = String(data: outputBuffer, encoding: .utf8)?.trimmingCharacters(in: .newlines) ?? "" return result } else { return "" } } else { if let logURL = logURL { // print log when run in GitHub Action if ProcessInfo.processInfo.environment.keys.contains("GITHUB_ACTION") { // if build FFmpeg failed, print the ffbuild/config.log content if logURL.path.contains("FFmpeg") { let ffbuildLogURL = logURL .deletingPathExtension() .appendingPathComponent("ffbuild/config.log") if FileManager.default.fileExists(atPath: ffbuildLogURL.path) { if let content = String(data: try Data(contentsOf: ffbuildLogURL), encoding: .utf8) { print("############# \(ffbuildLogURL) CONTENT BEGIN #############") print(content) print("############# \(ffbuildLogURL) CONTENT END #############") } } } if let content = String(data: try Data(contentsOf: logURL), encoding: .utf8) { print("############# \(logURL) CONTENT BEGIN #############") print(content) print("############# \(logURL) CONTENT END #############") if #available(macOS 13.0, *) { let regErrLogPath = try Regex("A full log can be found at\\s+?(/.*\\.txt)") if let firstMatch = content.firstMatch(of: regErrLogPath) { let errPath = "\(firstMatch[1].value ?? "")" if !errPath.isEmpty { print("############# \(errPath) CONTENT BEGIN #############") let content = Utility.shell("cat \(errPath)", isOutput: true) print(content ?? "") print("############# \(errPath) CONTENT END #############") } } } } } print("please view log file for detail: \(logURL)\n") } throw NSError(domain: "\(executableURL.lastPathComponent) execute failed", code: Int(task.terminationStatus)) } } @discardableResult static func listAllFiles(in directory: URL) -> [URL] { var allFiles: [URL] = [] let enumerator = FileManager.default.enumerator(atPath: directory.path) while let file = enumerator?.nextObject() as? String { let filePath = directory + [file] var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: filePath.path, isDirectory: &isDirectory) { if isDirectory.boolValue { // 如果是目录,则递归遍历该目录 listAllFiles(in: filePath) } else { allFiles.append(filePath) } } } return allFiles } static func removeFiles(extensions: [String], currentDirectoryURL: URL) throws { for ext in extensions { let directoryContents = try FileManager.default.contentsOfDirectory(atPath: currentDirectoryURL.path) for item in directoryContents { if item.hasSuffix(ext) { try FileManager.default.removeItem(at: currentDirectoryURL.appendingPathComponent(item)) } } } } } 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 right.forEach { url.appendPathComponent($0) } return url } }