├── .gitignore ├── 10.12 ├── LICENSE.md ├── README.md ├── bin ├── fileicon ├── fileicon.webloc ├── unsign └── unsign.webloc ├── lib ├── helpers.sh ├── input.sh └── verify_codesign.rb ├── resources ├── XcodeUnsigned.png └── readme_XcodeUnsigned.png ├── xcrestore └── xcunsign /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | artifacts/ 3 | -------------------------------------------------------------------------------- /10.12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johntmcintosh/xcunsign/8abddb1a088f019f6dba88b88e77abd771490473/10.12 -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 John T McIntosh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xcunsign/xcrestore 2 | 3 | 4 | 5 | ## About 6 | 7 | `xcunsign` and `xcrestore` are two scripts that allow for easy swapping between signed and unsigned copies of Xcode. 8 | 9 | ## Why? 10 | 11 | Xcode 8 disables the ability to run 3rd party plugins (such as [Alcatraz]), in favor of providing an official extensions API ([WWDC: Using and Extending the Xcode Source Editor](https://developer.apple.com/videos/play/wwdc2016/414/)). This is a great thing for security and preventing the next [XcodeGhost 👻](https://en.wikipedia.org/wiki/XcodeGhost), and it sounds like the Xcode engineers want to provide the extension points that the community is asking for. However, only the source editor extension is available right now, which means that some of our favorite plugins are disabled until official support becomes available. 12 | 13 | ## Security 14 | 15 | In light of the security benefits of using a signed Xcode, I would recommend swapping back to the signed version before any deployment builds are generated. These scripts can be integrated with [fastlane](https://fastlane.tools) to ensure that all deployment builds are generated from the signed Xcode, while you continue to use the unsigned version for access to plugins during development. 16 | 17 | Fastlane has an action called `verify_xcode` which can be run as part of your `Fastfile` to ensure that the Xcode being used for the build is properly signed. 18 | 19 | ## Installation 20 | 21 | To install the scripts, clone or download the repo, and then you can choose one of the following: 22 | 23 | 1. Call the scripts directly 24 | 2. Add the repo directory to your PATH 25 | 3. Symlink the scripts into a directory in your path 26 | 27 | ``` 28 | ln -s /xcunsign /usr/local/bin/xcunsign 29 | ln -s /xcrestore /usr/local/bin/xcrestore 30 | ``` 31 | 32 | ## Usage 33 | 34 | To unsign, call the script, passing in the version of Xcode that you want to unsign. The script will find the copy of Xcode in the `/Applications` directory with that version, run [`unsign`](https://github.com/steakknife/unsign) on it, and keep a copy of the original signed binary that can be used to restore later. It also modifies Xcode's icon to indicate whether the app is currently signed or not. 35 | 36 | ``` 37 | xcunsign 8.0 38 | ``` 39 | 40 | To restore the signed binary, `Xcode` will be restored to the original binary that was present before the unsigned copy was created. 41 | 42 | ``` 43 | xcrestore 8.0 44 | ``` 45 | 46 | 47 | ## Benefits of xcunsign 48 | 49 | The reason I created this as an alternative to other approaches I have seen is that this allows me to swap quickly between signed and unsigned installations without needing to maintain two full copies of Xcode.app. The only thing that gets swapped out when the scripts are run is the `Xcode` binary within the Xcode.app container. 50 | 51 | 52 | ## Roadmap 53 | 54 | - If there is only one version of Xcode installed, it shouldn't be necessary to pass in the version. 55 | - Implement a fastlane plugin to xcrestore before the build 56 | 57 | ## Credits 58 | 59 | Special thanks to [steakknife's unsign](https://github.com/steakknife/unsign) and [mklement0's fileicon](https://github.com/mklement0/fileicon). 60 | 61 | ## License 62 | 63 | MIT 64 | -------------------------------------------------------------------------------- /bin/fileicon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ### 4 | # Home page: https://github.com/mklement0/fileicon 5 | # Author: Michael Klement (http://same2u.net) 6 | # Invoke with: 7 | # --version for version information 8 | # --help for usage information 9 | ### 10 | 11 | # --- STANDARD SCRIPT-GLOBAL CONSTANTS 12 | 13 | kTHIS_NAME=${BASH_SOURCE##*/} 14 | kTHIS_HOMEPAGE='https://github.com/mklement0/fileicon' 15 | kTHIS_VERSION='v0.1.8' # NOTE: This assignment is automatically updated by `make version VER=` - DO keep the 'v' prefix. 16 | 17 | unset CDPATH # To prevent unpredictable `cd` behavior. 18 | 19 | # --- Begin: STANDARD HELPER FUNCTIONS 20 | 21 | die() { echo "$kTHIS_NAME: ERROR: ${1:-"ABORTING due to unexpected error."}" 1>&2; exit ${2:-1}; } 22 | dieSyntax() { echo "$kTHIS_NAME: ARGUMENT ERROR: ${1:-"Invalid argument(s) specified."} Use -h for help." 1>&2; exit 2; } 23 | 24 | # SYNOPSIS 25 | # openUrl 26 | # DESCRIPTION 27 | # Opens the specified URL in the system's default browser. 28 | openUrl() { 29 | local url=$1 platform=$(uname) cmd=() 30 | case $platform in 31 | 'Darwin') # OSX 32 | cmd=( open "$url" ) 33 | ;; 34 | 'CYGWIN_'*) # Cygwin on Windows; must call cmd.exe with its `start` builtin 35 | cmd=( cmd.exe /c start '' "$url " ) # !! Note the required trailing space. 36 | ;; 37 | 'MINGW32_'*) # MSYS or Git Bash on Windows; they come with a Unix `start` binary 38 | cmd=( start '' "$url" ) 39 | ;; 40 | *) # Otherwise, assume a Freedesktop-compliant OS, which includes many Linux distros, PC-BSD, OpenSolaris, ... 41 | cmd=( xdg-open "$url" ) 42 | ;; 43 | esac 44 | "${cmd[@]}" || { echo "Cannot locate or failed to open default browser; please go to '$url' manually." >&2; return 1; } 45 | } 46 | 47 | # Prints the embedded Markdown-formatted man-page source to stdout. 48 | printManPageSource() { 49 | /usr/bin/sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/ { s///; t\np;}' "$BASH_SOURCE" 50 | } 51 | 52 | # Opens the man page, if installed; otherwise, tries to display the embedded Markdown-formatted man-page source; if all else fails: tries to display the man page online. 53 | openManPage() { 54 | local pager embeddedText 55 | if ! man 1 "$kTHIS_NAME" 2>/dev/null; then 56 | # 2nd attempt: if present, display the embedded Markdown-formatted man-page source 57 | embeddedText=$(printManPageSource) 58 | if [[ -n $embeddedText ]]; then 59 | pager='more' 60 | command -v less &>/dev/null && pager='less' # see if the non-standard `less` is available, because it's preferable to the POSIX utility `more` 61 | printf '%s\n' "$embeddedText" | "$pager" 62 | else # 3rd attempt: open the the man page on the utility's website 63 | openUrl "${kTHIS_HOMEPAGE}/doc/${kTHIS_NAME}.md" 64 | fi 65 | fi 66 | } 67 | 68 | # Prints the contents of the synopsis chapter of the embedded Markdown-formatted man-page source for quick reference. 69 | printUsage() { 70 | local embeddedText 71 | # Extract usage information from the SYNOPSIS chapter of the embedded Markdown-formatted man-page source. 72 | embeddedText=$(/usr/bin/sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/!d; /^## SYNOPSIS$/,/^#/{ s///; t\np; }' "$BASH_SOURCE") 73 | if [[ -n $embeddedText ]]; then 74 | # Print extracted synopsis chapter - remove backticks for uncluttered display. 75 | printf '%s\n\n' "$embeddedText" | tr -d '`' 76 | else # No SYNOPIS chapter found; fall back to displaying the man page. 77 | echo "WARNING: usage information not found; opening man page instead." >&2 78 | openManPage 79 | fi 80 | } 81 | 82 | # --- End: STANDARD HELPER FUNCTIONS 83 | 84 | # --- PROCESS STANDARD, OUTPUT-INFO-THEN-EXIT OPTIONS. 85 | case $1 in 86 | --version) 87 | # Output version number and exit, if requested. 88 | ver="v0.2.0"; echo "$kTHIS_NAME $kTHIS_VERSION"$'\nFor license information and more, visit '"$kTHIS_HOMEPAGE"; exit 0 89 | ;; 90 | -h|--help) 91 | # Print usage information and exit. 92 | printUsage; exit 93 | ;; 94 | --man) 95 | # Display the manual page and exit. 96 | openManPage; exit 97 | ;; 98 | --man-source) # private option, used by `make update-doc` 99 | # Print raw, embedded Markdown-formatted man-page source and exit 100 | printManPageSource; exit 101 | ;; 102 | --home) 103 | # Open the home page and exit. 104 | openUrl "$kTHIS_HOMEPAGE"; exit 105 | ;; 106 | esac 107 | 108 | # --- Begin: SPECIFIC HELPER FUNCTIONS 109 | 110 | # NOTE: The functions below operate on byte strings such as the one above: 111 | # A single single string of pairs of hex digits, without separators or line breaks. 112 | # Thus, a given byte position is easily calculated: to get byte $byteIndex, use 113 | # ${byteString:byteIndex*2:2} 114 | 115 | # Outputs the specified EXTENDED ATTRIBUTE VALUE as a byte string (a hex dump that is a single-line string of pairs of hex digits, without separators or line breaks, such as "000A2C". 116 | # IMPORTANT: Hex. digits > 9 use UPPPERCASE characters. 117 | # getAttribByteString 118 | getAttribByteString() { 119 | xattr -px "$2" "$1" | tr -d ' \n' 120 | return ${PIPESTATUS[0]} 121 | } 122 | 123 | # Outputs the specified file's RESOURCE FORK as a byte string (a hex dump that is a single-line string of pairs of hex digits, without separators or line breaks, such as "000a2c". 124 | # IMPORTANT: Hex. digits > 9 use *lowercase* characters. 125 | # Note: This function relies on `xxd -p /..namedfork/rsrc | tr -d '\n'` rather than the conceptually equivalent `getAttributeByteString com.apple.ResourceFork` 126 | # for PERFORMANCE reasons: getAttributeByteString() relies on `xattr`, which is a *Python* script and therefore quite slow due to Python's startup cost. 127 | # getAttribByteString 128 | getResourceByteString() { 129 | xxd -p "$1"/..namedfork/rsrc | tr -d '\n' 130 | } 131 | 132 | # Patches a single byte in the byte string provided via stdin. 133 | # patchByteInByteString ndx byteSpec 134 | # ndx is the 0-based byte index 135 | # - If has NO prefix: becomes the new byte 136 | # - If has prefix '|': "adds" the value: the result of a bitwise OR with the existing byte becomes the new byte 137 | # - If has prefix '~': "removes" the value: the result of a applying a bitwise AND with the bitwise complement of to the existing byte becomes the new byte 138 | patchByteInByteString() { 139 | local ndx=$1 byteSpec=$2 byteVal byteStr charPos op='' charsBefore='' charsAfter='' currByte 140 | byteStr=$( 0 && charPos < ${#byteStr} )) || return 1 159 | # Determine the target byte, and strings before and after the byte to patch. 160 | (( charPos >= 2 )) && charsBefore=${byteStr:0:charPos} 161 | charsAfter=${byteStr:charPos + 2} 162 | # Determine the new byte value 163 | if [[ -n $op ]]; then 164 | currByte=${byteStr:charPos:2} 165 | printf -v patchedByte '%02X' "$(( 0x${currByte} $op 0x${byteVal} ))" 166 | else 167 | patchedByte=$byteSpec 168 | fi 169 | printf '%s%s%s' "$charsBefore" "$patchedByte" "$charsAfter" 170 | } 171 | 172 | # hasAttrib 173 | hasAttrib() { 174 | xattr "$1" | /usr/bin/grep -Fqx "$2" 175 | } 176 | 177 | # hasIconsResource 178 | hasIconsResource() { 179 | local file=$1 180 | getResourceByteString "$file" | /usr/bin/grep -Fq "$kMAGICBYTES_ICNS_RESOURCE" 181 | } 182 | 183 | 184 | # setCustomIcon 185 | setCustomIcon() { 186 | 187 | local fileOrFolder=$1 imgFile=$2 byteStr tmpDir sourceFileWithResourceFork targetFileWithResourceFork 188 | 189 | [[ (-f $fileOrFolder || -d $fileOrFolder) && -r $fileOrFolder && -w $fileOrFolder ]] || return 3 190 | [[ -f $imgFile ]] || return 3 191 | 192 | # Step 1: Create the icns resource by using `sips -i` on a temporary copy of the 193 | # image file, which puts the icns resource into the resource fork of that 194 | # temporary copy. 195 | 196 | # Create a temp. dir. and a temp. copy of the image file inside it. 197 | tmpDir=$(/usr/bin/mktemp -d -t "$kTHIS_NAME") || return 198 | sourceFileWithResourceFork=$tmpDir/${imgFile##*/} 199 | cp "$imgFile" "$sourceFileWithResourceFork" || return 200 | # Assign an icon representation of the image file to the image file itself. 201 | # This creates the resource fork we'll copy. 202 | sips -i "$sourceFileWithResourceFork" >/dev/null || return 203 | 204 | # Make sure a resource fork with icons was actually created. 205 | hasIconsResource "$sourceFileWithResourceFork" || { echo "Failed to create resource fork with icons. Typically, this means that the input file is not a (valid) image file: $sourceFileWithResourceFork" >&2; return 1; } 206 | 207 | # Step 2: Add the resource fork to the file itself (if target is a file) / to the special hidden file inside the folder (if target is a folder) 208 | 209 | # Determine what file to copy the resource fork to. 210 | if [[ -d $fileOrFolder ]]; then 211 | # Create the special, hidden file inside the folder that will store the icon in its resource fork. 212 | targetFileWithResourceFork=${fileOrFolder}/$kFILENAME_FOLDERCUSTOMICON 213 | : > "$targetFileWithResourceFork" || return 214 | # Assign the com.apple.FinderInfo extended attribute with values that identify the file as 215 | # both hidden from Finder and as a custom-icon file for the enclosing folder. 216 | xattr -wx com.apple.FinderInfo "$kFI_BYTES_CUSTOMICONFILEFORFOLDER" "$targetFileWithResourceFork" || return 217 | else 218 | targetFileWithResourceFork=$fileOrFolder 219 | fi 220 | 221 | # Copy the resource fork to the target file. 222 | cp "$sourceFileWithResourceFork/..namedfork/rsrc" "$targetFileWithResourceFork/..namedfork/rsrc" || { rm -rf "$tmpDir"; return 1; } && rm -rf "$tmpDir" 223 | 224 | # Step 3: Turn on the custom-icon flag in the com.apple.FinderInfo extended attribute for the target file/folder itself. 225 | if hasAttrib "$fileOrFolder" com.apple.FinderInfo; then 226 | byteStr=$(getAttribByteString "$fileOrFolder" com.apple.FinderInfo) || return 227 | else 228 | byteStr=$kFI_BYTES_BLANK 229 | fi 230 | byteStr=$(printf %s "$byteStr" | patchByteInByteString $kFI_BYTEOFFSET_CUSTOMICON '|'$kFI_VAL_CUSTOMICON) || return 231 | xattr -wx com.apple.FinderInfo "$byteStr" "$fileOrFolder" || return 232 | 233 | return 0 234 | } 235 | 236 | # getCustomIcon 237 | getCustomIcon() { 238 | 239 | local fileOrFolder=$1 icnsOutFile=$2 byteStr fileWithResourceFork byteOffset byteCount 240 | 241 | [[ (-f $fileOrFolder || -d $fileOrFolder) && -r $fileOrFolder ]] || return 3 242 | 243 | # Determine what file to extract the resource fork from. 244 | if [[ -d $fileOrFolder ]]; then 245 | fileWithResourceFork=${fileOrFolder}/$kFILENAME_FOLDERCUSTOMICON 246 | [[ -f $fileWithResourceFork ]] || { echo "Custom-icon file does not exist: '${fileWithResourceFork/$'\r'/\\r}'" >&2; return 1; } 247 | else 248 | fileWithResourceFork=$fileOrFolder 249 | fi 250 | 251 | # Determine (based on format description at https://en.wikipedia.org/wiki/Apple_Icon_Image_format): 252 | # - the byte offset at which the icns resource begins, via the magic literal identifying an icns resource 253 | # - the length of the resource, which is encoded in the 4 bytes right after the magic literal. 254 | read -r byteOffset byteCount < <(getResourceByteString "$fileWithResourceFork" | /usr/bin/awk -F "$kMAGICBYTES_ICNS_RESOURCE" '{ printf "%s %d", (length($1) + 2) / 2, "0x" substr($2, 0, 8) }') 255 | (( byteOffset > 0 && byteCount > 0 )) || { echo "Custom-icon file contains no icons resource: '${fileWithResourceFork/$'\r'/\\r}'" >&2; return 1; } 256 | 257 | # Extract the actual bytes using tail and head and save them to the output file. 258 | tail -c "+${byteOffset}" "$fileWithResourceFork/..namedfork/rsrc" | head -c $byteCount > "$icnsOutFile" || return 259 | 260 | return 0 261 | } 262 | 263 | # removeCustomIcon 264 | removeCustomIcon() { 265 | 266 | local fileOrFolder=$1 byteStr 267 | 268 | [[ (-f $fileOrFolder || -d $fileOrFolder) && -r $fileOrFolder && -w $fileOrFolder ]] || return 1 269 | 270 | # Step 1: Turn off the custom-icon flag in the com.apple.FinderInfo extended attribute. 271 | if hasAttrib "$fileOrFolder" com.apple.FinderInfo; then 272 | byteStr=$(getAttribByteString "$fileOrFolder" com.apple.FinderInfo | patchByteInByteString $kFI_BYTEOFFSET_CUSTOMICON '~'$kFI_VAL_CUSTOMICON) || return 273 | if [[ $byteStr == "$kFI_BYTES_BLANK" ]]; then # All bytes cleared? Remove the entire attribute. 274 | xattr -d com.apple.FinderInfo "$fileOrFolder" 275 | else # Update the attribute. 276 | xattr -wx com.apple.FinderInfo "$byteStr" "$fileOrFolder" || return 277 | fi 278 | fi 279 | 280 | # Step 2: Remove the resource fork (if target is a file) / hidden file with custom icon (if target is a folder) 281 | if [[ -d $fileOrFolder ]]; then 282 | rm -f "${fileOrFolder}/${kFILENAME_FOLDERCUSTOMICON}" 283 | else 284 | if hasIconsResource "$fileOrFolder"; then 285 | xattr -d com.apple.ResourceFork "$fileOrFolder" 286 | fi 287 | fi 288 | 289 | return 0 290 | } 291 | 292 | # testForCustomIcon 293 | testForCustomIcon() { 294 | 295 | local fileOrFolder=$1 byteStr byteVal fileWithResourceFork 296 | 297 | [[ (-f $fileOrFolder || -d $fileOrFolder) && -r $fileOrFolder ]] || return 3 298 | 299 | # Step 1: Check if the com.apple.FinderInfo extended attribute has the custom-icon 300 | # flag set. 301 | byteStr=$(getAttribByteString "$fileOrFolder" com.apple.FinderInfo 2>/dev/null) || return 1 302 | 303 | byteVal=${byteStr:2*kFI_BYTEOFFSET_CUSTOMICON:2} 304 | 305 | (( byteVal & kFI_VAL_CUSTOMICON )) || return 1 306 | 307 | # Step 2: Check if the resource fork of the relevant file contains an icns resource 308 | if [[ -d $fileOrFolder ]]; then 309 | fileWithResourceFork=${fileOrFolder}/${kFILENAME_FOLDERCUSTOMICON} 310 | else 311 | fileWithResourceFork=$fileOrFolder 312 | fi 313 | 314 | hasIconsResource "$fileWithResourceFork" || return 1 315 | 316 | return 0 317 | } 318 | 319 | # --- End: SPECIFIC HELPER FUNCTIONS 320 | 321 | # --- Begin: SPECIFIC SCRIPT-GLOBAL CONSTANTS 322 | 323 | kFILENAME_FOLDERCUSTOMICON=$'Icon\r' 324 | 325 | # The blank hex dump form (single string of pairs of hex digits) of the 32-byte data structure stored in extended attribute 326 | # com.apple.FinderInfo 327 | kFI_BYTES_BLANK='0000000000000000000000000000000000000000000000000000000000000000' 328 | 329 | # The hex dump form of the full 32 bytes that Finder assigns to the hidden $'Icon\r' 330 | # file whose com.apple.ResourceFork extended attribute contains the icon image data for the enclosing folder. 331 | # The first 8 bytes spell out the magic literal 'iconMACS'; they are followed by the invisibility flag, '40' in the 9th byte, and '10' (?? specifying what?) 332 | # in the 10th byte. 333 | # NOTE: Since file $'Icon\r' serves no other purpose than to store the icon, it is 334 | # safe to simply assign all 32 bytes blindly, without having to worry about 335 | # preserving existing values. 336 | kFI_BYTES_CUSTOMICONFILEFORFOLDER='69636F6E4D414353401000000000000000000000000000000000000000000000' 337 | 338 | # The hex dump form of the magic literal inside a resource fork that marks the 339 | # start of an icns (icons) resource. 340 | # NOTE: This will be used with `xxd -p .. | tr -d '\n'`, which uses *lowercase* 341 | # hex digits, so we must use lowercase here. 342 | kMAGICBYTES_ICNS_RESOURCE='69636e73' 343 | 344 | # The byte values (as hex strings) of the flags at the relevant byte position 345 | # of the com.apple.FinderInfo extended attribute. 346 | kFI_VAL_CUSTOMICON='04' 347 | 348 | # The custom-icon-flag byte offset in the com.apple.FinderInfo extended attribute. 349 | kFI_BYTEOFFSET_CUSTOMICON=8 350 | 351 | # --- End: SPECIFIC SCRIPT-GLOBAL CONSTANTS 352 | 353 | # Option defaults. 354 | force=0 quiet=0 355 | 356 | # --- Begin: OPTIONS PARSING 357 | allowOptsAfterOperands=1 operands=() i=0 optName= isLong=0 prefix= optArg= haveOptArgAttached=0 haveOptArgAsNextArg=0 acceptOptArg=0 needOptArg=0 358 | while (( $# )); do 359 | if [[ $1 =~ ^(-)[a-zA-Z0-9]+.*$ || $1 =~ ^(--)[a-zA-Z0-9]+.*$ ]]; then # an option: either a short option / multiple short options in compressed form or a long option 360 | prefix=${BASH_REMATCH[1]}; [[ $prefix == '--' ]] && isLong=1 || isLong=0 361 | for (( i = 1; i < (isLong ? 2 : ${#1}); i++ )); do 362 | acceptOptArg=0 needOptArg=0 haveOptArgAttached=0 haveOptArgAsNextArg=0 optArgAttached= optArgOpt= optArgReq= 363 | if (( isLong )); then # long option: parse into name and, if present, argument 364 | optName=${1:2} 365 | [[ $optName =~ ^([^=]+)=(.*)$ ]] && { optName=${BASH_REMATCH[1]}; optArgAttached=${BASH_REMATCH[2]}; haveOptArgAttached=1; } 366 | else # short option: *if* it takes an argument, the rest of the string, if any, is by definition the argument. 367 | optName=${1:i:1}; optArgAttached=${1:i+1}; (( ${#optArgAttached} >= 1 )) && haveOptArgAttached=1 368 | fi 369 | (( haveOptArgAttached )) && optArgOpt=$optArgAttached optArgReq=$optArgAttached || { (( $# > 1 )) && { optArgReq=$2; haveOptArgAsNextArg=1; }; } 370 | # ---- BEGIN: CUSTOMIZE HERE 371 | case $optName in 372 | f|force) 373 | force=1 374 | ;; 375 | q|quiet) 376 | quiet=1 377 | ;; 378 | *) 379 | dieSyntax "Unknown option: ${prefix}${optName}." 380 | ;; 381 | esac 382 | # ---- END: CUSTOMIZE HERE 383 | (( needOptArg )) && { (( ! haveOptArgAttached && ! haveOptArgAsNextArg )) && dieSyntax "Option ${prefix}${optName} is missing its argument." || (( haveOptArgAsNextArg )) && shift; } 384 | (( acceptOptArg || needOptArg )) && break 385 | done 386 | else # an operand 387 | if [[ $1 == '--' ]]; then 388 | shift; operands+=( "$@" ); break 389 | elif (( allowOptsAfterOperands )); then 390 | operands+=( "$1" ) # continue 391 | else 392 | operands=( "$@" ) 393 | break 394 | fi 395 | fi 396 | shift 397 | done 398 | (( "${#operands[@]}" > 0 )) && set -- "${operands[@]}"; unset allowOptsAfterOperands operands i optName isLong prefix optArgAttached haveOptArgAttached haveOptArgAsNextArg acceptOptArg needOptArg 399 | # --- End: OPTIONS PARSING: "$@" now contains all operands (non-option arguments). 400 | 401 | # Validate the command 402 | cmd=$1 403 | case $cmd in 404 | set|get|rm|test) 405 | shift 406 | ;; 407 | *) 408 | dieSyntax "Unrecognized or missing command: '$cmd'." 409 | ;; 410 | esac 411 | 412 | # Validate file operands 413 | (( $# > 0 )) || dieSyntax "Missing operand(s)." 414 | 415 | # Target file or folder. 416 | targetFileOrFolder=$1 imgFile= outFile= 417 | [[ -f $targetFileOrFolder || -d $targetFileOrFolder ]] || die "Target not found or neither file nor folder: '$targetFileOrFolder'" 418 | # Make sure the target file/folder is readable, and, unless only getting or testing for an icon are requested, writeable too. 419 | [[ -r $targetFileOrFolder ]] || die "Cannot access '$targetFileOrFolder': you do not have read permissions." 420 | [[ $cmd == 'test' || $cmd == 'get' || -w $targetFileOrFolder ]] || die "Cannot modify '$targetFileOrFolder': you do not have write permissions." 421 | 422 | # Other operands, if any, and their number. 423 | valid=0 424 | case $cmd in 425 | 'set') 426 | (( $# == 2 )) && { 427 | valid=1 428 | imgFile=$2 429 | [[ -f $imgFile && -r $imgFile ]] || die "Image file not found or not a (readable) file: $imgFile" 430 | } 431 | ;; 432 | 'rm'|'test') 433 | (( $# == 1 )) && valid=1 434 | ;; 435 | 'get') 436 | (( $# == 1 || $# == 2 )) && { 437 | valid=1 438 | outFile=$2 439 | if [[ $outFile == '-' ]]; then 440 | outFile=/dev/stdout 441 | else 442 | # By default, we extract to a file with the same filename root + '.icns' 443 | # in the current folder. 444 | [[ -z $outFile ]] && outFile=${targetFileOrFolder##*/} 445 | # Unless already specified, we append '.icns' to the output filename. 446 | mustReset=$(shopt -q nocasematch; echo $?); shopt -s nocasematch 447 | [[ $outFile =~ \.icns$ ]] || outFile+='.icns' 448 | (( mustReset )) && shopt -u nocasematch 449 | [[ -e $outFile && $force -eq 0 ]] && die "Output file '$outFile' already exists. To force its replacement, use -f." 450 | fi 451 | } 452 | ;; 453 | esac 454 | (( valid )) || dieSyntax "Unexpected number of operands." 455 | 456 | case $cmd in 457 | 'set') 458 | setCustomIcon "$targetFileOrFolder" "$imgFile" || die 459 | (( quiet )) || echo "Custom icon assigned to '$targetFileOrFolder' based on '$imgFile'." 460 | ;; 461 | 'rm') 462 | removeCustomIcon "$targetFileOrFolder" || die 463 | (( quiet )) || echo "Custom icon removed from '$targetFileOrFolder'." 464 | ;; 465 | 'get') 466 | getCustomIcon "$targetFileOrFolder" "$outFile" || die 467 | (( quiet )) || { [[ $outFile != '/dev/stdout' ]] && echo "Custom icon extracted to '$outFile'."; } 468 | exit 0 469 | ;; 470 | 'test') 471 | testForCustomIcon "$targetFileOrFolder" 472 | ec=$? 473 | (( ec <= 1 )) || die 474 | if (( ! quiet )); then 475 | (( ec == 0 )) && echo "HAS custom icon: '$targetFileOrFolder'" || echo "Has NO custom icon: '$targetFileOrFolder'" 476 | fi 477 | exit $ec 478 | ;; 479 | *) 480 | die "DESIGN ERROR: unanticipated command: $cmd" 481 | ;; 482 | esac 483 | 484 | exit 0 485 | 486 | #### 487 | # MAN PAGE MARKDOWN SOURCE 488 | # - Place a Markdown-formatted version of the man page for this script 489 | # inside the here-document below. 490 | # The document must be formatted to look good in all 3 viewing scenarios: 491 | # - as a man page, after conversion to ROFF with marked-man 492 | # - as plain text (raw Markdown source) 493 | # - as HTML (rendered Markdown) 494 | # Markdown formatting tips: 495 | # - GENERAL 496 | # To support plain-text rendering in the terminal, limit all lines to 80 chars., 497 | # and, for similar rendering as HTML, *end every line with 2 trailing spaces*. 498 | # - HEADINGS 499 | # - For better plain-text rendering, leave an empty line after a heading 500 | # marked-man will remove it from the ROFF version. 501 | # - The first heading must be a level-1 heading containing the utility 502 | # name and very brief description; append the manual-section number 503 | # directly to the CLI name; e.g.: 504 | # # foo(1) - does bar 505 | # - The 2nd, level-2 heading must be '## SYNOPSIS' and the chapter's body 506 | # must render reasonably as plain text, because it is printed to stdout 507 | # when `-h`, `--help` is specified: 508 | # Use 4-space indentation without markup for both the syntax line and the 509 | # block of brief option descriptions; represent option-arguments and operands 510 | # in angle brackets; e.g., '' 511 | # - All other headings should be level-2 headings in ALL-CAPS. 512 | # - TEXT 513 | # - Use NO indentation for regular chapter text; if you do, it will 514 | # be indented further than list items. 515 | # - Use 4-space indentation, as usual, for code blocks. 516 | # - Markup character-styling markup translates to ROFF rendering as follows: 517 | # `...` and **...** render as bolded (red) text 518 | # _..._ and *...* render as word-individually underlined text 519 | # - LISTS 520 | # - Indent list items by 2 spaces for better plain-text viewing, but note 521 | # that the ROFF generated by marked-man still renders them unindented. 522 | # - End every list item (bullet point) itself with 2 trailing spaces too so 523 | # that it renders on its own line. 524 | # - Avoid associating more than 1 paragraph with a list item, if possible, 525 | # because it requires the following trick, which hampers plain-text readability: 526 | # Use ' ' in lieu of an empty line. 527 | #### 528 | : <<'EOF_MAN_PAGE' 529 | # fileicon(1) - manage file and folder custom icons 530 | 531 | ## SYNOPSIS 532 | 533 | Manage custom icons for files and folders on OS X. 534 | 535 | SET a custom icon for a file or folder: 536 | 537 | fileicon set 538 | 539 | REMOVE a custom icon from a file or folder: 540 | 541 | fileicon rm 542 | 543 | GET a file or folder's custom icon: 544 | 545 | fileicon get [-f] [] 546 | 547 | -f ... force replacement of existing output file 548 | 549 | TEST if a file or folder has a custom icon: 550 | 551 | fileicon test 552 | 553 | All forms: option -q silences status output. 554 | 555 | Standard options: `--help`, `--man`, `--version`, `--home` 556 | 557 | ## DESCRIPTION 558 | 559 | `` is the file or folder whose custom icon should be managed. 560 | Note that symlinks are followed to their (ultimate target); that is, you 561 | can only assign custom icons to regular files and folders, not to symlinks 562 | to them. 563 | 564 | `` can be an image file of any format supported by the system. 565 | It is converted to an icon and assigned to ``. 566 | 567 | `` specifies the file to extract the custom icon to: 568 | Defaults to the filename of `` with extension `.icns` appended. 569 | If a value is specified, extension `.icns` is appended, unless already present. 570 | Either way, extraction fails if the target file already exists; use `-f` to 571 | override. 572 | Specify `-` to extract to stdout. 573 | 574 | Command `test` signals with its exit code whether a custom icon is set (0) 575 | or not (1); any other exit code signals an unexpected error. 576 | 577 | **Options**: 578 | 579 | * `-f`, `--force` 580 | When getting (extracting) a custom icon, forces replacement of the 581 | output file, if it already exists. 582 | 583 | * `-q`, `--quiet` 584 | Suppresses output of the status information that is by default output to 585 | stdout. 586 | Note that errors and warnings are still printed to stderr. 587 | 588 | ## NOTES 589 | 590 | Custom icons are stored in extended attributes of the HFS+ filesystem. 591 | Thus, if you copy files or folders to a different filesystem that doesn't 592 | support such attributes, custom icons are lost; for instance, custom icons 593 | cannot be stored in a Git repository. 594 | 595 | To determine if a give file or folder has extended attributes, use 596 | `ls -l@ `. 597 | 598 | When setting a custom icon, the source image is resized to 128 x 128 pixels 599 | and stored as a single icon, which the system resizes dynamically, depending 600 | on context. 601 | Currently, even if the source image file is itself an `.icns` file that 602 | contains multiple icons with distinct resolutions, only the 128 x 128 icon 603 | is assigned. 604 | 605 | ## STANDARD OPTIONS 606 | 607 | All standard options provide information only. 608 | 609 | * `-h, --help` 610 | Prints the contents of the synopsis chapter to stdout for quick reference. 611 | 612 | * `--man` 613 | Displays this manual page, which is a helpful alternative to using `man`, 614 | if the manual page isn't installed. 615 | 616 | * `--version` 617 | Prints version information. 618 | 619 | * `--home` 620 | Opens this utility's home page in the system's default web browser. 621 | 622 | ## LICENSE 623 | 624 | For license information and more, visit the home page by running 625 | `fileicon --home` 626 | 627 | EOF_MAN_PAGE -------------------------------------------------------------------------------- /bin/fileicon.webloc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johntmcintosh/xcunsign/8abddb1a088f019f6dba88b88e77abd771490473/bin/fileicon.webloc -------------------------------------------------------------------------------- /bin/unsign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johntmcintosh/xcunsign/8abddb1a088f019f6dba88b88e77abd771490473/bin/unsign -------------------------------------------------------------------------------- /bin/unsign.webloc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johntmcintosh/xcunsign/8abddb1a088f019f6dba88b88e77abd771490473/bin/unsign.webloc -------------------------------------------------------------------------------- /lib/helpers.sh: -------------------------------------------------------------------------------- 1 | # Common helper functions for working with Xcode versions 2 | 3 | RED='\033[0;31m' 4 | GREEN='\033[0;32m' 5 | NC='\033[0m' # No Color 6 | 7 | # Echo list of all installed Xcode versions 8 | list_installed_xcode_versions() { 9 | for i in /Applications/Xcode* 10 | do 11 | VERSION=`mdls -name kMDItemVersion "$i" | sed 's/kMDItemVersion = /Xcode /g' | sed 's/"//g'` 12 | echo $VERSION >&2 13 | done 14 | } 15 | 16 | # Echo path to Xcode version matching the version provided in the first calling argument 17 | xcode_app_path_for_version () { 18 | TARGET_VERSION=$1 19 | 20 | for i in /Applications/Xcode* 21 | do 22 | VERSION=`mdls -name kMDItemVersion "$i" | sed 's/kMDItemVersion = //g' | sed 's/"//g'` 23 | if [ $VERSION == $TARGET_VERSION ]; then 24 | echo $i 25 | return 26 | fi 27 | done 28 | 29 | local RED='\033[0;31m' 30 | local NC='\033[0m' # No Color 31 | echo "${RED}Unable to find an Xcode version matching: $TARGET_VERSION${NC}" >&2 32 | echo "Installed versions:" >&2; list_installed_xcode_versions 33 | return 1 34 | } 35 | -------------------------------------------------------------------------------- /lib/input.sh: -------------------------------------------------------------------------------- 1 | # Common script for parsing the command line input and generating 2 | # the variables that will represent the paths for the signed 3 | # and unsigned binaries. 4 | 5 | # Get the source directory to use for relative paths when referencing other scripts 6 | CALLING_DIR="${BASH_SOURCE%/*}" 7 | ROOT_DIR=$(dirname $CALLING_DIR) 8 | 9 | # Include helpers 10 | source "$ROOT_DIR/lib/helpers.sh" 11 | 12 | # Require an input version to be passed as the first parameter to the script. 13 | INPUT_VERSION=$1 14 | if [ -z $INPUT_VERSION ]; then 15 | echo "${RED}No version argument supplied.${NC}" 16 | echo "Please pass the desired Xcode version as an input parameter. For example './xcunsign.sh 8.0'" 17 | echo "TODO: Update this to use the current Xcode version if there is only one." 18 | exit 1 19 | fi 20 | 21 | # Get the path to the specified version of Xcode 22 | XCODE_PATH=`xcode_app_path_for_version $INPUT_VERSION` 23 | if [ -z $XCODE_PATH ]; then 24 | exit 1 25 | fi 26 | echo "Xcode version $INPUT_VERSION found in $XCODE_PATH" 27 | 28 | # Setup the paths that we'll be using 29 | BINARY_PATH="$XCODE_PATH/Contents/MacOS/Xcode" 30 | SIGNED_PATH="$BINARY_PATH.signed" 31 | UNSIGNED_PATH="$BINARY_PATH.unsigned" 32 | -------------------------------------------------------------------------------- /lib/verify_codesign.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Reference: https://github.com/fastlane/fastlane/blob/master/fastlane/lib/fastlane/actions/verify_xcode.rb 3 | 4 | def verify_codesign(params) 5 | 6 | path = params[:xcode_path] 7 | puts "Verifying signature for #{path}" 8 | codesign_output = %x[ codesign --display --verbose=4 #{path} 2>&1 ] 9 | 10 | # If the returned codesign info contains all entries for any one of these sets, we'll consider it valid 11 | accepted_codesign_detail_sets = [ 12 | [ # Found on App Store installed Xcode installations 13 | "Identifier=com.apple.dt.Xcode", 14 | "Authority=Apple Mac OS Application Signing", 15 | "Authority=Apple Worldwide Developer Relations Certification Authority", 16 | "Authority=Apple Root CA", 17 | "TeamIdentifier=59GAB85EFG" 18 | ], 19 | [ # Found on Xcode installations (pre-Xcode 8) downloaded from developer.apple.com 20 | "Identifier=com.apple.dt.Xcode", 21 | "Authority=Software Signing", 22 | "Authority=Apple Code Signing Certification Authority", 23 | "Authority=Apple Root CA", 24 | "TeamIdentifier=not set" 25 | ], 26 | [ # Found on Xcode installations (post-Xcode 8) downloaded from developer.apple.com 27 | "Identifier=com.apple.dt.Xcode", 28 | "Authority=Software Signing", 29 | "Authority=Apple Code Signing Certification Authority", 30 | "Authority=Apple Root CA", 31 | "TeamIdentifier=59GAB85EFG" 32 | ] 33 | ] 34 | 35 | # Map the accepted details sets into an equal number of sets collecting the details for which 36 | # the output of codesign did not have matches 37 | missing_details_sets = accepted_codesign_detail_sets.map do |accepted_details_set| 38 | accepted_details_set.reject { |detail| codesign_output.include?(detail) } 39 | end 40 | 41 | validated = missing_details_sets.any? { |set| set.empty? } 42 | puts "Codesigned: #{validated}" 43 | return validated ? 0 : 1 44 | 45 | end 46 | 47 | exit verify_codesign(xcode_path: ARGV[0]) 48 | -------------------------------------------------------------------------------- /resources/XcodeUnsigned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johntmcintosh/xcunsign/8abddb1a088f019f6dba88b88e77abd771490473/resources/XcodeUnsigned.png -------------------------------------------------------------------------------- /resources/readme_XcodeUnsigned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johntmcintosh/xcunsign/8abddb1a088f019f6dba88b88e77abd771490473/resources/readme_XcodeUnsigned.png -------------------------------------------------------------------------------- /xcrestore: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### 3 | # Home page: https://github.com/johntmcintosh/xcunsign/ 4 | # Author: John T McIntosh (http://johntmcintosh.com) 5 | ### 6 | 7 | MAC_OS_VERSION=$(sw_vers -productVersion) 8 | MAC_OS_SIERRA="10.12" 9 | if [ $(bc <<< "$MAC_OS_VERSION < $MAC_OS_SIERRA") -eq 1 ] 10 | then 11 | echo "MacOS version checked and error handling activated" 12 | set -o errexit # Exit if any statement returns a non-true return value (non-zero). 13 | set -o nounset # Exit on use of an uninitialized variable 14 | fi 15 | 16 | # Get the source directory to use for relative paths when referencing other scripts 17 | CALLING_DIR="${BASH_SOURCE%/*}" 18 | BINARY=`readlink "$CALLING_DIR/xcunsign"` 19 | if [ -n "$BINARY" ]; then 20 | ROOT_DIR=$(dirname $BINARY) 21 | else 22 | ROOT_DIR=$CALLING_DIR 23 | fi 24 | 25 | # Parse the command line input 26 | source "$ROOT_DIR/lib/input.sh" 27 | 28 | # If the app is already signed, there's nothing that needs to be done 29 | if $ROOT_DIR/lib/verify_codesign.rb "$BINARY_PATH"; then 30 | echo "${GREEN}$BINARY_PATH is already signed.${NC}" 31 | exit 0 32 | fi 33 | 34 | # Ensure that the app has a signed copy that can be restored 35 | # If the return code of the verify_codesign call is 0, then 36 | # we have a legitimately signed binary. 37 | if ! $ROOT_DIR/lib/verify_codesign.rb "$SIGNED_PATH"; then 38 | echo "${RED}There no file at ${SIGNED_PATH} to restore.${NC}" 39 | exit 1 40 | fi 41 | 42 | # Delete the existing binary, and replace it with the signed copy 43 | echo "Deleting unsigned Xcode binary." 44 | rm "$BINARY_PATH" 45 | 46 | echo "Setting $BINARY_PATH to signed copy" 47 | mv "$SIGNED_PATH" "$BINARY_PATH" 48 | 49 | echo "Signed binary restored." 50 | 51 | # Restore Xcode's app icon to the standard icon 52 | $ROOT_DIR/bin/fileicon rm $XCODE_PATH 53 | killall Finder 54 | killall Dock 55 | 56 | echo "${GREEN}Xcode $INPUT_VERSION signature has been restored.${NC}" 57 | -------------------------------------------------------------------------------- /xcunsign: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### 3 | # Home page: https://github.com/johntmcintosh/xcunsign/ 4 | # Author: John T McIntosh (http://johntmcintosh.com) 5 | ### 6 | 7 | MAC_OS_VERSION=$(sw_vers -productVersion) 8 | MAC_OS_SIERRA="10.12" 9 | if [ $(bc <<< "$MAC_OS_VERSION < $MAC_OS_SIERRA") -eq 1 ] 10 | then 11 | echo "MacOS version checked and error handling activated" 12 | set -o errexit # Exit if any statement returns a non-true return value (non-zero). 13 | set -o nounset # Exit on use of an uninitialized variable 14 | fi 15 | 16 | # Get the source directory to use for relative paths when referencing other scripts 17 | CALLING_DIR="${BASH_SOURCE%/*}" 18 | BINARY=`readlink "$CALLING_DIR/xcunsign"` 19 | if [ -n "$BINARY" ]; then 20 | ROOT_DIR=$(dirname $BINARY) 21 | else 22 | ROOT_DIR=$CALLING_DIR 23 | fi 24 | 25 | # Parse the command line input 26 | source "$ROOT_DIR/lib/input.sh" 27 | 28 | # If the app is already unsigned, there's nothing that needs to be done 29 | if ! $ROOT_DIR/lib/verify_codesign.rb "$BINARY_PATH"; then 30 | echo "${GREEN}$BINARY_PATH is already unsigned.${NC}" 31 | exit 0 32 | fi 33 | 34 | # echo "Executing unsign now..." 35 | echo "The app is currently signed. Executing unsign now..." 36 | $ROOT_DIR/bin/unsign $BINARY_PATH $UNSIGNED_PATH 37 | 38 | # Rename the original binary so it can later be restored. 39 | echo "Renaming Xcode to Xcode.signed." 40 | mv "$BINARY_PATH" "$SIGNED_PATH" 41 | 42 | # Rename the unsigned binary so it will be primary 43 | echo "Renaming Xcode.unsigned to Xcode." 44 | mv "$UNSIGNED_PATH" "$BINARY_PATH" 45 | 46 | # Set Xcode's app icon to the unsigned icon 47 | $ROOT_DIR/bin/fileicon set $XCODE_PATH $ROOT_DIR/resources/XcodeUnsigned.png 48 | killall Finder 49 | killall Dock 50 | 51 | echo "${GREEN}Xcode $INPUT_VERSION has been unsigned.${NC}" 52 | --------------------------------------------------------------------------------