116 Commits
Author SHA1 Message Date
Justin Spahr-Summers 33137bcae6 Fix update directories not being pruned (bad merge) 2014-01-24 23:09:33 -08:00
Justin Spahr-Summers 532c565b2a Need to -flattenMap: to the list of directories 2014-01-24 23:07:24 -08:00
Justin Spahr-Summers 9f5a0761ae Ignore the RACUnit we use to hack lazy memoization 2014-01-24 23:03:48 -08:00
Justin Spahr-Summers 572ef9aa9b Update RACAction usage, remove RACPromises 2014-01-24 23:01:00 -08:00
Justin Spahr-Summers b8aef91738 Merge branch 'master' into rac-3.0
Conflicts:
	External/ReactiveCocoa
	Squirrel/SQRLShipItLauncher.m
	Squirrel/SQRLUpdater.m
2014-01-24 22:48:31 -08:00
Josh Abernathy 36e0531218 Merge pull request #104 from Squirrel/dont-crash-with-no-signature
Log when we can’t get the running application code signature
2014-01-22 11:18:39 -08:00
Keith Duncan 77aec66456 Throw an exception when there’s no code signature in release 2014-01-22 11:08:31 -08:00
Keith Duncan e8523fd74d Log when we can’t get the running application code signature
Don’t crash.
2014-01-22 10:37:58 -08:00
Justin Spahr-Summers 767892c788 ++RAC to fix RACCommand KVO crashes 2014-01-21 14:36:05 -08:00
Rob Rix 34e37569d7 Merge pull request #103 from Squirrel/prune-old-updates
Prune temporary directories before performing first update check
2014-01-21 14:25:55 -08:00
Justin Spahr-Summers 4c93cb4508 Remove update directories before first check of any kind
Not just before beginning automatic updates.
2014-01-21 12:26:43 -08:00
Justin Spahr-Summers 431224c788 Test that updates are cleaned up 2014-01-21 12:16:35 -08:00
Justin Spahr-Summers ea8f805825 Remove old update directories before checking for updates 2014-01-21 11:55:52 -08:00
Justin Spahr-Summers 7bb4c131e1 Remove downloaded zips after unarchiving them 2014-01-21 11:43:32 -08:00
Justin Spahr-Summers 372a7d783a Merge pull request #101 from Squirrel/update-readme
Add steps to Readme
2014-01-20 15:57:27 -08:00
Justin Spahr-Summers b85e03fbe9 Minor ordering/phrasing tweaks 2014-01-20 15:56:31 -08:00
probablycorey 48455f1ee4 Add steps to get Squirrel to build 2014-01-20 15:47:46 -08:00
Justin Spahr-Summers 5cd5fdb2ed Merge pull request #97 from Squirrel/bridge-authorization
Don’t `__bridge` cast the `AuthorizationRef`
2014-01-07 10:49:48 -08:00
Keith Duncan aa545ed419 Merge remote-tracking branch 'origin/master' into bridge-authorization
Conflicts:
	External/ReactiveCocoa
2014-01-07 18:06:35 +00:00
Keith Duncan 3d507a58a6 Check that _authorization is non NULL before freeing 2014-01-07 17:25:54 +00:00
Justin Spahr-Summers 406728c066 ++RAC 2014-01-06 12:09:29 -08:00
Keith Duncan 53adc17554 Types++ 2014-01-06 12:05:58 +00:00
Keith Duncan 0fb9ed48c9 Merge remote-tracking branch 'origin/master' into bridge-authorization
Conflicts:
	Squirrel/SQRLShipItLauncher.m
2014-01-06 12:04:39 +00:00
Keith Duncan 1db648059a Wrap the authorization in an Objective-C object for lifetime management 2014-01-06 12:01:12 +00:00
Keith Duncan fccf29122d Don’t __bridge cast the AuthorizationRef 2014-01-03 20:52:33 +00:00
Matt Diephouse 502c96a6df ++RAC 2013-12-30 14:04:02 -05:00
Justin Spahr-Summers edb4aafa31 Merge pull request #93 from Squirrel/replace-then
Remove uses of -then: and -skipUntilBlock:
2013-12-23 20:58:01 -08:00
Josh Abernathy 90a6a2fe16 Merge pull request #94 from Squirrel/update-mantle-urls
Update URLs to Mantle repository
2013-12-13 14:57:10 -08:00
Justin Spahr-Summers c2869a05e9 Update URLs to Mantle repository
This change isn’t strictly necessary due to automatic redirection, but
let’s keep our links fresh.
2013-12-13 12:55:10 -08:00
Justin Spahr-Summers de276d73a6 Remove uses of -then: and -skipUntilBlock: 2013-12-06 23:48:06 -08:00
Justin Spahr-Summers 7655bd0a90 ++RAC 2013-12-06 23:40:17 -08:00
Justin Spahr-Summers e9a5874339 Merge pull request #89 from Squirrel/rac-3.0++
More RAC 3.0 support
2013-12-06 10:04:49 -08:00
Justin Spahr-Summers aa69586334 Merge pull request #90 from Squirrel/note-rac-dependency
Note that we're dependent on ReactiveCocoa and Mantle
2013-12-06 10:03:12 -08:00
Keith Duncan 8f3f5a5a72 Revert change to ReactiveCocoa pin 2013-12-06 12:53:55 +00:00
Keith Duncan f48ccd2a21 Float the version number dependency
Saves updating this when Squirrel switches to 3.0 too
2013-12-06 12:44:45 +00:00
Keith Duncan 1ac9c4ddc1 Separate the capturing of the current request from the issuing 2013-12-06 12:40:47 +00:00
Keith Duncan 9907d44f40 Complete the updates subject in dealloc 2013-12-06 12:36:03 +00:00
Keith Duncan e21fd640b9 Commas-- 2013-12-05 14:14:01 +00:00
Keith Duncan 7bb7799b17 Change version dependency to 2.0 2013-12-05 14:13:33 +00:00
Keith Duncan b5bbdfbed0 Don’t use dot syntax for deferred 2013-12-05 12:58:21 +00:00
Keith Duncan 28079454dc Don’t eagerly start the relaunch promise 2013-12-05 12:49:49 +00:00
Keith Duncan 210e09e079 Use the action signal to connect if needed, rather than execute: 2013-12-05 12:43:43 +00:00
Keith Duncan f0161a51d9 Name the updater updates subject 2013-12-05 12:42:11 +00:00
Keith Duncan d121f4cf7f Remove temporary variable 2013-12-05 12:41:45 +00:00
Keith Duncan 02bb683cf7 Hoist the NSURLConnection signal chain out of +defer:
The defer signal now takes the updateRequest at time of subscribe and passes it to the new method.
2013-12-05 12:40:20 +00:00
Keith Duncan d64a09bb2b Fix scoped instance variable access for @strong/@weak modifications 2013-12-05 12:32:29 +00:00
Keith Duncan 5f201f833e Remove comment 2013-12-05 12:29:03 +00:00
Keith Duncan 6e1b3c9ded Complete the updates signal when the updater is dealloc’d 2013-12-05 12:28:38 +00:00
Keith Duncan 13a35d195b Clarify that the host application must embed the dependencies. 2013-12-04 12:28:13 +00:00
Keith Duncan 0c95e0c8c4 Note that we're dependent on ReactiveCocoa and Mantle
The client is responsible for including these because we don’t embed them.
2013-12-03 18:21:48 +00:00
Keith Duncan 656decb128 Fix interval checks, the API is a subscription not a signal 2013-12-03 12:39:54 +00:00
Keith Duncan 4ef110e84b Update to new RACAction API 2013-12-03 12:39:27 +00:00
Keith Duncan a22ebbfdb9 Use RACAction for checkForUpdates 2013-12-03 12:34:56 +00:00
Keith Duncan 8cdf3d0666 Fix installer test with new RAC 3.0 API changes 2013-12-03 12:34:42 +00:00
Keith Duncan ff6a2e1cb7 Remove RACSequence for the log locations 2013-12-02 16:57:14 +00:00
Keith Duncan c3a41e9032 Build the tests when building ShipIt 2013-12-02 16:11:10 +00:00
Keith Duncan 27eb479de4 Remove RACSequence, multicast and replay uses 2013-12-02 16:10:03 +00:00
Keith Duncan aab4ca3efb Remove -initially: 2013-12-02 15:54:03 +00:00
Keith Duncan 6bc3b0d466 Replace the commands with methods
I’m not sure if this is a match for `RACAction` because it needs the argument from the caller/subscriber.
2013-12-02 15:45:07 +00:00
Keith Duncan ef83ed3175 Rename -finally: to -doFinished: 2013-12-02 15:09:57 +00:00
Keith Duncan c382fbce40 Remove NSEnumerator.rac_sequence 2013-12-02 15:09:22 +00:00
Keith Duncan d0498c528e ReactiveCocoa++
Pin to working ReactiveCocoa-Mac target
2013-12-02 15:09:04 +00:00
Justin Spahr-Summers e5c65e530f Merge pull request #88 from Squirrel/skip-install
SKIP_INSTALL for ShipIt
2013-11-27 10:25:49 -08:00
Keith Duncan 65f191ce30 SKIP_INSTALL for ShipIt 2013-11-27 12:34:13 +00:00
Justin Spahr-Summers cb2502510b Update SQRLShipItLauncher 2013-11-26 12:49:25 -08:00
Justin Spahr-Summers 49a8f3b624 Update SQRLZipArchiver 2013-11-26 12:48:28 -08:00
Justin Spahr-Summers 36e3b17abb Update SQRLTerminationListener 2013-11-26 12:47:55 -08:00
Justin Spahr-Summers eb49de653b Update SQRLCodeSignature 2013-11-26 12:46:53 -08:00
Justin Spahr-Summers 35e5a03130 Update RACSignal transaction extensions 2013-11-26 12:45:17 -08:00
Justin Spahr-Summers b4387c312a ++RAC to 3.0-development 2013-11-26 12:42:40 -08:00
Justin Spahr-Summers 2388fa3475 Merge pull request #83 from Squirrel/fix-test-races
Fix race conditions in tests
2013-11-19 11:27:42 -08:00
Justin Spahr-Summers 99bb3e042f Tighten up termination listening tests 2013-11-18 21:28:34 -08:00
Justin Spahr-Summers 5b930bcdb8 Don't log job removal errors in tests 2013-11-18 21:25:37 -08:00
Justin Spahr-Summers 7aace7204d Wait for a ShipIt state transition before sending signals 2013-11-18 21:21:44 -08:00
Justin Spahr-Summers c42950ccc4 Silence spurious "could not remove job" messages 2013-11-18 21:19:03 -08:00
Josh Abernathy efbb838e09 Merge pull request #82 from Squirrel/fix-codesigning
Unlock the keychain before building
2013-11-18 12:00:49 -08:00
Justin Spahr-Summers 249481c842 Unlock the keychain before building 2013-11-18 11:57:30 -08:00
Rob Rix 4062563d09 Merge pull request #81 from Squirrel/always-relaunch
Relaunch even if installation fails
2013-11-18 11:45:39 -08:00
Justin Spahr-Summers 55ca2cd87b ++RAC 2013-11-14 00:08:12 -08:00
Alan Rogers 6159fe07d5 Merge pull request #80 from Squirrel/should-relaunch-updating
Replace `shouldRelaunch` with a method to do it explicitly
2013-11-13 22:50:09 -08:00
Justin Spahr-Summers 2fed661399 indentation how does it work 2013-11-13 22:41:32 -08:00
Justin Spahr-Summers 1a9c430aca Merge pull request #79 from Squirrel/xctool
Use xctool for CI
2013-11-13 16:34:45 -08:00
Justin Spahr-Summers 2220774e01 Relaunch even if installation fails 2013-11-13 03:09:25 -08:00
Justin Spahr-Summers 77f5c72415 Failing test for relaunching after an update failure 2013-11-13 02:43:17 -08:00
Justin Spahr-Summers 3bd1e41270 Update README for RAC APIs 2013-11-13 02:00:35 -08:00
Justin Spahr-Summers 9b92542b9c Replay -relaunchToInstallUpdate 2013-11-13 01:57:51 -08:00
Justin Spahr-Summers 56b08482f4 Replace shouldRelaunch property with a method
This more closely aligns with the intention behind it anyways.
2013-11-13 01:38:39 -08:00
Justin Spahr-Summers c96467f35d Only build Squirrel scheme for CI 2013-11-12 23:12:50 -08:00
Justin Spahr-Summers 707678e926 Pipe TestApplication's output through NSLog too 2013-11-12 23:10:41 -08:00
Justin Spahr-Summers c50efc62a1 Capture output from tail and run it through NSLog
This avoids xctool dying on unexpected output.

See facebook/xctool#244.
2013-11-12 23:03:02 -08:00
Justin Spahr-Summers e4b7973685 Suppress deep-codesign output, fucks up xctool 2013-11-12 22:17:29 -08:00
Justin Spahr-Summers e4c1a33251 Stop logging killalls, fucks up xctool 2013-11-12 22:15:07 -08:00
Justin Spahr-Summers 7ba896fdde Revert "Disable deep codesigning"
This reverts commit 9fa097bec6.
2013-11-12 22:14:25 -08:00
Justin Spahr-Summers 9fa097bec6 Disable deep codesigning 2013-11-12 22:11:21 -08:00
Keith Duncan 9f8cbe17c3 Disable the deep codesign test
This is failing on 10.9 and is annoying
2013-11-12 22:08:53 -08:00
Justin Spahr-Summers 6f3e933d67 Update objc-build-scripts 2013-11-11 11:43:24 -08:00
Justin Spahr-Summers 516ab2af65 ++MTL and RAC to xctool branches 2013-11-11 11:43:15 -08:00
Josh Abernathy 4d385f4a33 Merge pull request #78 from Squirrel/fix-xcodebuild
Remove RunUnitTests script
2013-11-08 12:37:10 -08:00
Justin Spahr-Summers d5456f90f2 Remove RunUnitTests script
Not supported in Xcode 5's `xcodebuild`.
2013-11-08 12:35:07 -08:00
Justin Spahr-Summers 72d768d17d Merge pull request #74 from Squirrel/response-code-checking
Add response code checking to the check and download requests
2013-10-30 11:23:36 -07:00
Keith Duncan 91549567e1 Dot syntax++ 2013-10-30 11:45:58 +00:00
Keith Duncan 6e9efa0be3 :trollface: @jspahrsummers 2013-10-30 11:44:26 +00:00
Justin Spahr-Summers 8709338949 ++RAC to v2.1.6 2013-10-29 22:21:06 -07:00
Keith Duncan f9d6c00f60 Tweak comment 2013-10-29 11:27:39 +00:00
Keith Duncan e122927c5d Add test for non JSON data 2013-10-29 11:25:43 +00:00
Keith Duncan 43d524e186 Fix style 2013-10-29 11:15:49 +00:00
Keith Duncan a4dc3e4a47 Add test for non 2xx responses 2013-10-28 13:22:57 +00:00
Keith Duncan e5f838463b Make addCleanupBlock: public 2013-10-28 12:53:08 +00:00
Keith Duncan e24d538232 Add OHHTTPStubs 2013-10-28 12:53:01 +00:00
Keith Duncan 9170c4d106 Remove NSHTTPURLResponse cast, replace with id 2013-10-28 12:33:55 +00:00
Keith Duncan a93cf336b8 Use -reduceEach: returning signals then flatten
Replaces RACTupleUnpack
2013-10-28 12:33:23 +00:00
Keith Duncan 7bc6620b45 Split the updateURL class and content checking 2013-10-28 12:18:00 +00:00
Keith Duncan 4f0ba2c41a Add expects() for the written update
This was failing and expecting on the error would have given more details in the log
2013-10-25 14:54:06 +01:00
Keith Duncan cfaabf7cb0 Only check for nil hosts on non file:// scheme URLs
file scheme URLs have a nil host on 10.9
2013-10-25 14:53:58 +01:00
Keith Duncan d4cd9fa88c Add response code checking to the download request 2013-10-25 14:48:50 +01:00
Keith Duncan a7a3ddbf9c Add response code checking to the update request
Replace the -reduceEach: with a -flattenMap: which checks the response code, returns an empty signal if the response is No Content.
2013-10-25 14:44:16 +01:00
33 changed files with 1234 additions and 532 deletions
+4 -1
View File
@@ -3,4 +3,7 @@
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "External/Mantle"]
path = External/Mantle
url = https://github.com/github/Mantle.git
url = https://github.com/MantleFramework/Mantle.git
[submodule "External/OHHTTPStubs"]
path = External/OHHTTPStubs
url = https://github.com/github/OHHTTPStubs
Vendored Submodule
+1
Submodule External/OHHTTPStubs added at acaf8b8af7
+54 -22
View File
@@ -19,15 +19,40 @@ Support](#server-support) below.
Squirrel’s installer is also designed to be fault tolerant, and ensure that any
updates installed are valid.
![:shipit:](http://shipitsquirrel.github.io/images/ship%20it%20squirrel.png)
# Adopting Squirrel
1. Install xctool with `brew install xctool`
1. Add the Squirrel repository as a git submodule
1. Run `script/bootstrap` from within the submodule
1. Add a reference to Squirrel.xcodeproj to your project
1. Add Squirrel.framework as a target dependency
1. Link Squirrel.framework and add it to a Copy Files build phase which copies
it into your Frameworks directory
1. Ensure your Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`) includes the
Frameworks directory Squirrel.framework is copied into
1. Ensure your application includes the [dependencies](#dependencies). Squirrel
does not embed them itself.
# Dependencies
Squirrel depends on [ReactiveCocoa](http://github.com/ReactiveCocoa/ReactiveCocoa)
and [Mantle](https://github.com/MantleFramework/Mantle).
If your application is already using ReactiveCocoa, ensure it is using the same
version as Squirrel.
Otherwise, add a target dependency and Copy Files build phase entry for the
ReactiveCocoa.framework target included in Squirrel's repository, in
External/ReactiveCocoa.
Similarly, ensure your application includes Mantle, or copies in the Squirrel
version.
Finally, ensure your application's Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`)
includes the directory that Squirrel.framework, ReactiveCocoa.framework
and Mantle.framework are copied into.
# Configuration
Once Squirrel is added to your project, you need to configure and start it.
@@ -36,30 +61,38 @@ Once Squirrel is added to your project, you need to configure and start it.
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://mycompany.com/myapp/latest"]]];
[self.updater startAutomaticChecksWithInterval:/* 4 Hours */ 60 * 60 * 4];
[self.updater checkForUpdates];
// Check for updates every 4 hours.
[self.updater startAutomaticChecksWithInterval:60 * 60 * 4];
}
```
Squirrel will periodically request and automatically download any updates.
Squirrel will periodically request and automatically download any updates. When
your application terminates, any downloaded update will be automatically
installed.
Before your application terminates, it should tell Squirrel to install any
updates that it has downloaded:
## Update Notifications
To know when an update is ready to be installed, you can subscribe to the
`updates` signal on `SQRLUpdater`:
```objc
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
[self.updater installUpdateIfNeeded:^(BOOL success, NSError *error) {
if (success) {
NSLog(@"Successfully installed an update");
} else {
NSLog(@"Failed to update: %@", error);
}
[self.updater.updates subscribeNext:^(SQRLDownloadedUpdate *downloadedUpdate) {
NSLog(@"An update is ready to install: %@", downloadedUpdate);
}];
```
[sender replyToApplicationShouldTerminate:YES];
}];
If you've been notified of an available update, and don't want to wait for it to
be installed automatically, you can terminate the app to begin the installation
process immediately.
return NSTerminateLater;
}
If you want to install a downloaded update and automatically relaunch afterward,
`SQRLUpdater` can do that:
```objc
[[self.updater relaunchToInstallUpdate] subscribeError:^(NSError *error) {
NSLog(@"Error preparing update: %@", error);
}];
```
# Update JSON Format
@@ -100,7 +133,6 @@ will check for an update again at the interval you specify.
# User Interface
Squirrel does not provide an updates interface, if you want to display available
updates, subscribe to the `SQRLUpdaterUpdateAvailableNotification` notification.
![:shipit:](http://shipitsquirrel.github.io/images/ship%20it%20squirrel.png)
Squirrel does not provide any GUI components for presenting updates. If you want
to indicate updates to the user, make sure to [listen for downloaded
updates](#update-notifications).
+97 -23
View File
@@ -15,9 +15,12 @@
534FF37317D8E9370020A51A /* com.github.Squirrel.TestApplication.TestService.xpc in Copy XPCServices */ = {isa = PBXBuildFile; fileRef = 534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */; };
53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */; };
53710D8417D8F5CB00A992DE /* deep-codesign in Resources */ = {isa = PBXBuildFile; fileRef = 53710D7F17D8F5C300A992DE /* deep-codesign */; };
5374DCC6187AD0D8006B7056 /* SQRLAuthorization.h in Headers */ = {isa = PBXBuildFile; fileRef = 5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */; };
5374DCC7187AD0D8006B7056 /* SQRLAuthorization.m in Sources */ = {isa = PBXBuildFile; fileRef = 5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */; };
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */; };
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = 53AACF4917E9CA0500B41027 /* SQRLUpdate.h */; settings = {ATTRIBUTES = (Public, ); }; };
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */; };
53B70030181E9460000FFBD0 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */; };
D000218F17BACEFF0050109A /* SQRLZipArchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = D000218D17BACEFF0050109A /* SQRLZipArchiver.m */; };
D000219717BAD34D0050109A /* TestApplication.app.zip in Resources */ = {isa = PBXBuildFile; fileRef = D000219617BAD34D0050109A /* TestApplication.app.zip */; };
D000219917BAD35C0050109A /* SQRLZipArchiverSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = D000219817BAD35C0050109A /* SQRLZipArchiverSpec.m */; };
@@ -106,6 +109,34 @@
remoteGlobalIDString = 534FF35F17D8E90A0020A51A;
remoteInfo = TestService;
};
53B70020181E944F000FFBD0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 09793579161B6251006DB5D5;
remoteInfo = "OHHTTPStubs-iOS";
};
53B70022181E944F000FFBD0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 098368CE168FC7920082B1A4;
remoteInfo = UnitTests;
};
53B70024181E944F000FFBD0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = F6C37BA016F6C8680082B630;
remoteInfo = OHHTTPStubs;
};
53B7002E181E9456000FFBD0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = F6C37B9F16F6C8680082B630;
remoteInfo = OHHTTPStubs;
};
D014AC1617B9793A007D79D0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D0C22BCE179CC00E00158214 /* Project object */;
@@ -390,9 +421,12 @@
53710D8117D8F5C300A992DE /* README.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.md; sourceTree = "<group>"; };
53710D8217D8F5C300A992DE /* targets.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = targets.awk; sourceTree = "<group>"; };
53710D8317D8F5C300A992DE /* xcodebuild.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = xcodebuild.awk; sourceTree = "<group>"; };
5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLAuthorization.h; sourceTree = "<group>"; };
5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLAuthorization.m; sourceTree = "<group>"; };
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdateSpec.m; sourceTree = "<group>"; };
53AACF4917E9CA0500B41027 /* SQRLUpdate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLUpdate.h; sourceTree = "<group>"; };
53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdate.m; sourceTree = "<group>"; };
53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OHHTTPStubs.xcodeproj; path = External/ReactiveCocoa/external/specta/../../../OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj; sourceTree = "<group>"; };
D000218C17BACEFF0050109A /* SQRLZipArchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLZipArchiver.h; sourceTree = "<group>"; };
D000218D17BACEFF0050109A /* SQRLZipArchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLZipArchiver.m; sourceTree = "<group>"; };
D000219617BAD34D0050109A /* TestApplication.app.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = TestApplication.app.zip; sourceTree = "<group>"; };
@@ -540,6 +574,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
53B70030181E9460000FFBD0 /* OHHTTPStubs.framework in Frameworks */,
D0A56BCF1804B8FC00A84EDC /* Mantle.framework in Frameworks */,
D093682417FF732200BA0EAE /* ServiceManagement.framework in Frameworks */,
D0F3F06817E83B27003AFA6B /* ReactiveCocoa.framework in Frameworks */,
@@ -591,6 +626,25 @@
path = script;
sourceTree = "<group>";
};
5374DCD8187AD0DC006B7056 /* Authorization */ = {
isa = PBXGroup;
children = (
5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */,
5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */,
);
name = Authorization;
sourceTree = "<group>";
};
53B70015181E944F000FFBD0 /* Products */ = {
isa = PBXGroup;
children = (
53B70021181E944F000FFBD0 /* libOHHTTPStubs-iOS.a */,
53B70023181E944F000FFBD0 /* UnitTests.octest */,
53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */,
);
name = Products;
sourceTree = "<group>";
};
D00F5B8217E82CFB009A4818 /* Extensions */ = {
isa = PBXGroup;
children = (
@@ -705,9 +759,9 @@
isa = PBXGroup;
children = (
D0A56BA41804B1FD00A84EDC /* Mantle.framework */,
D0A56BA61804B1FD00A84EDC /* Mantle Mac Tests.octest */,
D0A56BA61804B1FD00A84EDC /* Mantle Tests.octest */,
D0A56BA81804B1FD00A84EDC /* libMantle-iOS.a */,
D0A56BAA1804B1FD00A84EDC /* Mantle iOS Tests.octest */,
D0A56BAA1804B1FD00A84EDC /* Mantle-iOS Tests.octest */,
D0A56BAC1804B1FD00A84EDC /* libMantle-Mac.a */,
);
name = Products;
@@ -766,6 +820,7 @@
children = (
D097B9BC17BB4777006C3FEB /* IOKit.framework */,
D014ABB917B97403007D79D0 /* ServiceManagement.framework */,
53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */,
D0F3F01417E83918003AFA6B /* Expecta.xcodeproj */,
D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */,
D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */,
@@ -926,6 +981,7 @@
D0A56BF61804BABB00A84EDC /* Code Signing */,
D0A56BE71804BA2400A84EDC /* State */,
D06B58BC18032BE800656D97 /* Transactions */,
5374DCD8187AD0DC006B7056 /* Authorization */,
);
name = Shared;
path = Squirrel;
@@ -974,6 +1030,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
5374DCC6187AD0D8006B7056 /* SQRLAuthorization.h in Headers */,
D0964B3C17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.h in Headers */,
D0C22C4C179CC15100158214 /* SQRLUpdater.h in Headers */,
D0C22C52179CC23900158214 /* Squirrel.h in Headers */,
@@ -1073,11 +1130,11 @@
D0C22BEA179CC00E00158214 /* Sources */,
D0C22BEB179CC00E00158214 /* Frameworks */,
D0C22BEC179CC00E00158214 /* Resources */,
D0C22BED179CC00E00158214 /* ShellScript */,
);
buildRules = (
);
dependencies = (
53B7002F181E9456000FFBD0 /* PBXTargetDependency */,
D0F3F03317E83924003AFA6B /* PBXTargetDependency */,
D0F3F03517E83924003AFA6B /* PBXTargetDependency */,
D08D4E4C17B4520C0012B22D /* PBXTargetDependency */,
@@ -1118,6 +1175,10 @@
ProductGroup = D0A56B971804B1FD00A84EDC /* Products */;
ProjectRef = D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */;
},
{
ProductGroup = 53B70015181E944F000FFBD0 /* Products */;
ProjectRef = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
},
{
ProductGroup = D0F3F04B17E83A1D003AFA6B /* Products */;
ProjectRef = D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */;
@@ -1139,6 +1200,27 @@
/* End PBXProject section */
/* Begin PBXReferenceProxy section */
53B70021181E944F000FFBD0 /* libOHHTTPStubs-iOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libOHHTTPStubs-iOS.a";
remoteRef = 53B70020181E944F000FFBD0 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
53B70023181E944F000FFBD0 /* UnitTests.octest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = UnitTests.octest;
remoteRef = 53B70022181E944F000FFBD0 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = OHHTTPStubs.framework;
remoteRef = 53B70024181E944F000FFBD0 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
D0964B1C17F2DD4500D88BF7 /* libReactiveCocoa-Mac.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1153,10 +1235,9 @@
remoteRef = D0A56BA31804B1FD00A84EDC /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
D0A56BA61804B1FD00A84EDC /* Mantle Mac Tests.octest */ = {
D0A56BA61804B1FD00A84EDC /* Mantle Tests.octest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
name = "Mantle Mac Tests.octest";
path = "Mantle Tests.octest";
remoteRef = D0A56BA51804B1FD00A84EDC /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
@@ -1168,10 +1249,9 @@
remoteRef = D0A56BA71804B1FD00A84EDC /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
D0A56BAA1804B1FD00A84EDC /* Mantle iOS Tests.octest */ = {
D0A56BAA1804B1FD00A84EDC /* Mantle-iOS Tests.octest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
name = "Mantle iOS Tests.octest";
path = "Mantle-iOS Tests.octest";
remoteRef = D0A56BA91804B1FD00A84EDC /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
@@ -1326,22 +1406,6 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
D0C22BED179CC00E00158214 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
534FF35C17D8E90A0020A51A /* Sources */ = {
isa = PBXSourcesBuildPhase;
@@ -1392,6 +1456,7 @@
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */,
D0A56BEB1804BA3C00A84EDC /* SQRLShipItState.m in Sources */,
D00F5B8C17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m in Sources */,
5374DCC7187AD0D8006B7056 /* SQRLAuthorization.m in Sources */,
D06B58B518032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1425,6 +1490,11 @@
target = 534FF35F17D8E90A0020A51A /* TestService */;
targetProxy = 534FF37017D8E9170020A51A /* PBXContainerItemProxy */;
};
53B7002F181E9456000FFBD0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = OHHTTPStubs;
targetProxy = 53B7002E181E9456000FFBD0 /* PBXContainerItemProxy */;
};
D014AC1717B9793A007D79D0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D014AC0017B97885007D79D0 /* ShipIt */;
@@ -1575,6 +1645,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Debug;
};
@@ -1584,6 +1655,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Release;
};
@@ -1593,6 +1665,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Test;
};
@@ -1602,6 +1675,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Profile;
};
@@ -20,6 +20,20 @@
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.octest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
+13 -16
View File
@@ -97,15 +97,12 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
va_end(args);
}
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
RACDisposable *subscriptionDisposable = [self subscribe:subscriber];
return [RACDisposable disposableWithBlock:^{
[subscriptionDisposable dispose];
[transactionDisposable dispose];
}];
}] setNameWithFormat:@"[%@] -sqrl_addTransactionWithName: %@ description: %@", self.name, name, description];
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
[subscriber.disposable addDisposable:SQRLCreateTransaction(name, description)];
[self subscribe:subscriber];
}]
setNameWithFormat:@"[%@] -sqrl_addTransactionWithName: %@ description: %@", self.name, name, description];
}
- (RACSignal *)sqrl_addSubscriptionTransactionWithName:(NSString *)name description:(NSString *)descriptionFormat, ... {
@@ -117,13 +114,13 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
va_end(args);
}
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
RACDisposable *subscriptionDisposable = [self subscribe:subscriber];
[transactionDisposable dispose];
return subscriptionDisposable;
}] setNameWithFormat:@"[%@] -sqrl_addSubscriptionTransactionWithName: %@ description: %@", self.name, name, description];
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
[self subscribe:subscriber];
[transactionDisposable dispose];
}]
setNameWithFormat:@"[%@] -sqrl_addSubscriptionTransactionWithName: %@ description: %@", self.name, name, description];
}
@end
+27
View File
@@ -0,0 +1,27 @@
//
// SQRLAuthorization.h
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
// Wraps an AuthorizationRef.
@interface SQRLAuthorization : NSObject
// Designated initializer.
//
// authorization - Must not be NULL, the returned object assumes ownership of
// the passed authorization.
//
// Returns an object which ties the `authorization` argument to the returned
// object's lifetime.
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization;
@property (readonly, nonatomic, assign) AuthorizationRef authorization;
@end
+28
View File
@@ -0,0 +1,28 @@
//
// SQRLAuthorization.m
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLAuthorization.h"
@implementation SQRLAuthorization
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization {
NSParameterAssert(authorization != NULL);
self = [self init];
if (self == nil) return nil;
_authorization = authorization;
return self;
}
- (void)dealloc {
if (_authorization != NULL) AuthorizationFree(_authorization, kAuthorizationFlagDestroyRights);
}
@end
+5 -4
View File
@@ -83,7 +83,7 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL {
NSParameterAssert(bundleURL != nil);
return [[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
return [[RACSignal create:^(id<RACSubscriber> subscriber) {
SecStaticCodeRef staticCode = NULL;
OSStatus result = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
@@ -100,8 +100,10 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
return nil;
return;
}
if (subscriber.disposable.disposed) return;
CFErrorRef validityError = NULL;
result = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSCheckAllArchitectures, (__bridge SecRequirementRef)self.requirement, &validityError);
@@ -119,11 +121,10 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
if (validityError != NULL) userInfo[NSUnderlyingErrorKey] = (__bridge NSError *)validityError;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
return nil;
return;
}
[subscriber sendCompleted];
return nil;
}] setNameWithFormat:@"-verifyCodeSignatureOfBundle: %@", bundleURL];
}
+11 -10
View File
@@ -34,7 +34,9 @@ extern const NSInteger SQRLInstallerErrorInvalidState;
// There was an error moving a bundle across volumes.
extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
@class RACCommand;
@class RACSignal;
@class RACAction;
@class SQRLShipItState;
@class SQRLDirectoryManager;
// Performs the installation of an update, using the `SQRLShipItState` on disk,
@@ -44,21 +46,20 @@ extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
// terminated.
@interface SQRLInstaller : NSObject
// When executed with a `SQRLShipItState`, attempts to install the update or
// resume an in-progress installation.
// Attempts to install the update or resume an in-progress installation.
//
// Each execution will complete or error on an unspecified scheduler when
// Returns a signal which completes or errors on an unspecified scheduler when
// installation has completed or failed.
@property (nonatomic, strong, readonly) RACCommand *installUpdateCommand;
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state;
// When executed with a `SQRLShipItState`, aborts an installation, and attempts
// to restore the old version of the application if necessary.
// Aborts an installation, and attempts to restore the old version of the
// application if necessary.
//
// This must not be executed while `installUpdateCommand` is executing.
// This must not be executed while `installUpdateWithState:` is executing.
//
// Each execution will complete or error on an unspecified scheduler once
// Returns a signal while completes or errors on an unspecified scheduler once
// aborting/recovery has finished.
@property (nonatomic, strong, readonly) RACCommand *abortInstallationCommand;
- (RACSignal *)abortInstallationWithState:(SQRLShipItState *)state;
// Initializes an installer using the given directory manager to read and write the
// state of the installation.
+45 -86
View File
@@ -145,44 +145,34 @@ typedef struct {
if (self == nil) return nil;
_directoryManager = directoryManager;
@weakify(self);
RACSignal *aborting = [[[[RACObserve(self, abortInstallationCommand)
ignore:nil]
map:^(RACCommand *command) {
return command.executing;
}]
switchToLatest]
setNameWithFormat:@"aborting"];
_installUpdateCommand = [[RACCommand alloc] initWithEnabled:[aborting not] signalBlock:^(SQRLShipItState *state) {
@strongify(self);
NSParameterAssert(state != nil);
return [[self
resumeInstallationFromState:state]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}];
_abortInstallationCommand = [[RACCommand alloc] initWithEnabled:[self.installUpdateCommand.executing not] signalBlock:^(SQRLShipItState *state) {
@strongify(self);
NSParameterAssert(state != nil);
return [[[RACSignal
zip:@[
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
] reduce:^(NSURL *targetBundleURL, SQRLCodeSignature *codeSignature) {
return [self verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:state.backupBundleURL];
}]
flatten]
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}];
return self;
}
#pragma mark Actions
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[self
resumeInstallationFromState:state]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}
- (RACSignal *)abortInstallationWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[[RACSignal
zip:@[
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
] reduce:^(NSURL *targetBundleURL, SQRLCodeSignature *codeSignature) {
return [self verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:state.backupBundleURL];
}]
flatten]
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}
#pragma mark Installer State
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state {
@@ -211,7 +201,6 @@ typedef struct {
{ .installerState = SQRLInstallerStateBackingUp, .selector = @selector(backUpWithState:) },
{ .installerState = SQRLInstallerStateInstalling, .selector = @selector(installWithState:) },
{ .installerState = SQRLInstallerStateVerifyingInPlace, .selector = @selector(verifyInPlaceWithState:) },
{ .installerState = SQRLInstallerStateRelaunching, .selector = @selector(relaunchWithState:) },
};
const size_t tableCount = sizeof(dispatchTablePrototype) / sizeof(*dispatchTablePrototype);
@@ -263,13 +252,12 @@ typedef struct {
nextState = dispatchTable[tableIndex + 1].installerState;
}
return [[step
return [[[step
doCompleted:^{
NSLog(@"Completed state %i", (int)installerState);
}]
then:^{
return [RACSignal return:@(nextState)];
}];
ignoreValues]
concat:[RACSignal return:@(nextState)]];
}];
return [[self
@@ -366,11 +354,9 @@ typedef struct {
// failure. Try recovering it if it did.
return [[self
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
then:^{
// Recovery succeeded, but we still want to pass
// through the original error.
return [RACSignal error:error];
}];
// Recovery succeeded, but we still want to pass
// through the original error.
concat:[RACSignal error:error]];
}];
}]
flatten]
@@ -388,37 +374,14 @@ typedef struct {
] reduce:^(NSURL *targetBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
return [[self
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
then:^{
return [[self
deleteBackupAtURL:backupBundleURL]
catchTo:[RACSignal empty]];
}];
concat:[[self
deleteBackupAtURL:backupBundleURL]
catchTo:[RACSignal empty]]];
}]
flatten]
setNameWithFormat:@"%@ -verifyInPlaceWithState: %@", self, state];
}
- (RACSignal *)relaunchWithState:(SQRLShipItState *)state {
return [[[[RACSignal
defer:^{
if (state.relaunchAfterInstallation) {
return [self getRequiredKey:@keypath(state.targetBundleURL) fromState:state];
} else {
return [RACSignal empty];
}
}]
deliverOn:RACScheduler.mainThreadScheduler]
flattenMap:^(NSURL *bundleURL) {
NSError *error = nil;
if ([NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
return [RACSignal empty];
} else {
return [RACSignal error:error];
}
}]
setNameWithFormat:@"%@ -relaunch", self];
}
#pragma mark Backing Up
- (RACSignal *)backUpBundleAtURL:(NSURL *)targetBundleURL {
@@ -460,26 +423,23 @@ typedef struct {
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL {
NSParameterAssert(backupURL != nil);
return [[[RACSignal
return [[RACSignal
defer:^{
NSError *error = nil;
if ([NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
return [RACSignal empty];
} else {
if (![NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
return [RACSignal error:error];
}
}]
then:^{
// Also remove the temporary directory that the backup lived in.
NSURL *temporaryDirectoryURL = backupURL.URLByDeletingLastPathComponent;
// However, use rmdir() to skip it in case there are other files
// contained within (for whatever reason).
if (rmdir(temporaryDirectoryURL.path.fileSystemRepresentation) == 0) {
return [RACSignal empty];
} else {
if (rmdir(temporaryDirectoryURL.path.fileSystemRepresentation) != 0) {
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
}
return [RACSignal empty];
}]
setNameWithFormat:@"%@ -deleteBackupAtURL: %@", self, backupURL];
}
@@ -493,10 +453,11 @@ typedef struct {
catch:^(NSError *error) {
if (backupBundleURL == nil) return [RACSignal error:error];
return [[[[self
installItemAtURL:bundleURL fromURL:backupBundleURL]
initially:^{
return [[[RACSignal
defer:^{
[NSFileManager.defaultManager removeItemAtURL:bundleURL error:NULL];
return [self installItemAtURL:bundleURL fromURL:backupBundleURL];
}]
doCompleted:^{
NSLog(@"Restored backup bundle to %@", bundleURL);
@@ -517,9 +478,7 @@ typedef struct {
return [[[[self
verifyBundleAtURL:targetURL usingSignature:signature recoveringUsingBackupAtURL:nil]
then:^{
return [RACSignal return:@YES];
}]
concat:[RACSignal return:@YES]]
catch:^(NSError *error) {
BOOL directory;
if ([NSFileManager.defaultManager fileExistsAtPath:sourceURL.path isDirectory:&directory]) {
@@ -585,7 +544,7 @@ typedef struct {
return YES;
}];
return enumerator.rac_sequence.signal;
return enumerator.allObjects.rac_signal;
}]
flattenMap:^(NSURL *URL) {
const char *path = URL.path.fileSystemRepresentation;
+14 -16
View File
@@ -13,6 +13,7 @@
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "SQRLAuthorization.h"
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
@@ -71,7 +72,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
+ (RACSignal *)shipItAuthorization {
return [[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
create:^(id<RACSubscriber> subscriber) {
AuthorizationItem rightItems[] = {
{
.name = kSMRightModifySystemDaemons,
@@ -108,16 +109,13 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:(__bridge id)authorization];
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
return [RACDisposable disposableWithBlock:^{
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
}];
}]
setNameWithFormat:@"+shipItAuthorization"];
}
@@ -127,22 +125,22 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
zip:@[
self.shipItJobDictionary,
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
] reduce:^(NSDictionary *jobDictionary, id authorization) {
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
#if DEBUG
NSLog(@"Could not remove previous ShipIt job: %@", cfError);
#endif
AuthorizationRef authorization = authorizationValue.authorization;
if (cfError != NULL) {
CFRelease(cfError);
cfError = NULL;
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
NSLog(@"Could not remove previous ShipIt job: %@", error);
}
}
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, (__bridge AuthorizationRef)authorization, &cfError)) {
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
-4
View File
@@ -41,9 +41,6 @@ extern NSString * const SQRLShipItStatePropertyErrorKey;
// update bundle.
// SQRLInstallerStateVerifyingInPlace - Verifying that the target bundle is
// still valid after updating.
// SQRLInstallerStateRelaunching - Relaunching the updated application.
// This state will be entered even if
// there's no relaunching to do.
//
// Note that these values must remain backwards compatible, so ShipIt doesn't
// start up in a weird mode on a newer version.
@@ -53,7 +50,6 @@ typedef enum : NSInteger {
SQRLInstallerStateBackingUp,
SQRLInstallerStateInstalling,
SQRLInstallerStateVerifyingInPlace,
SQRLInstallerStateRelaunching
} SQRLInstallerState;
@class RACSignal;
+4 -4
View File
@@ -45,7 +45,7 @@
return [[[[RACSignal
defer:^{
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:self.bundleIdentifier];
return [apps.rac_sequence signalWithScheduler:RACScheduler.immediateScheduler];
return apps.rac_signal;
}]
filter:^(NSRunningApplication *application) {
return [application.bundleURL.URLByStandardizingPath isEqual:self.bundleURL];
@@ -60,7 +60,7 @@
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier {
return [[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
create:^(id<RACSubscriber> subscriber) {
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, processIdentifier, DISPATCH_PROC_EXIT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
dispatch_source_set_registration_handler(source, ^{
@@ -72,10 +72,10 @@
});
dispatch_resume(source);
return [RACDisposable disposableWithBlock:^{
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
dispatch_source_cancel(source);
dispatch_release(source);
}];
}]];
}]
setNameWithFormat:@"%@ -waitForTerminationOfProcessIdentifier: %i", self, (int)processIdentifier];
}
+16 -2
View File
@@ -134,19 +134,33 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
- (BOOL)validateUpdateURL:(NSURL **)updateURLPtr error:(NSError **)error {
NSURL *updateURL = *updateURLPtr;
if (![updateURL isKindOfClass:NSURL.class] || updateURL.scheme == nil || updateURL.host == nil || updateURL.path == nil) {
if (![updateURL isKindOfClass:NSURL.class]) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Validation failed", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An invalid updateURL was given to SQRLUpdate: %@", nil), updateURL]
};
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
BOOL valid = (updateURL.scheme != nil);
valid &= ([updateURL.scheme isEqualToString:@"file"] || updateURL.host != nil);
valid &= (updateURL.path != nil);
if (!valid) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Validation failed", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Update URLs must have a scheme, a host and a path: %@", nil), updateURL]
};
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
return YES;
}
+25 -12
View File
@@ -25,6 +25,11 @@ extern const NSInteger SQRLUpdaterErrorRetrievingCodeSigningRequirement;
// Includes `SQRLUpdaterServerDataErrorKey` in the error's `userInfo`.
extern const NSInteger SQRLUpdaterErrorInvalidServerResponse;
// The server sent a response body that we didn't understand.
//
// Includes `SQRLUpdaterServerDataErrorKey` in the error's `userInfo`.
extern const NSInteger SQRLUpdaterErrorInvalidServerBody;
// The server sent update JSON that we didn't understand.
//
// Includes `SQRLUpdaterJSONObjectErrorKey` in the error's `userInfo`.
@@ -38,7 +43,7 @@ extern NSString * const SQRLUpdaterServerDataErrorKey;
// error with code `SQRLUpdaterErrorInvalidJSON` is generated.
extern NSString * const SQRLUpdaterJSONObjectErrorKey;
@class RACCommand;
@class RACAction;
@class RACDisposable;
@class RACSignal;
@@ -47,21 +52,14 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// Kicks off a check for updates.
//
// If an update is available, it will be sent on `updates` once downloaded.
@property (nonatomic, strong, readonly) RACCommand *checkForUpdatesCommand;
// If an update is available, it will be sent on
// `[checkForUpdatesAction deferred]` and `updates` once downloaded.
@property (nonatomic, strong, readonly) RACAction *checkForUpdatesAction;
// Sends an `SQRLDownloadedUpdate` object on the main thread whenever a new
// update is available.
//
// This signal is actually just `checkForUpdatesCommand.executionSignals`,
// flattened for convenience.
// update is available. Completes when the receiver deallocates.
@property (nonatomic, strong, readonly) RACSignal *updates;
// Whether or not to relaunch after installing an update.
//
// This will be reset to NO whenever update installation fails.
@property (atomic) BOOL shouldRelaunch;
// The request that will be sent to check for updates.
//
// The default value is the argument that was originally passed to
@@ -99,6 +97,21 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// checking.
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval;
// Terminates the running application to install any available update, then
// automatically relaunches the app after updating.
//
// This method is only useful if you want the application to automatically
// relaunch. Otherwise, you can simply use `-[NSApplication terminate:]` or any
// other exit mechanism.
//
// After subscribing to this signal, the receiver is responsible for terminating
// the application upon success. The app must not be terminated in any other way
// unless an error occurs.
//
// Returns a signal that will error on the main scheduler if anything goes
// wrong before termination. The signal will never complete.
- (RACSignal *)relaunchToInstallUpdate;
@end
@interface SQRLUpdater (Unavailable)
+263 -97
View File
@@ -30,6 +30,11 @@ const NSInteger SQRLUpdaterErrorPreparingUpdateJob = 3;
const NSInteger SQRLUpdaterErrorRetrievingCodeSigningRequirement = 4;
const NSInteger SQRLUpdaterErrorInvalidServerResponse = 5;
const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// The prefix used when creating temporary directories for updates. This will be
// followed by a random string of characters.
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
@interface SQRLUpdater ()
@@ -42,6 +47,16 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
// Sends completed or error.
@property (nonatomic, strong, readonly) RACSignal *shipItLauncher;
// Lazily removes outdated temporary directories (used for previous updates)
// upon first subscription.
//
// Pruning directories while an update is pending or in progress will result in
// undefined behavior.
//
// Sends each removed directory then completes, or errors, on an unspecified
// thread.
@property (nonatomic, strong, readonly) RACSignal *prunedUpdateDirectories;
// Parses an update model from downloaded data.
//
// data - JSON data representing an update manifest. This must not be nil.
@@ -109,16 +124,27 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
// Returns a signal which completes or errors on a background thread.
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update;
// Verifies that an existing state is innocuous, and therefore safe to
// overwrite.
//
// This won't be the case if, for example, an update is currently being
// installed.
//
// Returns a signal which sends `existingState` then completes upon successful
// validation, or errors otherwise.
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState;
@end
@implementation SQRLUpdater
@implementation SQRLUpdater {
RACSubject *_updates;
}
#pragma mark Properties
- (RACSignal *)updates {
return [[self.checkForUpdatesCommand.executionSignals
concat]
setNameWithFormat:@"%@ -updates", self];
return [_updates
deliverOn:RACScheduler.mainThreadScheduler];
}
#pragma mark Lifecycle
@@ -139,37 +165,68 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
NSError *error = nil;
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", error);
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
if (_signature == nil) {
#if DEBUG
NSLog(@"Could not get code signature for running application, application updates are disabled: %@", error);
return nil;
#else
NSDictionary *exceptionInfo = @{ NSUnderlyingErrorKey: error };
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Could not get code signature for running application" userInfo:exceptionInfo];
#endif
}
@weakify(self);
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
@strongify(self);
NSParameterAssert(self.updateRequest != nil);
// TODO: Maybe allow this to be an argument to the command?
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
_prunedUpdateDirectories = [[[[[[[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSFileManager *manager = [[NSFileManager alloc] init];
NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:appSupportURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, appSupportURL, error);
return YES;
}];
return [[[[[[NSURLConnection
rac_sendAsynchronousRequest:request]
reduceEach:^(id _, NSData *data) {
return data;
}]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
}]
doError:^(id _) {
self.shouldRelaunch = NO;
}]
deliverOn:RACScheduler.mainThreadScheduler];
}];
return [[enumerator.allObjects.rac_signal
filter:^(NSURL *enumeratedURL) {
NSString *name = enumeratedURL.lastPathComponent;
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
}]
doNext:^(NSURL *directoryURL) {
NSError *error = nil;
if (![manager removeItemAtURL:directoryURL error:&error]) {
NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
}
}];
}]
collect]
// These operators ensure that the actual work only executes once,
// without starting it immediately.
concat:[RACSignal never]]
shareWhileActive]
take:1]
flattenMap:^(NSArray *URLs) {
return URLs.rac_signal;
}]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
_shipItLauncher = [[[RACSignal
_checkForUpdatesAction = [[[RACSignal
defer:^{
@strongify(self);
return [RACSignal return:self.updateRequest];
}]
flattenMap:^(NSURLRequest *request) {
return [self checkForUpdates:request];
}]
action];
_updates = [[RACSubject
subject]
setNameWithFormat:@"%@ updates", self];
_shipItLauncher = [[[[[[[RACSignal
defer:^{
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
@@ -181,31 +238,90 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
// wait for another, more canonical error.
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
}]
replayLazily]
// These operators ensure that the actual work only executes once,
// without starting it immediately. Super hacky.
concat:[RACSignal return:RACUnit.defaultUnit]]
concat:[RACSignal never]]
shareWhileActive]
take:1]
ignoreValues]
setNameWithFormat:@"shipItLauncher"];
return self;
}
- (void)dealloc {
[_updates sendCompleted];
}
#pragma mark Checking for Updates
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval {
@weakify(self);
return [[[[[RACSignal
return [[[[RACSignal
interval:interval onScheduler:[RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]]
flattenMap:^(id _) {
@strongify(self);
return [[self.checkForUpdatesCommand
execute:RACUnit.defaultUnit]
return [[[self.checkForUpdatesAction
signalWithValue:nil]
ignoreValues]
catch:^(NSError *error) {
NSLog(@"Error checking for updates: %@", error);
return [RACSignal empty];
}];
}]
takeUntil:self.rac_willDeallocSignal]
publish]
connect];
subscribeCompleted:^{}];
}
- (RACSignal *)checkForUpdates:(NSURLRequest *)request {
NSParameterAssert(request != nil);
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
if (updatesDisabled) return [RACSignal empty];
NSMutableURLRequest *newRequest = [request mutableCopy];
[newRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
return [[[[[[[[self.prunedUpdateDirectories
catch:^(NSError *error) {
NSLog(@"Error pruning old updates: %@", error);
return [RACSignal empty];
}]
ignoreValues]
concat:[NSURLConnection rac_sendAsynchronousRequest:newRequest]]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
}
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
}]
doNext:^(SQRLDownloadedUpdate *update) {
[self->_updates sendNext:update];
}];
}
- (RACSignal *)updateFromJSONData:(NSData *)data {
@@ -222,7 +338,7 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
userInfo[SQRLUpdaterServerDataErrorKey] = data;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerBody userInfo:userInfo]];
}
Class updateClass = self.updateClass;
@@ -278,11 +394,25 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
return [[[NSURLConnection
return [[[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(id _, NSData *data) {
return data;
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
}
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:zipDownloadURL.lastPathComponent];
@@ -298,11 +428,16 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
NSLog(@"Download completed to: %@", zipOutputURL);
}]
flattenMap:^(NSURL *zipOutputURL) {
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory];
}]
then:^{
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
return [[SQRLZipArchiver
unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory]
doCompleted:^{
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:zipOutputURL error:&error]) {
NSLog(@"Error removing downloaded archive at %@: %@", zipOutputURL, error.sqrl_verboseDescription);
}
}];
}]
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
}
@@ -315,7 +450,7 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:@"update.XXXXXXX"];
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
@@ -348,25 +483,27 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, directory, error);
return YES;
}];
NSURL *updateBundleURL = [enumerator.rac_sequence objectPassingTest:^(NSURL *URL) {
NSString *type = nil;
NSError *error = nil;
if (![URL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&error]) {
NSLog(@"Error retrieving UTI for item at %@: %@", URL, error);
return NO;
}
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
NSURL *updateBundleURL = [[enumerator.allObjects.rac_signal
filter:^(NSURL *URL) {
NSString *type = nil;
NSError *error = nil;
if (![URL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&error]) {
NSLog(@"Error retrieving UTI for item at %@: %@", URL, error);
return NO;
}
NSBundle *bundle = [NSBundle bundleWithURL:URL];
if (bundle == nil) {
NSLog(@"Could not open application bundle at %@", URL);
return NO;
}
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
return [bundle.bundleIdentifier isEqual:NSRunningApplication.currentApplication.bundleIdentifier];
}];
NSBundle *bundle = [NSBundle bundleWithURL:URL];
if (bundle == nil) {
NSLog(@"Could not open application bundle at %@", URL);
return NO;
}
return [bundle.bundleIdentifier isEqual:NSRunningApplication.currentApplication.bundleIdentifier];
}]
first];
if (updateBundleURL != nil) {
return [RACSignal return:updateBundleURL];
@@ -384,6 +521,15 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
setNameWithFormat:@"%@ -applicationBundleMatchingCurrentApplicationInDirectory: %@", self, directory];
}
- (RACSignal *)shipItStateURL {
return [[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return directoryManager.shipItStateURL;
}]
setNameWithFormat:@"%@ -shipItStateURL", self];
}
#pragma mark Installing Updates
- (RACSignal *)verifyAndPrepareUpdate:(SQRLUpdate *)update fromBundle:(NSBundle *)updateBundle {
@@ -392,53 +538,73 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
return [[[[self.signature
verifyBundleAtURL:updateBundle.bundleURL]
then:^{
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle];
return [RACSignal return:downloadedUpdate];
}]
concat:[RACSignal return:[[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle]]]
flattenMap:^(SQRLDownloadedUpdate *downloadedUpdate) {
return [[self prepareUpdateForInstallation:downloadedUpdate] then:^{
return [RACSignal return:downloadedUpdate];
}];
return [[self
prepareUpdateForInstallation:downloadedUpdate]
concat:[RACSignal return:downloadedUpdate]];
}]
setNameWithFormat:@"%@ -verifyAndPrepareUpdate: %@ fromBundle: %@", self, update, updateBundle];
}
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState {
return [[RACSignal
defer:^{
if (existingState.installerState != SQRLInstallerStateNothingToDo) {
// If this happens, shit is crazy, because it implies that an
// update is being installed over us right now.
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Installation in progress", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An update for %@ is already in progress.", nil), NSRunningApplication.currentApplication.bundleIdentifier],
};
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
}
return [RACSignal return:existingState];
}]
setNameWithFormat:@"%@ -validateExistingState: %@", self, existingState];
}
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update {
NSParameterAssert(update != nil);
return [[[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
RACSignal *stateLocation = directoryManager.shipItStateURL;
return [[[[SQRLShipItState
readUsingURL:stateLocation]
catchTo:[RACSignal empty]]
flattenMap:^(SQRLShipItState *existingState) {
if (existingState.installerState != SQRLInstallerStateNothingToDo) {
// If this happens, shit is crazy, because it implies that an
// update is being installed over us right now.
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Installation in progress", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An update for %@ is already in progress.", nil), NSRunningApplication.currentApplication.bundleIdentifier],
};
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
}
return [RACSignal empty];
}]
then:^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
state.relaunchAfterInstallation = self.shouldRelaunch;
return [state writeUsingURL:stateLocation];
}];
}]
then:^{
return self.shipItLauncher;
return [[[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
catchTo:[RACSignal empty]]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
}]
ignoreValues]
concat:[RACSignal defer:^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
return [state writeUsingURL:self.shipItStateURL];
}]]
concat:self.shipItLauncher]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing update", nil) description:NSLocalizedString(@"An update for %@ is being prepared. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier]
setNameWithFormat:@"%@ -prepareUpdateForInstallation: %@", self, update];
}
- (RACSignal *)relaunchToInstallUpdate {
return [[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
}]
flattenMap:^(SQRLShipItState *state) {
state.relaunchAfterInstallation = YES;
return [[state
writeUsingURL:self.shipItStateURL]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing to relaunch", nil) description:NSLocalizedString(@"%@ is preparing to relaunch to install an update. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier];
}]
deliverOn:RACScheduler.mainThreadScheduler]
doCompleted:^{
[NSApp terminate:self];
}]
// Never allow `completed` to escape this signal chain (in case
// -terminate: is asynchronous or something crazy).
concat:[RACSignal never]]
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
}
@end
+12 -14
View File
@@ -130,25 +130,23 @@ const NSInteger SQRLZipArchiverShellTaskFailed = 1;
// This is important because the signals on `self` complete upon
// dealloc.
return:self]
then:^{
return [RACSignal
zip:@[ self.taskTerminated, self.standardErrorData ]
reduce:^(NSNumber *exitStatus, NSData *errorData) {
if (exitStatus.intValue == 0) return [RACSignal empty];
ignoreValues]
concat:[RACSignal
zip:@[ self.taskTerminated, self.standardErrorData ]
reduce:^(NSNumber *exitStatus, NSData *errorData) {
if (exitStatus.intValue == 0) return [RACSignal empty];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}];
}]
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}]]
take:1]
flatten]
replay]
setNameWithFormat:@"-launchWithArguments: %@", arguments];
self.dittoTask.arguments = arguments;
+40 -15
View File
@@ -55,16 +55,15 @@ int main(int argc, const char * argv[]) {
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@(jobLabel)];
RACSignal *stateLocation = directoryManager.shipItStateURL;
[[[[[[SQRLShipItState
[[[[[[[SQRLShipItState
readUsingURL:stateLocation]
flattenMap:^(SQRLShipItState *state) {
return waitForTerminationIfNecessary(state);
}]
then:^{
// Read the latest state, in case it was modified by the
// controlling application in the meantime.
return [SQRLShipItState readUsingURL:stateLocation];
}]
ignoreValues]
// Read the latest state, in case it was modified by the
// controlling application in the meantime.
concat:[SQRLShipItState readUsingURL:stateLocation]]
catch:^(NSError *error) {
NSLog(@"Error reading saved installer state: %@", error);
@@ -76,11 +75,14 @@ int main(int argc, const char * argv[]) {
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:directoryManager];
NSUInteger attempt = (freshInstall ? 1 : state.installationStateAttempt + 1);
RACSignal *action;
if (attempt > SQRLShipItMaximumInstallationAttempts) {
return [[[installer.abortInstallationCommand
execute:state]
initially:^{
action = [[RACSignal
defer:^{
NSLog(@"Too many attempts to install from state %i, aborting update", (int)state.installerState);
return [installer abortInstallationWithState:state];
}]
catch:^(NSError *error) {
NSLog(@"Error aborting installation: %@", error);
@@ -89,9 +91,8 @@ int main(int argc, const char * argv[]) {
return [RACSignal empty];
}];
} else {
return [[[[[state
writeUsingURL:stateLocation]
initially:^{
action = [[[[RACSignal
defer:^{
if (freshInstall) {
NSLog(@"Beginning installation");
state.installerState = SQRLInstallerStateClearingQuarantine;
@@ -100,15 +101,39 @@ int main(int argc, const char * argv[]) {
}
state.installationStateAttempt = attempt;
return [state writeUsingURL:stateLocation];
}]
then:^{
return [installer.installUpdateCommand execute:state];
}]
concat:[installer installUpdateWithState:state]]
doCompleted:^{
NSLog(@"Installation completed successfully");
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}
if (state.relaunchAfterInstallation) {
// Relaunch regardless of whether installation succeeds or
// fails.
action = [[action
deliverOn:RACScheduler.mainThreadScheduler]
doFinished:^{
NSURL *bundleURL = state.targetBundleURL;
if (bundleURL == nil) {
NSLog(@"Missing target bundle URL, cannot relaunch application");
return;
}
NSError *error = nil;
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
NSLog(@"Could not relaunch application at %@: %@", bundleURL, error);
return;
}
NSLog(@"Application relaunched at %@", bundleURL);
}];
}
return action;
}]
subscribeError:^(NSError *error) {
NSLog(@"Installation error: %@", error);
+5 -1
View File
@@ -60,12 +60,16 @@ void (^deepCodesignTestApplication)(void) = ^{
NSTask *deepCodesignTask = [[NSTask alloc] init];
deepCodesignTask.launchPath = deepCodesignLocation.path;
deepCodesignTask.standardError = [NSPipe pipe];
deepCodesignTask.standardOutput = [NSPipe pipe];
NSMutableDictionary *environment = environmentSuitableForChildProcess();
[environment addEntriesFromDictionary:@{
@"CODE_SIGN_IDENTITY": @"-",
@"CONFIGURATION_BUILD_DIR": testApplicationLocation.URLByDeletingLastPathComponent.path,
@"FULL_PRODUCT_NAME": testApplicationLocation.lastPathComponent,
}];
deepCodesignTask.environment = environment;
[deepCodesignTask launch];
@@ -106,7 +110,7 @@ it(@"should deep sign the test application", ^{
deepCodesignTestApplication();
});
it(@"should deep verify after signing", ^{
xit(@"should deep verify after signing", ^{
expect(deepVerify()).to.beFalsy();
deepCodesignTestApplication();
expect(deepVerify()).to.beTruthy();
+31 -6
View File
@@ -77,7 +77,7 @@ it(@"should install an update in process", ^{
expect(installer).notTo.beNil();
NSError *installError = nil;
BOOL install = [[installer.installUpdateCommand execute:state] asynchronouslyWaitUntilCompleted:&installError];
BOOL install = [[installer installUpdateWithState:state] asynchronouslyWaitUntilCompleted:&installError];
expect(install).to.beTruthy();
expect(installError).to.beNil();
});
@@ -104,6 +104,28 @@ it(@"should not install an update after too many attempts", ^{
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
it(@"should relaunch even after failing to install an update", ^{
NSURL *targetURL = self.testApplicationURL;
NSURL *backupURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app.bak"];
expect([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:backupURL error:NULL]).to.beTruthy();
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
state.backupBundleURL = backupURL;
state.installerState = SQRLInstallerStateInstalling;
state.installationStateAttempt = 4;
state.relaunchAfterInstallation = YES;
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
[self launchShipIt];
__block NSError *error = nil;
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
expect(error).to.beNil();
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count).will.equal(1);
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
describe(@"signal handling", ^{
__block NSURL *targetURL;
@@ -117,6 +139,9 @@ describe(@"signal handling", ^{
[self launchShipIt];
// Wait until ShipIt has transitioned by at least one state.
expect([[[SQRLShipItState readUsingURL:self.shipItDirectoryManager.shipItStateURL] asynchronousFirstOrDefault:nil success:NULL error:NULL] installerState]).willNot.equal(SQRLInstallerStateNothingToDo);
// Apply a random delay before sending the termination signal, to
// fuzz out race conditions.
NSTimeInterval delay = arc4random_uniform(50) / 1000.0;
@@ -136,26 +161,26 @@ describe(@"signal handling", ^{
});
it(@"should handle SIGHUP", ^{
system("killall -v -HUP ShipIt");
system("killall -HUP ShipIt");
});
it(@"should handle SIGTERM", ^{
system("killall -v -TERM ShipIt");
system("killall -TERM ShipIt");
});
it(@"should handle SIGINT", ^{
system("killall -v -INT ShipIt");
system("killall -INT ShipIt");
});
it(@"should handle SIGQUIT", ^{
system("killall -v -QUIT ShipIt");
system("killall -QUIT ShipIt");
});
it(@"should handle SIGKILL", ^{
// SIGKILL is unique in that it'll always terminate ShipIt, so send it
// a few times to really test resumption.
for (int i = 0; i < 3; i++) {
system("killall -v -KILL ShipIt");
system("killall -KILL ShipIt");
// Wait at least for the launchd throttle interval.
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
+6 -3
View File
@@ -40,7 +40,8 @@ it(@"should wait until one instance terminates", ^{
expect(observedApp).will.equal(app);
expect(completed).to.beFalsy();
expect([app terminate]).to.beTruthy();
[app forceTerminate];
expect(app.terminated).will.beTruthy();
expect(completed).will.beTruthy();
});
@@ -55,10 +56,12 @@ it(@"should wait until multiple instances terminate", ^{
expect(completed).to.beFalsy();
expect([app1 terminate]).to.beTruthy();
[app1 forceTerminate];
expect(app1.terminated).will.beTruthy();
expect(completed).to.beFalsy();
expect([app2 terminate]).to.beTruthy();
[app2 forceTerminate];
expect(app2.terminated).will.beTruthy();
expect(completed).will.beTruthy();
});
+7
View File
@@ -88,4 +88,11 @@ extern NSString * const SQRLBundleShortVersionStringKey;
// automatically be unmounted and deleted at the end of the example.
- (NSURL *)createAndMountDiskImageNamed:(NSString *)name fromDirectory:(NSURL *)directoryURL;
// Add a block to cleanup after each example has run
//
// Blocks are run in reverse order, i.e. LIFO
//
// block - The block to invoke after the current example has finished
- (void)addCleanupBlock:(dispatch_block_t)block;
@end
+45 -25
View File
@@ -51,6 +51,10 @@ static void SQRLSignalHandler(int sig) {
// all copied test data.
@property (nonatomic, copy, readonly) NSURL *baseTemporaryDirectoryURL;
// Returns an _unlaunched_ task that will follow log files at the given paths,
// then pipe that output through this process.
+ (NSTask *)tailTaskWithPaths:(RACSignal *)paths;
@end
@implementation SQRLTestCase
@@ -71,27 +75,27 @@ static void SQRLSignalHandler(int sig) {
@"otest-x86_64.ShipIt",
];
RACSequence *URLs = [folders.rac_sequence flattenMap:^(NSString *folder) {
NSURL *baseURL = [appSupportURL URLByAppendingPathComponent:folder];
return @[
[baseURL URLByAppendingPathComponent:@"ShipIt_stdout.log"],
[baseURL URLByAppendingPathComponent:@"ShipIt_stderr.log"]
].rac_sequence;
}];
for (NSURL *URL in URLs) {
[[NSData data] writeToURL:URL atomically:YES];
}
NSArray *args = [[[URLs
map:^(NSURL *URL) {
return URL.path;
RACSignal *logLocations = [[[[folders.rac_signal
flattenMap:^(NSString *folder) {
return [folder stringsByAppendingPaths:@[
@"ShipIt_stdout.log",
@"ShipIt_stderr.log",
]].rac_signal;
}]
startWith:@"-f"]
array];
map:^(NSString *path) {
return [appSupportURL URLByAppendingPathComponent:path];
}]
doNext:^(NSURL *location) {
[[NSData data] writeToURL:location atomically:YES];
}]
map:^(NSURL *location) {
return location.path;
}];
NSTask *readShipIt = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:args];
NSAssert([readShipIt isRunning], @"Could not start task %@ with arguments: %@", readShipIt, args);
NSTask *readShipIt = [self tailTaskWithPaths:logLocations];
[readShipIt launch];
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
atexit_b(^{
[readShipIt terminate];
@@ -131,6 +135,24 @@ static void SQRLSignalHandler(int sig) {
[self.exampleCleanupBlocks addObject:[block copy]];
}
#pragma mark Logging
+ (NSTask *)tailTaskWithPaths:(RACSignal *)paths {
NSPipe *outputPipe = [NSPipe pipe];
NSFileHandle *outputHandle = outputPipe.fileHandleForReading;
outputHandle.readabilityHandler = ^(NSFileHandle *handle) {
NSString *output = [[NSString alloc] initWithData:handle.availableData encoding:NSUTF8StringEncoding];
NSLog(@"\n%@", output);
};
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/tail";
task.standardOutput = outputPipe;
task.arguments = [[paths startWith:@"-f"] array];
return task;
}
#pragma mark Temporary Directory
- (NSURL *)baseTemporaryDirectoryURL {
@@ -176,7 +198,9 @@ static void SQRLSignalHandler(int sig) {
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
[[NSData data] writeToURL:testAppLog atomically:YES];
NSTask *readTestApp = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:@[ @"-f", testAppLog.path ]];
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSignal return:testAppLog.path]];
[readTestApp launch];
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
[self addCleanupBlock:^{
@@ -301,11 +325,7 @@ static void SQRLSignalHandler(int sig) {
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
CFErrorRef removeError = NULL;
if (!SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, &removeError)) {
NSLog(@"Could not remove ShipIt job after tests: %@", removeError);
if (removeError != NULL) CFRelease(removeError);
}
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
NSError *lookupError = nil;
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
+173 -71
View File
@@ -6,9 +6,12 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLTestUpdate.h"
#import "SQRLDirectoryManager.h"
#import "SQRLZipArchiver.h"
#import "SQRLTestUpdate.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
SpecBegin(SQRLUpdater)
__block NSURL *JSONURL;
@@ -49,85 +52,184 @@ beforeEach(^{
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
});
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
describe(@"updating", ^{
__block NSURL *updateURL;
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
writeUpdate(update);
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should not install a corrupt update", ^{
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
it(@"should update to the most recently enqueued job", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"cleaning up", ^{
__block NSURL *appSupportURL;
__block RACSignal *updateDirectoryURLs;
beforeEach(^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication.ShipIt"];
appSupportURL = [[directoryManager applicationSupportURL] first];
expect(appSupportURL).notTo.beNil();
updateDirectoryURLs = [[RACSignal
defer:^{
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:appSupportURL includingPropertiesForKeys:nil options:0 error:NULL];
if (contents == nil) return [RACSignal empty];
return contents.rac_signal;
}]
filter:^(NSURL *directoryURL) {
return [directoryURL.lastPathComponent hasPrefix:@"update."];
}];
});
it(@"should remove downloaded archives after updating", ^{
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
// Also test that this is the only update directory (any others
// should've been removed).
NSArray *directoryURLs = [updateDirectoryURLs array];
expect(directoryURLs.count).to.equal(1);
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:directoryURLs[0] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:NULL];
expect(contents).to.equal(@[]);
});
});
});
it(@"should not install a corrupt update", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
describe(@"response handling", ^{
__block NSURLRequest *localRequest = nil;
__block SQRLUpdater *updater = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
before(^{
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"fake://host/path"]];
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
});
writeUpdate(update);
it(@"should return an error for non 2xx code HTTP responses", ^{
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL isEqual:localRequest.URL];
} withStubResponse:^(NSURLRequest *request) {
return [OHHTTPStubsResponse responseWithData:nil statusCode:/* Server Error */ 500 responseTime:0 headers:nil];
}];
[self addCleanupBlock:^{
[OHHTTPStubs removeRequestHandler:stubs];
}];
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
});
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
it(@"should return an error for non JSON data", ^{
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL isEqual:localRequest.URL];
} withStubResponse:^(NSURLRequest *request) {
return [OHHTTPStubsResponse responseWithData:NSData.data statusCode:/* OK */ 200 responseTime:0 headers:nil];
}];
[self addCleanupBlock:^{
[OHHTTPStubs removeRequestHandler:stubs];
}];
it(@"should update to the most recently enqueued job", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
NSURL *updateURL = [self createTestApplicationUpdate];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
});
});
SpecEnd
+5 -5
View File
@@ -70,18 +70,18 @@
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
updateCheckCount++;
return [self.updater.checkForUpdatesCommand execute:RACUnit.defaultUnit];
return [self.updater.checkForUpdatesAction signalWithValue:nil];
}]
doNext:^(SQRLDownloadedUpdate *update) {
NSLog(@"Got a candidate update: %@", update);
}]
// Retry until we get the expected release.
repeat]
skipUntilBlock:^(SQRLDownloadedUpdate *download) {
skipWhile:^ BOOL (SQRLDownloadedUpdate *download) {
SQRLTestUpdate *testUpdate = (id)download.update;
NSAssert([testUpdate isKindOfClass:SQRLTestUpdate.class], @"Unexpected update type: %@", testUpdate);
return testUpdate.final;
return !testUpdate.final;
}]
take:1]
doNext:^(id _) {
@@ -92,12 +92,12 @@
NSLog(@"Error in updater: %@", error);
return [RACSignal empty];
}]
then:^{
concat:[RACSignal defer:^{
NSString *delayString = NSProcessInfo.processInfo.environment[@"SQRLUpdateDelay"];
if (delayString == nil) return [RACSignal empty];
return [[RACSignal interval:delayString.doubleValue onScheduler:RACScheduler.mainThreadScheduler] take:1];
}]
}]]
subscribeCompleted:^{
[NSApp terminate:self];
}];
+68 -6
View File
@@ -1,6 +1,66 @@
These scripts are primarily meant to support the use of
[Janky](https://github.com/github/janky). To use them, read the contents of this
repository into a `script` folder:
# objc-build-scripts
This project is a collection of scripts created with two goals:
1. To standardize how Objective-C projects are bootstrapped after cloning
1. To easily build Objective-C projects on continuous integration servers
## Scripts
Right now, there are two important scripts: [`bootstrap`](#bootstrap) and
[`cibuild`](#cibuild). Both are Bash scripts, to maximize compatibility and
eliminate pesky system configuration issues (like setting up a working Ruby
environment).
The structure of the scripts on disk is meant to follow that of a typical Ruby
project:
```
script/
bootstrap
cibuild
```
### bootstrap
This script is responsible for bootstrapping (initializing) your project after
it's been checked out. Here, you should install or clone any dependencies that
are required for a working build and development environment.
By default, the script will verify that [xctool][] is installed, then initialize
and update submodules recursively. If any submodules contain `script/bootstrap`,
that will be run as well.
To check that other tools are installed, you can set the `REQUIRED_TOOLS`
environment variable before running `script/bootstrap`, or edit it within the
script directly. Note that no installation is performed automatically, though
this can always be added within your specific project.
### cibuild
This script is responsible for building the project, as you would want it built
for continuous integration. This is preferable to putting the logic on the CI
server itself, since it ensures that any changes are versioned along with the
source.
By default, the script will run [`bootstrap`](#bootstrap), look for any Xcode
workspace or project in the working directory, then build all targets/schemes
(as found by `xcodebuild -list`) using [xctool][].
You can also specify the schemes to build by passing them into the script:
```sh
script/cibuild ReactiveCocoa-Mac ReactiveCocoa-iOS
```
As with the `bootstrap` script, there are several environment variables that can
be used to customize behavior. They can be set on the command line before
invoking the script, or the defaults changed within the script directly.
## Getting Started
To add the scripts to your project, read the contents of this repository into
a `script` folder:
```
$ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git
@@ -8,13 +68,15 @@ $ git fetch objc-build-scripts
$ git read-tree --prefix=script/ -u objc-build-scripts/master
```
Then commit the changes to incorporate the scripts into your own repository's
Then commit the changes, to incorporate the scripts into your own repository's
history. You can also freely tweak the scripts for your specific project's
needs.
To bring in upstream changes later:
To merge in upstream changes later:
```
$ git fetch -p objc-build-scripts
$ git merge -Xsubtree=script objc-build-scripts/master
$ git merge --ff --squash -Xsubtree=script objc-build-scripts/master
```
[xctool]: https://github.com/facebook/xctool
+69 -7
View File
@@ -1,11 +1,73 @@
#!/bin/bash
SCRIPT_DIR=$(dirname "$0")
cd "$SCRIPT_DIR/.."
export SCRIPT_DIR=$(dirname "$0")
set -o errexit
##
## Configuration Variables
##
echo "*** Updating submodules..."
git submodule sync --quiet
git submodule update --init
git submodule foreach --recursive --quiet "git submodule sync --quiet && git submodule update --init"
config ()
{
# A whitespace-separated list of executables that must be present and locatable.
: ${REQUIRED_TOOLS="xctool"}
export REQUIRED_TOOLS
}
##
## Bootstrap Process
##
main ()
{
config
if [ -n "$REQUIRED_TOOLS" ]
then
echo "*** Checking dependencies..."
check_deps
fi
local submodules=$(git submodule status 2>/dev/null)
if [ -n "$submodules" ]
then
echo "*** Updating submodules..."
update_submodules
fi
}
check_deps ()
{
for tool in $REQUIRED_TOOLS
do
which -s "$tool"
if [ "$?" -ne "0" ]
then
echo "*** Error: $tool not found. Please install it and bootstrap again."
exit 1
fi
done
}
bootstrap_submodule ()
{
local bootstrap="script/bootstrap"
if [ -e "$bootstrap" ]
then
echo "*** Bootstrapping $name..."
"$bootstrap" >/dev/null
else
update_submodules
fi
}
update_submodules ()
{
git submodule sync --quiet && git submodule update --init && git submodule foreach --quiet bootstrap_submodule
}
export -f bootstrap_submodule
export -f update_submodules
main
+113 -80
View File
@@ -1,129 +1,162 @@
#!/bin/bash
SCRIPT_DIR=$(dirname "$0")
cd "$SCRIPT_DIR/.."
export SCRIPT_DIR=$(dirname "$0")
##
## Configuration Variables
##
# The build configuration to use.
if [ -z "$XCCONFIGURATION" ]
then
XCCONFIGURATION="Test"
fi
SCHEMES="$@"
# The workspace to build.
#
# If not set and no workspace is found, the -workspace flag will not be passed
# to xcodebuild.
if [ -z "$XCWORKSPACE" ]
then
XCWORKSPACE=$(ls -d *.xcworkspace 2>/dev/null | head -n 1)
fi
config ()
{
# The workspace to build.
#
# If not set and no workspace is found, the -workspace flag will not be passed
# to `xctool`.
#
# Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will
# take precedence.
: ${XCWORKSPACE=$(find_pattern "*.xcworkspace")}
# A bootstrap script to run before building.
#
# If this file does not exist, it is not considered an error.
BOOTSTRAP="$SCRIPT_DIR/bootstrap"
# The project to build.
#
# If not set and no project is found, the -project flag will not be passed
# to `xctool`.
#
# Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will
# take precedence.
: ${XCODEPROJ=$(find_pattern "*.xcodeproj")}
# A whitespace-separated list of default targets or schemes to build, if none
# are specified on the command line.
#
# Individual names can be quoted to avoid word splitting.
DEFAULT_TARGETS=Squirrel
# A bootstrap script to run before building.
#
# If this file does not exist, it is not considered an error.
: ${BOOTSTRAP="$SCRIPT_DIR/bootstrap"}
# Extra build settings to pass to xcodebuild.
XCODEBUILD_SETTINGS="TEST_AFTER_BUILD=YES"
# Extra options to pass to xctool.
: ${XCTOOL_OPTIONS="RUN_CLANG_STATIC_ANALYZER=NO"}
# A whitespace-separated list of default schemes to build.
#
# Individual names can be quoted to avoid word splitting.
: ${SCHEMES:=Squirrel}
# A source-able file with information about the Keychain, containing our
# code-signing certificate.
: ${KEYCHAIN_INFO:=/var/lib/jenkins/config/xcodekeychain}
export XCWORKSPACE
export XCODEPROJ
export BOOTSTRAP
export XCTOOL_OPTIONS
export SCHEMES
export KEYCHAIN_INFO
}
##
## Code-signing Setup
## Code Signing
##
# A source-able file with information about the Keychain containing our
# code-signing certificate.
KEYCHAIN_INFO=/var/lib/jenkins/config/xcodekeychain
if [ -e "$KEYCHAIN_INFO" ]
then
# Unlock the keychain so the certificates it contains can be used for building.
. "${KEYCHAIN_INFO}"
security unlock-keychain -p "${XCODE_KEYCHAIN_PASSWORD}" "${XCODE_KEYCHAIN}"
fi
unlock_keychain ()
{
if [ -e "$KEYCHAIN_INFO" ]
then
# Unlock the keychain so the certificates it contains can be used for building.
. "$KEYCHAIN_INFO"
security unlock-keychain -p "$XCODE_KEYCHAIN_PASSWORD" "$XCODE_KEYCHAIN"
fi
}
##
## Build Process
##
if [ -z "$*" ]
then
# lol recursive shell script
if [ -n "$DEFAULT_TARGETS" ]
main ()
{
config
unlock_keychain
if [ -f "$BOOTSTRAP" ]
then
echo "$DEFAULT_TARGETS" | xargs "$SCRIPT_DIR/cibuild"
else
xcodebuild -list | awk -f "$SCRIPT_DIR/targets.awk" | xargs "$SCRIPT_DIR/cibuild"
echo "*** Bootstrapping..."
"$BOOTSTRAP" || exit $?
fi
exit $?
fi
echo "*** The following schemes will be built:"
echo "$SCHEMES" | xargs -n 1 echo " "
echo
if [ -f "$BOOTSTRAP" ]
then
echo "*** Bootstrapping..."
bash "$BOOTSTRAP" || exit $?
fi
echo "$SCHEMES" | xargs -n 1 | (
local status=0
echo "*** The following targets will be built:"
while read scheme
do
build_scheme "$scheme" || status=1
done
for target in "$@"
do
echo "$target"
done
exit $status
)
}
echo "*** Cleaning all targets..."
xcodebuild -alltargets clean OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
run_xcodebuild ()
find_pattern ()
{
local scheme=$1
ls -d $1 2>/dev/null | head -n 1
}
run_xctool ()
{
if [ -n "$XCWORKSPACE" ]
then
xcodebuild -workspace "$XCWORKSPACE" -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
xctool -workspace "$XCWORKSPACE" $XCTOOL_OPTIONS "$@" 2>&1
elif [ -n "$XCODEPROJ" ]
then
xctool -project "$XCODEPROJ" $XCTOOL_OPTIONS "$@" 2>&1
else
xcodebuild -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
echo "*** No workspace or project file found."
exit 1
fi
}
local status=$?
return $status
parse_build ()
{
awk -f "$SCRIPT_DIR/xctool.awk" 2>&1 >/dev/null
}
build_scheme ()
{
local scheme=$1
run_xcodebuild "$scheme" 2>&1 | awk -f "$SCRIPT_DIR/xcodebuild.awk"
echo "*** Cleaning $scheme..."
run_xctool -scheme "$scheme" clean >/dev/null || exit $?
echo "*** Building and testing $scheme..."
echo
local sdkflag=
local action=test
# Determine whether we can run unit tests for this target.
run_xctool -scheme "$scheme" run-tests | parse_build
local awkstatus=$?
local xcstatus=${PIPESTATUS[0]}
if [ "$xcstatus" -eq "65" ]
if [ "$awkstatus" -ne "0" ]
then
# This probably means that there's no scheme by that name. Give up.
echo "*** Error building scheme $scheme -- perhaps it doesn't exist"
elif [ "$awkstatus" -eq "1" ]
then
return $awkstatus
# Unit tests aren't supported.
action=build
fi
return $xcstatus
if [ "$awkstatus" -eq "1" ]
then
# Build for iOS.
sdkflag="-sdk iphonesimulator"
fi
run_xctool $sdkflag -scheme "$scheme" $action
}
echo "*** Building..."
export -f build_scheme
export -f run_xctool
export -f parse_build
for scheme in "$@"
do
build_scheme "$scheme" || exit $?
done
main
+12
View File
@@ -0,0 +1,12 @@
BEGIN {
FS = "\n";
}
/Targets:/ {
while (getline && $0 != "") {
if ($0 ~ /Test/) continue;
sub(/^ +/, "");
print "'" $0 "'";
}
}
+25
View File
@@ -0,0 +1,25 @@
# Exit statuses:
#
# 0 - No errors found.
# 1 - Wrong SDK. Retry with SDK `iphonesimulator`.
# 2 - Missing target.
BEGIN {
status = 0;
}
{
print;
}
/Testing with the '(.+)' SDK is not yet supported/ {
status = 1;
}
/does not contain a target named/ {
status = 2;
}
END {
exit status;
}