In the unit tests for my app, I create an document-scoped NSURL
bookmark. These tests have always worked correctly on my machine (and still do), but are now failing when run on an Xcode Server bot. I don't codesign the unit test bundle.
- (void)testBookmarks
{
// Create testing directory
NSFileManager *fm = [NSFileManager defaultManager];
NSString *sourceDir = [fm currentDirectoryPath];
NSString *testingDir = [sourceDir stringByAppendingPathComponent:@"~testing dir"];
if ([fm fileExistsAtPath:testingDir]) {
[fm removeItemAtPath:testingDir error:NULL];
}
[fm createDirectoryAtPath:testingDir
withIntermediateDirectories:NO
attributes:nil
error:NULL];
// Create file to create bookmark to
NSString *bookmarkedFilePath = [testingDir stringByAppendingPathComponent:@"fileToBookmark.txt"];
[fm createFileAtPath:bookmarkedFilePath
contents:nil
attributes:nil];
NSURL *originalURL = [NSURL fileURLWithPath:bookmarkedFilePath];
// Create file to create bookmark relative to
NSString *relativeFilePath = [testingDir stringByAppendingPathComponent:@"relativeToFile.txt"];
[fm createFileAtPath:relativeFilePath
contents:nil
attributes:nil];
// Create a document-scoped bookmark
NSError *docScopedError = nil;
NSURL *relativeToURL = [NSURL fileURLWithPath:relativeFilePath];
NSData *bookmark = [originalURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:relativeToURL
error:&docScopedError];
// Assert everything went well
XCTAssertNil(docScopedError, @"Error while creating document-scoped bookmark from URL:\n%@\nrelative to: %@",
originalURL, relativeToURL);
XCTAssertNotNil(bookmark, @"No bookmark created to URL:\n%@\nrelative to: %@",
originalURL, relativeToURL);
}
Both assertions fail, and the message that gets logged verifies that both URLs are not nil, and I was able to verify that both files do exist on disk. They are both contained within the Git checkout directory, which the account has full access to. The relativeToUrl
points to a file created earlier in the test.
The NSError
produced has the following info:
"Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened." (Item URL disallowed by security policy) UserInfo=0x10691c6d0 {NSDebugDescription=Item URL disallowed by security policy}"
What security policy could it be referring to, and how would I update it? Again, all of this works fine on my local development machine.
Update
I created a demo project, and pushed it to GitHub. Feel free to create your own Xcode Bot that pulls from there to see if you can reproduce. I was able to reproduce with a clean OS X, Xcode, and Server installation.
Here is what I learned (so far) from my discussion with a DTS team member.
Security-scoped bookmarks aren't meant to work with non-sandboxed (and non-codesigned) apps. My unit tests are not signed or sandboxed (and doing so introduced other problems), but they do work, regardless. They may break when the next OS X release comes out, but time will tell there
Apparently, there are certain directories that the security-scoping mechanism considers off-limits. I'm going to start a list below (given the absence of documentation), and update it as I find more exceptions (please do the same).
/Library
~/Library
/private/var
/var
/tmp
My unit tests created a directory inside the checkout location, and create a bookmark to one of those files. Since my local Xcode checkout is in ~/Source Code
, it worked fine. Xcode Server, however, checkout out to /Library/Developer/XcodeServer/Caches/...
, which is what caused problems.
I updated my sample GitHub project with my implementation that works around this. In a nutshell, I have the following code in my unit test:
NSString *envVarTestingDir = [[NSProcessInfo processInfo].environment objectForKey:@"UNIT_TESTING_DIR"];
NSString *sourceRelativeDir = [[fm currentDirectoryPath] stringByAppendingPathComponent:@"~testing dir"];
NSString *testingDirPath = [envVarTestingDir length] > 0 ? envVarTestingDir : sourceRelativeDir;
And then, I have a separate scheme for CI builds, which defines the UNIT_TESTING_DIR
environment variable to be /Users/Shared/~testing dir
, since that is a globally accessible location. That way, I get my local builds to write to my preferred location, but CI builds don't fail. Win/win!
PS
Apparently, Xcode Server reassigns the directory returned by
NSHomeDirectory()
to be/var/_xcsbuildd
, which is off-limits.