├── .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 |
--------------------------------------------------------------------------------