|
| 1 | +# This script copies and signs the current `ruby` executable, |
| 2 | +# so that it can be run in a debugger or profiler, like Xcode's |
| 3 | +# Instruments. |
| 4 | +# |
| 5 | +# The output folder is configured by `destination_folder` below, |
| 6 | +# but it's just the current dir by default. |
| 7 | +# This script will create a new `Ruby.app` bundle in that dest. |
| 8 | +# |
| 9 | +# Before running this script: |
| 10 | +# |
| 11 | +# 1. Create a signing certificate for yourself. |
| 12 | +# |
| 13 | +# Follow the instructions under the heading "To obtain a self-signed certificate using Certificate Assistant" |
| 14 | +# https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html |
| 15 | +# |
| 16 | +# The defaults worked for me, so I didn't need to check "Let me override defaults". |
| 17 | +# |
| 18 | +# Once done, it should show up when you run `security find-identity` |
| 19 | +# |
| 20 | +# 2. Set `$signing_cert_name` to the name of your new certificate |
| 21 | +# |
| 22 | +# 3. Run `sudo DevToolsSecurity -enable` (Optional) |
| 23 | +# This will make it so you don't get prompted for a password every time you run a debugger/profiler. |
| 24 | +# |
| 25 | +# After running this script, you can either use the `dest/Ruby.app/Contents/macOS/ruby` binary from there, |
| 26 | +# or copy it to another place of your choosing. |
| 27 | + |
| 28 | +require "rbconfig" |
| 29 | +require "pathname" |
| 30 | +require "tempfile" |
| 31 | + |
| 32 | +# Inputs |
| 33 | +$signing_cert_name = "'Alexander Momchilov (Shopify)'" |
| 34 | +destination_folder = Pathname.pwd |
| 35 | + |
| 36 | +app_bundle = create_bundle_skeleton(at: destination_folder) |
| 37 | + |
| 38 | +copy_ruby_executable(into: app_bundle) |
| 39 | + |
| 40 | +sign_bundle(app_bundle) |
| 41 | + |
| 42 | +if verify_entitlements(app_bundle) |
| 43 | + puts "Successfully created a signed Ruby at: #{app_bundle}" |
| 44 | + exit(true) |
| 45 | +else |
| 46 | + puts "Something went wrong." |
| 47 | + exit(false) |
| 48 | +end |
| 49 | + |
| 50 | + |
| 51 | + |
| 52 | + |
| 53 | + |
| 54 | +BEGIN { # Helper methods |
| 55 | + def create_bundle_skeleton(at:) |
| 56 | + destination = at |
| 57 | + |
| 58 | + app_bundle = (destination / "Ruby.app").expand_path |
| 59 | + contents_folder = app_bundle / "Contents" |
| 60 | + |
| 61 | + contents_folder.mkpath |
| 62 | + |
| 63 | + info_plist_content = <<~INFO_PLIST |
| 64 | + <?xml version="1.0" encoding="UTF-8"?> |
| 65 | + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 66 | + <plist version="1.0"> |
| 67 | + <dict> |
| 68 | + <key>CFBundleInfoDictionaryVersion</key> |
| 69 | + <string>6.0</string> |
| 70 | + <key>CFBundleExecutable</key> |
| 71 | + <string>ruby</string> |
| 72 | + <key>CFBundleIdentifier</key> |
| 73 | + <string>com.shopify.amomchilov.Ruby</string> |
| 74 | + <key>CFBundleName</key> |
| 75 | + <string>Ruby</string> |
| 76 | + <key>CFBundleDisplayName</key> |
| 77 | + <string>Ruby</string> |
| 78 | + <key>CFBundleShortVersionString</key> |
| 79 | + <string>#{RUBY_VERSION}</string> |
| 80 | + <key>CFBundleVersion</key> |
| 81 | + <string>#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}</string> |
| 82 | + <key>NSHumanReadableCopyright</key> |
| 83 | + <string>#{RUBY_COPYRIGHT.delete_prefix("ruby - ")}</string> |
| 84 | + </dict> |
| 85 | + </plist> |
| 86 | + INFO_PLIST |
| 87 | + |
| 88 | + (contents_folder / "Info.plist").write(info_plist_content) |
| 89 | + |
| 90 | + app_bundle |
| 91 | + end |
| 92 | + |
| 93 | + def copy_ruby_executable(into:) |
| 94 | + app_bundle = into |
| 95 | + destination = (app_bundle / "Contents/MacOS") |
| 96 | + |
| 97 | + begin |
| 98 | + destination.mkpath |
| 99 | + rescue Errno::EEXIST |
| 100 | + # Folder already exists. No problem. |
| 101 | + end |
| 102 | + |
| 103 | + original_ruby = Pathname.new(RbConfig.ruby) |
| 104 | + puts "Copying Ruby #{RUBY_VERSION} from #{original_ruby}" |
| 105 | + FileUtils.cp(original_ruby, destination) |
| 106 | + destination |
| 107 | + end |
| 108 | + |
| 109 | + def sign_bundle(bundle_path) |
| 110 | + entitlements_plist = <<~PLIST |
| 111 | + <?xml version="1.0" encoding="UTF-8"?> |
| 112 | + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 113 | + <plist version="1.0"> |
| 114 | + <dict> |
| 115 | + <key>com.apple.security.get-task-allow</key> |
| 116 | + <true/> |
| 117 | +
|
| 118 | + <!-- https://developer.apple.com/documentation/BundleResources/Entitlements/com.apple.security.cs.disable-library-validation --> |
| 119 | + <key>com.apple.security.cs.disable-library-validation</key> |
| 120 | + <true/> |
| 121 | + </dict> |
| 122 | + </plist> |
| 123 | + PLIST |
| 124 | + |
| 125 | + Tempfile.create("entitlements.plist") do |entitlements_file| |
| 126 | + entitlements_path = entitlements_file.path |
| 127 | + |
| 128 | + args = [ |
| 129 | + "/usr/bin/codesign", |
| 130 | + "--force", # Replace the existing signiture, if any |
| 131 | + "--sign", $signing_cert_name, |
| 132 | + "-o", "runtime", |
| 133 | + "--entitlements", entitlements_file.path, |
| 134 | + "--timestamp\\=none", |
| 135 | + "--generate-entitlement-der", # necessary? |
| 136 | + "#{bundle_path}" |
| 137 | + ] |
| 138 | + |
| 139 | + entitlements_file.puts(entitlements_plist) |
| 140 | + entitlements_file.flush |
| 141 | + |
| 142 | + puts "\nSigning #{bundle_path}." |
| 143 | + puts "----- codesign output:" |
| 144 | + system(args.join(" "), exception: true) |
| 145 | + puts "-----\n\n" |
| 146 | + end |
| 147 | + |
| 148 | + nil |
| 149 | + end |
| 150 | + |
| 151 | + def verify_entitlements(bundle_path) |
| 152 | + puts "Verifying the code signature..." |
| 153 | + entitlements_xml = `codesign --display --entitlements - --xml #{bundle_path} 2>/dev/null` |
| 154 | + |
| 155 | + disable_lib_validation = "com.apple.security.cs.disable-library-validation" |
| 156 | + allow_debugging = "com.apple.security.get-task-allow" |
| 157 | + |
| 158 | + issues = [] |
| 159 | + |
| 160 | + # Doing dumb string matching, so we don't need to pull in an XML/Plist parser. |
| 161 | + if entitlements_xml.include?(disable_lib_validation) |
| 162 | + if entitlements_xml.include?("<key>#{disable_lib_validation}</key><true/>") |
| 163 | + puts "\t- ✅ Entitlement #{disable_lib_validation.inspect} was set correctly" |
| 164 | + else |
| 165 | + issues << "\t- ❌ #{disable_lib_validation.inspect} was not `true` in the bundle's entitlements." |
| 166 | + end |
| 167 | + else |
| 168 | + issues << "\t- ❌ #{disable_lib_validation.inspect} was missing from the bundle's entitlements." |
| 169 | + end |
| 170 | + |
| 171 | + if entitlements_xml.include?(allow_debugging) |
| 172 | + if entitlements_xml.include?("<key>#{allow_debugging}</key><true/>") |
| 173 | + puts "\t- ✅ Entitlement #{allow_debugging.inspect} was set correctly" |
| 174 | + else |
| 175 | + issues << "\t- ❌ #{allow_debugging.inspect} was not `true` in the bundle's entitlements." |
| 176 | + end |
| 177 | + else |
| 178 | + issues << "\t- ❌ #{allow_debugging.inspect} was missing from the bundle's entitlements." |
| 179 | + end |
| 180 | + |
| 181 | + if issues.any? |
| 182 | + puts "There were issues with the code-signing:" |
| 183 | + puts issues |
| 184 | + puts |
| 185 | + return false |
| 186 | + end |
| 187 | + |
| 188 | + puts |
| 189 | + |
| 190 | + true |
| 191 | + end |
| 192 | +} |
0 commit comments