用Swift写一个自动打包ipa,并上传蒲公英

4,546 阅读4分钟
在项目中看到以前同事写的自动打包并上传蒲公英脚本,就萌发了用原生swift或者OC可不可以编写脚本的想法。查阅相关资料后发现是可行的。

1、Process是一个可以执行终端命令的类

我们给Process扩展一个便捷方法执行终端命令

extension Process {
    
    /// 执行命令
    /// - Parameters:
    ///   - launchPath: 命令路径
    ///   - arguments: 命令参数
    ///   - currentDirectoryPath: 命令执行目录
    ///   - environment: 环境变量
    /// - Returns: 返回执行结果
    static func executable(launchPath:String,
                           arguments:[String],
                           currentDirectoryPath:String? = nil,
                           environment:[String:String]? = nil)->Pipe{
        let process = Process()
        process.launchPath = launchPath
        process.arguments = arguments
        if let environment = environment {
            process.environment = environment
        }
        if let currentDirectoryPath = currentDirectoryPath {
            process.currentDirectoryPath = currentDirectoryPath
        }
        let pipe = Pipe()
        process.standardOutput = pipe
        process.launch()
        return pipe
    }
}

2、xcodebuild命令

  1. Clean

    xcodebuild clean
               -workspace <workspaceName>
               -scheme <schemeName>
               -configuration <Debug|Release>
    
  2. Archive

    xcodebuild archive 
               -archivePath <archivePath>
               -project <projectName>
               -workspace <workspaceName>
               -scheme <schemeName> 
               -configuration <Debug|Release>
    
  3. Export

    xcodebuild -exportArchive
               -archivePath <xcarchivepath>
               -exportPath <destinationpath>
               -exportOptionsPlist <plistpath>
    

参考文章:xcodebuild命令介绍

3、使用 SPM 搭建开发或者直接使用xcode的创建一个命令行程序项目

$ mkdir SwiftCommandLineTool
$ cd SwiftCommandLineTool
$ swift package init --type executable

最后一行的 type executable 参数将告诉 SPM,我们想创建一个 CLI,而不是一个 Framework。

  1. 封装一个打包上传相关的工具

    extension String{
        func appPath(_ value:String) -> String {
            if self.hasSuffix("/") {
                return self + value
            }
            return self + "/" + value
        }
    }
    struct IpaTool {
        
        struct Output {
            var pipe:Pipe
            var readData:String
            init(pipe:Pipe) {
                self.pipe = pipe
                self.readData = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: String.Encoding.utf8) ?? ""
            }
        }
        enum Configuration:String {
            case debug = "Debug"
            case release = "Release"
        }
        
        var workspace:String{
            projectPath.appPath("\(projectName).xcworkspace")
        }
        ///scheme
        var scheme:String
        ///Debug|Release
        var configuration:Configuration
        ///编译产物路径
        var xcarchive:String{
            exportIpaPath.appPath("\(projectName).xcarchive")
        }
        ///配置文件路径
        var exportOptionsPlist:String
        ///导出ipa包的路径
        var exportIpaPath:String
        ///项目路径
        let projectPath:String
        ///项目名称
        let projectName:String
        ///存放打包目录
        let packageDirectory:String
        ///蒲公英_api_key
        let pgyerKey:String
        
        ///
        /// - Parameters:
        ///   - projectPath: 项目路径
        ///   - configuration: Debug|Release
        ///   - exportOptionsPlist: 配置文件Plist的路径
        ///   - pgyerKey: 上传蒲公英的key
        /// - Throws: 抛出错误
        init(projectPath:String,
          configuration:Configuration,
          exportOptionsPlist:String,
          pgyerKey:String) throws {
         self.projectPath = projectPath
         self.configuration = configuration
         self.exportOptionsPlist = exportOptionsPlist
         self.pgyerKey = pgyerKey
         let manager = FileManager.default
         var allFiles = try manager.contentsOfDirectory(atPath: projectPath)
         projectName = allFiles.first(where: { $0.hasSuffix(".xcodeproj")  })?.components(separatedBy: ".").first ?? ""
         packageDirectory = NSHomeDirectory()
             .appPath("Desktop/\(projectName)_ipa")
         
         allFiles = try manager.contentsOfDirectory(atPath: projectPath.appPath("\(projectName).xcodeproj/xcshareddata/xcschemes")
         )
         scheme = allFiles[0].components(separatedBy: ".").first ?? ""
         let formatter = DateFormatter()
         formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
         exportIpaPath = packageDirectory.appPath(formatter.string(from: Date()))
     }
    }
    
  2. 封装执行clean,archive,exportArchive

    extension IpaTool{
        /// 执行 xcodebuild clean
        func clean()->Output{
            let arguments = ["clean",
                             "-workspace",
                             workspace,
                             "-scheme",
                             scheme,
                             "-configuration",
                             configuration.rawValue,
                             "-quiet",
                            ]
            return Output(pipe: Process.executable(launchPath: "/usr/bin/xcodebuild", arguments: arguments))
        }
        /// 执行 xcodebuild archive
        func archive()->Output{
            let arguments = ["archive",
                             "-workspace",
                             workspace,
                             "-scheme",
                             scheme,
                             "-configuration",
                             configuration.rawValue,
                             "-archivePath",
                             xcarchive,
                             "-quiet",
                            ]
            return Output(pipe: Process.executable(launchPath: "/usr/bin/xcodebuild", arguments: arguments))
        }
        /// 执行 xcodebuild exportArchive
        func exportArchive()->Output{
            let arguments = ["-exportArchive",
                             "-archivePath",
                             xcarchive,
                             "-configuration",
                             configuration.rawValue,
                             "-exportPath",
                             exportIpaPath,
                             "-exportOptionsPlist",
                             exportOptionsPlist,
                             "-quiet",
                            ]
            return Output(pipe: Process.executable(launchPath: "/usr/bin/xcodebuild", arguments: arguments))
        }
    }
    
  3. 准备工作完成,我们现在来编写打包代码

    do{
        let ipaTool = try IpaTool(projectPath: "项目路径",
                              configuration: .debug,
                              exportOptionsPlist: "ExportOptions.plist文件路径",
                              pgyerKey: "xxxxxx")
        
        print(ipaTool)
        print("执行clean")
        var output = ipaTool.clean()
        if output.readData.count > 0 {
            print("执行失败clean error = \(output.readData)")
            exit(-1)
        }
        print("执行archive")
        output = ipaTool.archive()
        if !FileManager.default.fileExists(atPath: ipaTool.xcarchive) {
            print("执行失败archive error = \(output.readData)")
            exit(-1)
        }
        print("执行exportArchive")
        output = ipaTool.exportArchive()
        
        if !FileManager.default.fileExists(atPath: ipaTool.exportIpaPath.appPath("\(ipaTool.scheme).ipa")) {
            print("执行失败exportArchive error =\(output.readData)")
            exit(-1)
        }
        print("导出ipa成功\(ipaTool.exportIpaPath)")
    }catch{
        print(error)
    }
    

    注意projectPath使用自己项目路径,exportOptionsPlist建议先手动打一次包来获取

  4. 运行结果

    IpaTool(scheme: "xxx", configuration: SwiftCommandLineTool.IpaTool.Configuration.debug, exportOptionsPlist: "/Users/xxx/Desktop/xxx_ipa/2021-06-03 09:48:40/ExportOptions.plist", exportIpaPath: "/Users/xxx/Desktop/xxx_ipa/2021-06-07 13:59:21", projectPath: "/Users/xxx/xxx_ios", projectName: "xxx", packageDirectory: "/Users/xxx/Desktop/xxx_ipa", pgyerKey: "51895949ad44dcc3934f47c17aa0c0e5")
    执行clean
    执行archive
    执行exportArchive
    导出ipa成功/Users/xxx/Desktop/xxx_ipa/2021-06-07 13:59:21
    Program ended with exit code: 0
    

4、把ipa包上传蒲公英

我这里上传文件使用Alamofire,如果你们熟悉URLSession用它也行

  1. 在Package.swift

    let package = Package(
        name: "SwiftCommandLineTool",
        platforms: [.macOS("10.12")],
        dependencies: [
            .package(name: "Alamofire",
                     url: "https://github.com/Alamofire/Alamofire.git",
                     from: "5.4.3")
        ],
        targets: [
            .target(
                name: "SwiftCommandLineTool",
                dependencies: ["Alamofire"]),
            .testTarget(
                name: "SwiftCommandLineToolTests",
                dependencies: ["SwiftCommandLineTool","Alamofire"]),
        ]
    )
    
    
  2. 给IpaTool添加一个上传ipa的函数

    //上传蒲公英
        func update(){
           
            let ipaPath = exportIpaPath.appPath("\(scheme).ipa")
            
            let upload = AF.upload(multipartFormData: { formdata in
                formdata.append(pgyerKey.data(using: .utf8)!, withName: "_api_key")
                formdata.append(URL(fileURLWithPath: ipaPath), withName: "file")
            }, to: URL(string: "https://www.pgyer.com/apiv2/app/upload")!)
            
            var isExit = true
            let queue = DispatchQueue(label: "queue")
            upload.uploadProgress(queue: queue) { progress in
                let p = Int((Double(progress.completedUnitCount) / Double(progress.totalUnitCount)) * 100)
                print("上传进度:\(p)%")
            }
            upload.responseData(queue:queue) { dataResponse in
                switch dataResponse.result {
                case .success(let data):
                    let result = String(data: data, encoding: .utf8) ?? ""
                    print("上传成功:\(result)")
                case .failure(let error):
                    print("上传失败: \(error)")
                }
                isExit = false
            }
            //使用循环换保证命令行程序,不会死掉
            while isExit {
                Thread.sleep(forTimeInterval: 1)
            }
        }
    
    print("导出ipa成功\(ipaTool.exportIpaPath)")
    print("开始上传蒲公英")
    ipaTool.update()
    

    上传文件的时候使用DispatchQueue.main命令行程序还是会死掉,所以加了一个while循环来保证程序不死。大家有其他方法告送我一下。

5、打代码打包成一个CLl工具(命令行程序)

我这里就不提供教程了,大家可以参考这篇文章:使用 Swift 编写 CLI 工具的入门教程

我不喜欢使用终端所以使用SwiftUI写了一个简单的macOS App

1623050858375.jpg

6、一般我们的项目中用了CocoaPods我们打包的时候要执行一下pod相关的命令

 // pod install
    func podInstall()->Output{
        var environment = [String:String]()
        /*
         添加环境变量LANG = en_US.UTF-8
         否则这个错误
         WARNING: CocoaPods requires your terminal to be using UTF-8 encoding.
         Consider adding the following to ~/.profile:
         export LANG=en_US.UTF-8
         */
        environment["LANG"] = "en_US.UTF-8"
        /*
         添加环境变量PATH = /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/xxx/.rvm/bin
         终端运行 echo $PATH 获取
         否则这个错误
         Traceback (most recent call last):
         9: from /usr/local/bin/pod:23:in `<main>'
         8: from /usr/local/bin/pod:23:in `load'
         7: from /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.10.1/bin/pod:55:in `<top (required)>'
         6: from /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.10.1/lib/cocoapods/command.rb:49:in `run'
         5: from /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.10.1/lib/cocoapods/command.rb:140:in `verify_minimum_git_version!'
         4: from /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.10.1/lib/cocoapods/command.rb:126:in `git_version'
         3: from /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.10.1/lib/cocoapods/executable.rb:143:in `capture_command'
         2: from /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.10.1/lib/cocoapods/executable.rb:117:in `which!'
         1: from /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.10.1/lib/cocoapods/executable.rb:117:in `tap'
     /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.10.1/lib/cocoapods/executable.rb:118:in `block in which!': [!] Unable to locate the executable `git` (Pod::Informative)
         */
        environment["PATH"] = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/xxx/.rvm/bin"
        /*
         添加环境变量CP_HOME_DIR = NSHomeDirectory().appending("/.cocoapods")
         我的cocoapods安装在home目录所以使用这个,
         你们可以在访达->前往文件夹...-> ~/.cocoapods,来获取路径
         否则这个错误
         Analyzing dependencies
         Cloning spec repo `cocoapods` from `https://github.com/CocoaPods/Specs.git`
         [!] Unable to add a source with url `https://github.com/CocoaPods/Specs.git` named `cocoapods`.
         You can try adding it manually in `/var/root/.cocoapods/repos` or via `pod repo add`.
         */
        environment["CP_HOME_DIR"] = NSHomeDirectory().appending("/.cocoapods")
        let pipe = Process.executable(launchPath: "/usr/local/bin/pod",
                                    arguments: ["install"],
                                    currentDirectoryPath: projectPath,
                                    environment: environment)
        return Output(pipe: pipe)
    }

最后附上 项目地址