Scripting in Swift For a Testable Build - iOS Conf SG, Singapore, January 2020

Scripting in Swift For a Testable Build - iOS Conf SG, Singapore, January 2020

Abstract:

Underlying many large iOS projects is a tangled nest of bash scripts that developers are often afraid to touch for fear of breaking something, and which is littered with print statements from generations of developers trying to figure out how on earth it works. How can we move away from this mess and to something more sustainable? Ellen will discuss moving a codebase like this to a command line tool that can be called from a Swift script, and which can be tested and breakpointed, and just might save a tiny bit of your sanity.

C4861b1dfdf3bbb21faec4a1acdf183d?s=128

Ellen Shapiro

January 17, 2020
Tweet

Transcript

  1. SCRIPTING IN SWIFT FOR A TESTABLE BUILD IOS CONF SG

    | SINGAPORE | JANUARY 2020 ELLEN SHAPIRO | @DESIGNATEDNERD | APOLLOGRAPHQL.COM
  2. None
  3. SINGAPORE NOW @CARROT_App

  4. MADISON NOW @CARROT_App

  5. YOUR IOS PROJECT HAS A NEST OF BASH SCRIPTS @DesignatedNerd

  6. YOUR IOS PROJECT HAS A NEST OF BASH SCRIPTS (IT'S

    OKAY, MY PROJECTS DO TOO) @DesignatedNerd
  7. @DesignatedNerd

  8. None
  9. SPOILER: THERE IS A BETTER WAY @DesignatedNerd

  10. SCRIPTING WITH ✨ SWIFT @DesignatedNerd

  11. @DesignatedNerd

  12. ! HOW DOES APOLLO WORK? @DesignatedNerd

  13. ! HOW DOES APOLLO WORK? (THE SHORT VERSION) @DesignatedNerd

  14. GraphQL @DesignatedNerd

  15. SCHEMA + QUERIES = CODE @DesignatedNerd

  16. SCHEMA + QUERIES = CODE @DesignatedNerd

  17. SCHEMA + QUERIES = CODE WHAT IS POSSIBLE? @DesignatedNerd

  18. SCHEMA + QUERIES = CODE @DesignatedNerd

  19. SCHEMA + QUERIES = CODE WHAT AM I ASKING FOR?

    @DesignatedNerd
  20. SCHEMA + QUERIES = CODE @DesignatedNerd

  21. SCHEMA + QUERIES = CODE IS WHAT I'M ASKING FOR

    POSSIBLE? @DesignatedNerd
  22. ! @DesignatedNerd

  23. None
  24. ! @DesignatedNerd

  25. SCHEMA + QUERIES = CODE @DesignatedNerd

  26. SCHEMA + QUERIES = CODE HAVE SOME TYPE-SAFE CODE! @DesignatedNerd

  27. JAVASCRIPT + NODE @DesignatedNerd

  28. TYPESCRIPT + NODE @DesignatedNerd

  29. via Webcomic Name and Alex Norris

  30. npm = @DesignatedNerd

  31. None
  32. HOW DO WE MAKE THIS BETTER? @DesignatedNerd

  33. !!!! @DesignatedNerd

  34. HOW DO WE MAKE THIS LESS TERRIBLE AS QUICKLY AS

    POSSIBLE? @DesignatedNerd
  35. BUNDLING EVERYTHING @DesignatedNerd

  36. BUNDLING EVERYTHING A SMALLER @DesignatedNerd

  37. BEFORE LOTS OF BASH YOU HAD TO WRITE @DesignatedNerd

  38. AFTER LOTS OF BASH I HAD TO WRITE @DesignatedNerd

  39. @DesignatedNerd

  40. I HATE BASH @DesignatedNerd

  41. I HATE SUCK AT BASH @DesignatedNerd

  42. TESTING AND DEBUGGING BASH: ¯\_(ϑ)_/¯ @DesignatedNerd

  43. echo "Variable: ${VARIABLE}" @DesignatedNerd

  44. FIGURING OUT WHAT WENT WRONG IN BASH: ¯\_(ϑ)_/¯ @DesignatedNerd

  45. ! @DesignatedNerd

  46. DON'T IT ALWAYS SEEM TO GO THAT YOU DON'T KNOW

    WHAT YOU GOT 'TIL IT'S GONE - JONI MITCHELL @DesignatedNerd
  47. LET'S DO THIS! @DesignatedNerd

  48. 1. BUILD A SWIFT FRAMEWORK THAT DOES WHAT YOUR SCRIPT

    CURRENTLY DOES @DesignatedNerd
  49. IDENTIFY WHAT YOUR SCRIPT DOES @DesignatedNerd

  50. IDENTIFY WHAT YOUR SCRIPT DOES (THIS SOUNDS MUCH EASIER THAN

    IT IS) @DesignatedNerd
  51. run_bundled_codegen.sh @DesignatedNerd

  52. WHAT APOLLO'S SCRIPT DOES @DesignatedNerd

  53. WHAT APOLLO'S SCRIPT DOES Is there a zipped version of

    the CLI? @DesignatedNerd
  54. WHAT APOLLO'S SCRIPT DOES Is there a zipped version of

    the CLI? NO @DesignatedNerd
  55. WHAT APOLLO'S SCRIPT DOES Is there a zipped version of

    the CLI? NO Download and unzip current version of CLI. @DesignatedNerd
  56. WHAT APOLLO'S SCRIPT DOES Is there a zipped version of

    the CLI? YES @DesignatedNerd
  57. WHAT APOLLO'S SCRIPT DOES Is there a zipped version of

    the CLI? YES Does the zipped version of the CLI have the correct hash for this version of the iOS SDK? @DesignatedNerd
  58. WHAT APOLLO'S SCRIPT DOES Is there a zipped version of

    the CLI? YES Does the zipped version of the CLI have the correct hash for this version of the iOS SDK? NO @DesignatedNerd
  59. WHAT APOLLO'S SCRIPT DOES Is there a zipped version of

    the CLI? YES Does the zipped version of the CLI have the correct hash for this version of the iOS SDK? NO Download and unzip current version of CLI. @DesignatedNerd
  60. WHAT APOLLO'S SCRIPT DOES Is there a zipped version of

    the CLI? YES Does the zipped version of the CLI have the correct hash for this version of the iOS SDK? YES @DesignatedNerd
  61. WHAT APOLLO'S SCRIPT DOES Is there a zipped version of

    the CLI? YES Does the zipped version of the CLI have the correct hash for this version of the iOS SDK? YES Unzip if needed @DesignatedNerd
  62. RUN THE CLI WITH THE PASSED IN INFORMATION ⬇ GENERATE

    CODE @DesignatedNerd
  63. codegen:generate --target=swift --localSchemaFile="schema.json" --includes=./**/*.graphql --operationIdsPath=operationIdsPath.json --suppressSwiftMultilineStringLiterals ⬇ ApolloCodegenOptions @DesignatedNerd

  64. if [ -f "${ZIP_FILE}" ] ⬇ FileManager @DesignatedNerd

  65. curl ⬇ URLSession @DesignatedNerd

  66. /usr/bin/shasum ⬇ CommonCrypto @DesignatedNerd

  67. SEPARATION OF CONCERNS @DesignatedNerd

  68. ! DEPENDENCY INJECTION @DesignatedNerd

  69. CODE COVERAGE @DesignatedNerd

  70. @DesignatedNerd

  71. @DesignatedNerd

  72. @DesignatedNerd

  73. ! BEST PRACTICES, DUH @DesignatedNerd

  74. YOUR SCRIPTS DESERVE TO BE FIRST-CLASS CITIZENS @DesignatedNerd

  75. IF YOU CAN'T RELEASE YOUR APP WITHOUT IT TEST IT

    @DesignatedNerd
  76. IF YOU CAN'T RELEASE YOUR APP WITHOUT IT TEST IT

    (AND IT'S A LOT EASIER TO TEST IT IN SWIFT) @DesignatedNerd
  77. 2. USE THE LIBRARY IN A SPM EXECUTABLE @DesignatedNerd

  78. #! /usr/bin/env xcrun swift -F ../build/Debug @DesignatedNerd

  79. chmod +x main.swift @DesignatedNerd

  80. @DesignatedNerd

  81. @DesignatedNerd

  82. @DesignatedNerd

  83. swift run @DesignatedNerd

  84. DON'T DO THIS @DesignatedNerd

  85. CREATE IT WITH SWIFT PACKAGE MANAGER $ mkdir Codegen $

    cd Codegen $ swift package init --type executable @DesignatedNerd
  86. Package.swift // swift-tools-version:5.1 import PackageDescription let package = Package( name:

    "Codegen", products: [ .executable(name: "Codegen", targets: ["Codegen"]), ], dependencies: [ .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen")) ], targets: [ .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]), .testTarget(name: "CodegenTests", dependencies: ["Codegen"]), ] ) @DesignatedNerd
  87. Package.swift // swift-tools-version:5.1 import PackageDescription let package = Package( name:

    "Codegen", products: [ .executable(name: "Codegen", targets: ["Codegen"]), ], dependencies: [ .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen")) ], targets: [ .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]), .testTarget(name: "CodegenTests", dependencies: ["Codegen"]), ] ) @DesignatedNerd
  88. Package.swift // swift-tools-version:5.1 import PackageDescription let package = Package( name:

    "Codegen", products: [ .executable(name: "Codegen", targets: ["Codegen"]), ], dependencies: [ .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen")) ], targets: [ .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]), .testTarget(name: "CodegenTests", dependencies: ["Codegen"]), ] ) @DesignatedNerd
  89. Package.swift // swift-tools-version:5.1 import PackageDescription let package = Package( name:

    "Codegen", products: [ .executable(name: "Codegen", targets: ["Codegen"]), ], dependencies: [ .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen")) ], targets: [ .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]), .testTarget(name: "CodegenTests", dependencies: ["Codegen"]), ] ) @DesignatedNerd
  90. Package.swift // swift-tools-version:5.1 import PackageDescription let package = Package( name:

    "Codegen", products: [ .executable(name: "Codegen", targets: ["Codegen"]), ], dependencies: [ .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen")) ], targets: [ .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]), .testTarget(name: "CodegenTests", dependencies: ["Codegen"]), ] ) @DesignatedNerd
  91. Package.swift // swift-tools-version:5.1 import PackageDescription let package = Package( name:

    "Codegen", products: [ .executable(name: "Codegen", targets: ["Codegen"]), ], dependencies: [ .package(url: "https://github.com/apollographql/apollo-ios.git", .branch("swift-codegen")) ], targets: [ .target(name: "Codegen", dependencies: ["ApolloCodegenLib"]), .testTarget(name: "CodegenTests", dependencies: ["Codegen"]), ] ) @DesignatedNerd
  92. @DesignatedNerd

  93. $SRCROOT @DesignatedNerd

  94. @DesignatedNerd

  95. main.swift print("Hello, world!") @DesignatedNerd

  96. main.swift import Foundation enum MyCodegenError: Error { case sourceRootNotProvided case

    sourceRootNotADirectory case targetDoesntExist } guard let sourceRootPath = ProcessInfo.processInfo.environment["SRCROOT"] else { throw MyCodegenError.sourceRootNotProvided } @DesignatedNerd
  97. main.swift import Foundation import ApolloCodegenLib enum MyCodegenError: Error { case

    sourceRootNotProvided case sourceRootNotADirectory case targetDoesntExist } guard let sourceRootPath = ProcessInfo.processInfo.environment["SRCROOT"] else { throw MyCodegenError.sourceRootNotProvided } guard FileManager.default.apollo_folderExists(at: sourceRootPath) else { throw MyCodegenError.sourceRootNotADirectory } @DesignatedNerd
  98. main.swift (pt. 2) let applicationTargetFolderURL = sourceRootURL .appendingPathComponent("SwiftScriptTestSPM") guard FileManager.default.apollo_folderExists(at:

    applicationTarget) else { throw MyCodegenError.targetDoesntExist } let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts") let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL) do { try ApolloCodegen.run(from: applicationTargetFolderURL, with: scriptsFolderURL, options: options) } catch { CodegenLogger.log("ERROR: \(error.localizedDescription)") exit(1) } @DesignatedNerd
  99. main.swift (pt. 2) let applicationTargetFolderURL = sourceRootURL .appendingPathComponent("SwiftScriptTestSPM") guard FileManager.default.apollo_folderExists(at:

    applicationTarget) else { throw MyCodegenError.targetDoesntExist } let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts") let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL) do { try ApolloCodegen.run(from: applicationTargetFolderURL, with: scriptsFolderURL, options: options) } catch { CodegenLogger.log("ERROR: \(error.localizedDescription)") exit(1) } @DesignatedNerd
  100. main.swift (pt. 2) let applicationTargetFolderURL = sourceRootURL .appendingPathComponent("SwiftScriptTestSPM") guard FileManager.default.apollo_folderExists(at:

    applicationTarget) else { throw MyCodegenError.targetDoesntExist } let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts") let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL) do { try ApolloCodegen.run(from: applicationTargetFolderURL, with: scriptsFolderURL, options: options) } catch { CodegenLogger.log("ERROR: \(error.localizedDescription)") exit(1) } @DesignatedNerd
  101. main.swift (pt. 2) let applicationTargetFolderURL = sourceRootURL .appendingPathComponent("SwiftScriptTestSPM") guard FileManager.default.apollo_folderExists(at:

    applicationTarget) else { throw MyCodegenError.targetDoesntExist } let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts") let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL) do { try ApolloCodegen.run(from: applicationTargetFolderURL, with: scriptsFolderURL, options: options) } catch { CodegenLogger.log("ERROR: \(error.localizedDescription)") exit(1) } @DesignatedNerd
  102. main.swift (pt. 2) let applicationTargetFolderURL = sourceRootURL .appendingPathComponent("SwiftScriptTestSPM") guard FileManager.default.apollo_folderExists(at:

    applicationTarget) else { throw MyCodegenError.targetDoesntExist } let scriptFolderURL = sourceRootURL.appendingPathComponent("scripts") let options = ApolloCodegenOptions(targetRootURL: applicationTargetFolderURL) do { try ApolloCodegen.run(from: applicationTargetFolderURL, with: scriptsFolderURL, options: options) } catch { CodegenLogger.log("ERROR: \(error.localizedDescription)") exit(1) } @DesignatedNerd
  103. IN YOUR APPLICATION PROJECT @DesignatedNerd

  104. 3. ! PROFIT! @DesignatedNerd

  105. 3. ! PROFIT!* * - BY WASTING LESS OF YOUR

    TIME FAFFING AROUND WITH BASH @DesignatedNerd
  106. A CAVEAT @DesignatedNerd

  107. DEVELOPERS ARE NOT USED TO SCRIPTING IN SWIFT @DesignatedNerd

  108. ! " @DesignatedNerd

  109. WRITE THE DOCS @DesignatedNerd

  110. COMING SOON TO AN APOLLO IOS SDK NEAR YOU! @DesignatedNerd

  111. OBLIGATORY SUMMARY SLIDE @DesignatedNerd

  112. OBLIGATORY SUMMARY SLIDE > Use swift to write testable script

    code @DesignatedNerd
  113. OBLIGATORY SUMMARY SLIDE > Use swift to write testable script

    code > Know what broke when stuff breaks @DesignatedNerd
  114. OBLIGATORY SUMMARY SLIDE > Use swift to write testable script

    code > Know what broke when (not if) stuff breaks @DesignatedNerd
  115. OBLIGATORY SUMMARY SLIDE > Use swift to write testable script

    code > Know what broke when (not if) stuff breaks > Dissect your old scripts, rebuild in a Swift Framework @DesignatedNerd
  116. OBLIGATORY SUMMARY SLIDE > Use swift to write testable script

    code > Know what broke when (not if) stuff breaks > Dissect your old scripts, rebuild in a Swift Framework > Test everything you can't release your app without @DesignatedNerd
  117. OBLIGATORY SUMMARY SLIDE > Use swift to write testable script

    code > Know what broke when (not if) stuff breaks > Dissect your old scripts, rebuild in a Swift Framework > Test everything you can't release your app without > Use Swift Package Manager to build an executable @DesignatedNerd
  118. OBLIGATORY SUMMARY SLIDE > Use swift to write testable script

    code > Know what broke when (not if) stuff breaks > Dissect your old scripts, rebuild in a Swift Framework > Test everything you can't release your app without > Use Swift Package Manager to build an executable > Run that from a build script run phase in your main app @DesignatedNerd
  119. OBLIGATORY SUMMARY SLIDE > Use swift to write testable script

    code > Know what broke when (not if) stuff breaks > Dissect your old scripts, rebuild in a Swift Framework > Test everything you can't release your app without > Use Swift Package Manager to build an executable > Run that from a build script run phase in your main app > Document the crap out of everything @DesignatedNerd
  120. ! THANK YOU! @DesignatedNerd

  121. LINKS! ADOPTING SWIFT PACKAGES IN XCODE https://developer.apple.com/videos/play/wwdc2019/408/ CREATING SWIFT PACKAGES

    https://developer.apple.com/videos/play/wwdc2019/410/ @DesignatedNerd