build-number-update.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. #!/usr/bin/env xcrun --sdk macosx swift
  2. import Foundation
  3. // MARK: Types
  4. struct InfoPlist {
  5. private typealias Plist = (dict: [String: Any], format: PropertyListSerialization.PropertyListFormat)
  6. private var fileURL: URL
  7. private var plist: Plist
  8. enum Const: String {
  9. case version = "CFBundleShortVersionString"
  10. case build = "CFBundleVersion"
  11. case settings = "PreferenceSpecifiers"
  12. case settingsValue = "DefaultValue"
  13. }
  14. var version: String? {
  15. get {
  16. plist.dict[InfoPlist.Const.version.rawValue] as? String
  17. }
  18. set {
  19. plist.dict[InfoPlist.Const.version.rawValue] = newValue
  20. }
  21. }
  22. var build: String? {
  23. get {
  24. plist.dict[InfoPlist.Const.build.rawValue] as? String
  25. }
  26. set {
  27. plist.dict[InfoPlist.Const.build.rawValue] = newValue
  28. }
  29. }
  30. var settingsVersion: String? {
  31. get {
  32. (
  33. plist
  34. .dict[InfoPlist.Const.settings.rawValue] as! [[String: Any]]
  35. )[1][
  36. InfoPlist.Const.settingsValue
  37. .rawValue
  38. ] as? String
  39. }
  40. set {
  41. var dictCopy = plist.dict
  42. var specs = dictCopy[InfoPlist.Const.settings.rawValue] as! [[String: Any]]
  43. specs[1][InfoPlist.Const.settingsValue.rawValue] = newValue ?? ""
  44. dictCopy[InfoPlist.Const.settings.rawValue] = specs
  45. plist.dict = dictCopy
  46. }
  47. }
  48. private init(fileURL: URL, plist: Plist) {
  49. self.fileURL = fileURL
  50. self.plist = plist
  51. }
  52. init?(fromFileAtURL fileURL: URL) {
  53. guard let plist = InfoPlist.readPlist(fromFileAtURL: fileURL) else { return nil }
  54. self.init(fileURL: fileURL, plist: plist)
  55. }
  56. func save() {
  57. InfoPlist.writePlist(plist, toFileAtURL: fileURL)
  58. }
  59. // MARK: Plist file read/write
  60. private static func readPlist(fromFileAtURL fileURL: URL) -> Plist? {
  61. var data: Data
  62. do {
  63. data = try Data(contentsOf: fileURL)
  64. } catch {
  65. return nil
  66. }
  67. var format: PropertyListSerialization.PropertyListFormat = .xml
  68. let dict: [String: Any]
  69. do {
  70. dict = try PropertyListSerialization.propertyList(from: data, format: &format) as! [String: Any]
  71. } catch {
  72. print("error: Failed to deserialize plist read from file: \(fileURL.absoluteString)")
  73. print("Error details: \(error)")
  74. return nil
  75. }
  76. return (dict: dict, format: format)
  77. }
  78. private static func writePlist(_ plist: Plist, toFileAtURL fileURL: URL) {
  79. let data: Data
  80. do {
  81. data = try PropertyListSerialization.data(fromPropertyList: plist.dict, format: plist.format, options: 0)
  82. } catch {
  83. print("error: Failed to serialize plist!")
  84. return
  85. }
  86. do {
  87. try data.write(to: fileURL)
  88. } catch {
  89. print("error: Failed to write file: \(fileURL.absoluteString)")
  90. print("Error details: \(error)")
  91. return
  92. }
  93. }
  94. }
  95. enum Branch: CustomStringConvertible {
  96. private static var releasePrefix = "release"
  97. case Release(version: String)
  98. case Other(name: String)
  99. init(_ string: String) {
  100. let parts = string.components(separatedBy: "/")
  101. if parts.count >= 2, parts[0] == Branch.releasePrefix {
  102. self = .Release(version: parts[1])
  103. } else {
  104. self = .Other(name: string)
  105. }
  106. }
  107. var description: String {
  108. switch self {
  109. case let .Release(version):
  110. return "\(Branch.releasePrefix)/\(version)"
  111. case let .Other(name):
  112. return name
  113. }
  114. }
  115. }
  116. // MARK: Helpers
  117. func execute(command: String, args: [String]) -> String? {
  118. let process = Process()
  119. process.launchPath = command
  120. process.arguments = args
  121. let pipe = Pipe()
  122. process.standardOutput = pipe
  123. process.launch()
  124. let data = pipe.fileHandleForReading.readDataToEndOfFile()
  125. return String(data: data, encoding: .utf8)?
  126. .trimmingCharacters(in: .newlines)
  127. }
  128. func checkdSYM(buildNumber: String?) {
  129. // Не правильная версия в dSYM может привести к проблемам в работе с сервисами, которые эти dSYM используют (например: Crashlytics)
  130. // http://tgoode.com/2014/06/05/sensible-way-increment-bundle-version-cfbundleversion-xcode/#comment-600
  131. // http://tgoode.com/2014/06/05/sensible-way-increment-bundle-version-cfbundleversion-xcode/#comment-2704
  132. // http://stackoverflow.com/q/13323728
  133. // http://stackoverflow.com/a/22460268
  134. if let dsymFolderPath = ProcessInfo.processInfo.environment["DWARF_DSYM_FOLDER_PATH"], !dsymFolderPath.isEmpty,
  135. let productName = ProcessInfo.processInfo.environment["PRODUCT_NAME"], !productName.isEmpty
  136. {
  137. let dsymPlistURL = URL(fileURLWithPath: "\(dsymFolderPath)/\(productName).app.dSYM/Contents/Info.plist")
  138. guard var dsymInfoPlist = InfoPlist(fromFileAtURL: dsymPlistURL) else {
  139. print("Failed to read dsym at url \(dsymPlistURL). This is ok for debug builds.")
  140. return
  141. }
  142. dsymInfoPlist.build = buildNumber
  143. dsymInfoPlist.save()
  144. }
  145. }
  146. // MARK: Implementation
  147. guard let currentBranch = execute(command: "/usr/bin/git", args: ["rev-parse", "--abbrev-ref", "HEAD"]).map(Branch.init) else {
  148. print("Can't determine current branch! Skiping...")
  149. exit(0)
  150. }
  151. guard let commitsCount = execute(command: "/usr/bin/git", args: ["rev-list", "--count", "HEAD"]), !commitsCount.isEmpty else {
  152. print("Can't determine commits count! Skiping..")
  153. exit(0)
  154. }
  155. guard let targetBuildDir = ProcessInfo.processInfo.environment["TARGET_BUILD_DIR"], !targetBuildDir.isEmpty else {
  156. print("error: TARGET_BUILD_DIR environment variable is empty!")
  157. exit(1)
  158. }
  159. guard let infoPlistPath = ProcessInfo.processInfo.environment["INFOPLIST_PATH"], !infoPlistPath.isEmpty else {
  160. print("error: INFOPLIST_PATH environment variable is empty!")
  161. exit(1)
  162. }
  163. guard let infoPlistFile = ProcessInfo.processInfo.environment["INFOPLIST_FILE"], !infoPlistFile.isEmpty else {
  164. print("error: INFOPLIST_FILE environment variable is empty!")
  165. exit(1)
  166. }
  167. guard let currentVersion = ProcessInfo.processInfo.environment["CURRENT_PROJECT_VERSION"] else {
  168. print("error: Current version can't be determined!")
  169. exit(1)
  170. }
  171. guard var buildInfoPlist = InfoPlist(fromFileAtURL: URL(fileURLWithPath: "\(targetBuildDir)/\(infoPlistPath)")) else {
  172. print("error: Build Info Plist cannot be load!")
  173. exit(1)
  174. }
  175. guard let scrRoot = ProcessInfo.processInfo.environment["SRCROOT"], !targetBuildDir.isEmpty else {
  176. print("error: SRCROOT environment variable is empty!")
  177. exit(1)
  178. }
  179. guard var sourceInfoPlist = InfoPlist(fromFileAtURL: URL(fileURLWithPath: "\(scrRoot)/FreeAPS/Resources/Info.plist")) else {
  180. print("error: Source Info Plist cannot be load!")
  181. exit(1)
  182. }
  183. switch currentBranch {
  184. case let .Release(version):
  185. buildInfoPlist.version = version
  186. buildInfoPlist.build = commitsCount
  187. sourceInfoPlist.build = commitsCount
  188. default:
  189. buildInfoPlist.build = "\(commitsCount)"
  190. }
  191. buildInfoPlist.save()
  192. sourceInfoPlist.save()
  193. checkdSYM(buildNumber: buildInfoPlist.build)