Matt Jacobson
Happy new year.
Here's something annoying. When you unarchive a xip
archive, like the ones
that Xcode releases come in, the unarchive happens to a temporary directory that
lives on the root volume, even if the archive doesn't, and even if the result of
the unarchive is ultimately going back to the secondary volume.
So, for example, if you are unarchiving /Volumes/ExternalHDD/Xcode.xip
, the
files get unarchived to /private/var/folders/foo/bar/T/<some UUID>
, and then
the resultant Xcode.app
goes back to /Volumes/ExternalHDD/Xcode.app
.
This happens if you use Archive Utility. This happens if you use the xip
command-line tool.
It's all because PackageKit, the framework that underlies both tools, is lazy
and uses NSTemporaryDirectory()
to generate a temporary working directory. It
should really be using
-[NSFileManager URLForDirectory:inDomain:appropriateForURL:create:error:]
with
NSItemReplacementDirectory
,
but to be fair that API only came around recently, you know, in 2009.
Anyway, this is dumb for several reasons. It results in extra copies, sure.
But the whole reason I put Xcode.xip
on an external disk is that I can't spare
the half a terabyte or whatever that out-of-box Xcode installs take up now. Not
on my internal disk.
This sort of thing happens all over the place in Apple's installer stack. I guess they consider external disks a corner case that doesn't merit testing.
Working around this is pretty easy. Just set a breakpoint on
NSTemporaryDirectory()
and return a more appropriate working directory. In my
case, I set up a /Volumes/ExternalHDD/tmp/
and then did this:
$ lldb -- xip --expand Xcode_13.4.1.xip
(lldb) target create "xip"
Current executable set to 'xip' (x86_64).
(lldb) settings set -- target.run-args "--expand" "Xcode_13.4.1.xip"
(lldb) b NSTemporaryDirectory
Breakpoint 1: where = Foundation`NSTemporaryDirectory, address = 0x00007fff211727c7
(lldb) run
Process 4370 launched: '/usr/bin/xip' (x86_64)
xip: signing certificate was "Software Update" (validation not attempted)
Process 4370 stopped
* thread #3, queue = 'com.apple.root.default-qos', stop reason = breakpoint 1.1
frame #0: 0x00007fff211837c7 Foundation`NSTemporaryDirectory
Foundation`NSTemporaryDirectory:
-> 0x7fff211837c7 <+0>: pushq %rbp
0x7fff211837c8 <+1>: movq %rsp, %rbp
0x7fff211837cb <+4>: pushq %r15
0x7fff211837cd <+6>: pushq %r14
Target 0: (xip) stopped.
(lldb) bt
* thread #3, queue = 'com.apple.root.default-qos', stop reason = breakpoint 1.1
* frame #0: 0x00007fff211837c7 Foundation`NSTemporaryDirectory
frame #1: 0x00007fff3b95dd83 PackageKit`-[PKSignedContainer _startUnarchivingAtPath:cancelHandler:notifyOnQueue:progress:finish:] + 92
frame #2: 0x00007fff3b95f657 PackageKit`__74-[PKSignedContainer startUnarchivingAtPath:notifyOnQueue:progress:finish:]_block_invoke + 69
frame #3: 0x00007fff20174623 libdispatch.dylib`_dispatch_call_block_and_release + 12
frame #4: 0x00007fff20175806 libdispatch.dylib`_dispatch_client_callout + 8
frame #5: 0x00007fff20177e37 libdispatch.dylib`_dispatch_queue_override_invoke + 775
frame #6: 0x00007fff20184818 libdispatch.dylib`_dispatch_root_queue_drain + 326
frame #7: 0x00007fff20184f70 libdispatch.dylib`_dispatch_worker_thread2 + 92
frame #8: 0x00007fff2031c417 libsystem_pthread.dylib`_pthread_wqthread + 244
frame #9: 0x00007fff2031b42f libsystem_pthread.dylib`start_wqthread + 15
(lldb) p (id)[@"/Volumes/ExternalHDD/tmp/" copyWithZone:0]
(__NSCFString *) $0 = 0x000000010020a260 @"/Volumes/ExternalHDD/tmp/"
(lldb) thread return 0x000000010020a260
* thread #3, queue = 'com.apple.root.default-qos', stop reason = breakpoint 1.1
frame #0: 0x00007fff3b95dd83 PackageKit`-[PKSignedContainer _startUnarchivingAtPath:cancelHandler:notifyOnQueue:progress:finish:] + 92
PackageKit`-[PKSignedContainer _startUnarchivingAtPath:cancelHandler:notifyOnQueue:progress:finish:]:
-> 0x7fff3b95dd83 <+92>: testq %rax, %rax
0x7fff3b95dd86 <+95>: movq %r14, -0xae8(%rbp)
0x7fff3b95dd8d <+102>: je 0x7fff3b95e1db ; <+1204>
0x7fff3b95dd93 <+108>: movq %rax, %rbx
(lldb) c
Process 4370 resuming
It only ever asks once, and it seems always to be the first hit of the breakpoint. So pleasant! And then it does exactly what you would want.