Author SHA1 Message Date
Keith Duncan 9b12e8faa7 Move all the error codes into one domain 2013-10-30 13:03:04 +00:00
Keith Duncan e8c0e4748e Add Squirrel-Constants for SQRLErrorDomain 2013-10-30 12:38:11 +00:00
37 changed files with 467 additions and 1171 deletions
+1 -4
View File
@@ -3,7 +3,4 @@
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "External/Mantle"]
path = External/Mantle
url = https://github.com/MantleFramework/Mantle.git
[submodule "External/OHHTTPStubs"]
path = External/OHHTTPStubs
url = https://github.com/github/OHHTTPStubs
url = https://github.com/github/Mantle.git
Submodule External/OHHTTPStubs deleted from acaf8b8af7
+45 -108
View File
@@ -13,46 +13,21 @@ suitable update.
The update JSON Squirrel requests should be dynamically generated based on
criteria in the request, and whether an update is required. Squirrel relies
on server side support for determining whether an update is required, see
[Server Support](#server-support).
on server side support for determining whether an update is required, see [Server
Support](#server-support) below.
Squirrel's installer is also designed to be fault tolerant, and ensure that any
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 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
1. Ensure your Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`) includes the
Frameworks directory Squirrel.framework is copied into
Once Squirrel is added to your project, you need to configure and start it.
@@ -60,89 +35,37 @@ Once Squirrel is added to your project, you need to configure and start it.
#import <Squirrel/Squirrel.h>
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
NSURLComponents *components = [[NSURLComponents alloc] init];
components.scheme = @"http";
components.host = @"mycompany.com";
components.path = @"/myapp/latest";
NSString *bundleVersion = NSBundle.mainBundle.sqrl_bundleVersion;
components.query = [[NSString stringWithFormat:@"version=%@", bundleVersion] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:components.URL]];
// Check for updates every 4 hours.
[self.updater startAutomaticChecksWithInterval:60 * 60 * 4];
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];
}
```
Squirrel will periodically request and automatically download any updates. When
your application terminates, any downloaded update will be automatically
installed.
Squirrel will periodically request and automatically download any updates.
## Update Requests
Squirrel is indifferent to the request the client application provides for
update checking. `Accept: application/json` is added to the request headers
because Squirrel is responsible for parsing the response.
For the requirements imposed on the responses and the body format of an update
response see [Server Support](#server-support).
Your update request must *at least* include a version identifier so that the
server can determine whether an update for this specific version is required. It
may also include other identifying criteria such as operating system version or
username, to allow the server to deliver as fine grained an update as you
would like.
How you include the version identifier or other criteria is specific to the
server that you are requesting updates from. A common approach is to use query
parameters, [Configuration](#configuration) shows an example of this.
## Update Available Notifications
To know when an update is ready to be installed, you can subscribe to the
`updates` signal on `SQRLUpdater`:
Before your application terminates, it should tell Squirrel to install any
updates that it has downloaded:
```objc
[self.updater.updates subscribeNext:^(SQRLDownloadedUpdate *downloadedUpdate) {
NSLog(@"An update is ready to install: %@", downloadedUpdate);
}];
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
[self.updater installUpdateIfNeeded:^(BOOL success, NSError *error) {
if (success) {
NSLog(@"Successfully installed an update");
} else {
NSLog(@"Failed to update: %@", error);
}
[sender replyToApplicationShouldTerminate:YES];
}];
return NSTerminateLater;
}
```
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.
# Update JSON Format
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);
}];
```
# Server Support
Your server should determine whether an update is required based on the
[Update Request](#update-requests) your client issues.
If an update is required your server should respond with a status code of
[200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the
[update JSON](#update-json-format) in the body. Squirrel **will** download and
install this update, even if the version of the update is the same as the
currently running version. To save redundantly downloading the same version
multiple times your server must not inform the client to update.
If no update is required your server must respond with a status code of
[204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5). Squirrel
will check for an update again at the interval you specify.
## Update JSON Format
When an update is available, Squirrel expects the following schema in response
to the update request provided:
Squirrel requests the URL you provide with `Accept: application/json` and
expects the following schema in response:
```json
{
@@ -160,10 +83,24 @@ installing ZIP updates. If future update formats are supported their MIME type
will be added to the `Accept` header so that your server can return the
appropriate format.
"pub_date" if present must be formatted according to ISO 8601.
"pub_date" if present must be formatted according to ISO 8601
# Server Support
If an update is required your server should respond with a status code of
[200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the
update JSON in the body. Squirrel **will** download and install this update,
even if the version of the update is the same as the currently running version.
To save redundantly downloading the same version multiple times your server must
inform the client not to update.
If no update is required your server must respond with a status code of
[204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5). Squirrel
will check for an update again at the interval you specify.
# User Interface
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).
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)
+30 -88
View File
@@ -15,12 +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 */; };
53A60408182134F9002DB2C7 /* Squirrel-Constants.h in Headers */ = {isa = PBXBuildFile; fileRef = 53A60406182134F9002DB2C7 /* Squirrel-Constants.h */; settings = {ATTRIBUTES = (Public, ); }; };
53A60409182134F9002DB2C7 /* Squirrel-Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A60407182134F9002DB2C7 /* Squirrel-Constants.m */; };
53A6040A182134F9002DB2C7 /* Squirrel-Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A60407182134F9002DB2C7 /* Squirrel-Constants.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 */; };
@@ -109,34 +109,6 @@
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 */;
@@ -421,12 +393,11 @@
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>"; };
53A60406182134F9002DB2C7 /* Squirrel-Constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Squirrel-Constants.h"; sourceTree = "<group>"; };
53A60407182134F9002DB2C7 /* Squirrel-Constants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Squirrel-Constants.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>"; };
@@ -574,7 +545,6 @@
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 */,
@@ -626,23 +596,13 @@
path = script;
sourceTree = "<group>";
};
5374DCD8187AD0DC006B7056 /* Authorization */ = {
53A603F7182134D3002DB2C7 /* Supporting Files */ = {
isa = PBXGroup;
children = (
5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */,
5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */,
53A60406182134F9002DB2C7 /* Squirrel-Constants.h */,
53A60407182134F9002DB2C7 /* Squirrel-Constants.m */,
);
name = Authorization;
sourceTree = "<group>";
};
53B70015181E944F000FFBD0 /* Products */ = {
isa = PBXGroup;
children = (
53B70021181E944F000FFBD0 /* libOHHTTPStubs-iOS.a */,
53B70023181E944F000FFBD0 /* UnitTests.octest */,
53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */,
);
name = Products;
name = "Supporting Files";
sourceTree = "<group>";
};
D00F5B8217E82CFB009A4818 /* Extensions */ = {
@@ -820,7 +780,6 @@
children = (
D097B9BC17BB4777006C3FEB /* IOKit.framework */,
D014ABB917B97403007D79D0 /* ServiceManagement.framework */,
53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */,
D0F3F01417E83918003AFA6B /* Expecta.xcodeproj */,
D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */,
D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */,
@@ -981,7 +940,7 @@
D0A56BF61804BABB00A84EDC /* Code Signing */,
D0A56BE71804BA2400A84EDC /* State */,
D06B58BC18032BE800656D97 /* Transactions */,
5374DCD8187AD0DC006B7056 /* Authorization */,
53A603F7182134D3002DB2C7 /* Supporting Files */,
);
name = Shared;
path = Squirrel;
@@ -1030,12 +989,12 @@
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 */,
D0964B3817F2E01500D88BF7 /* SQRLDownloadedUpdate.h in Headers */,
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */,
53A60408182134F9002DB2C7 /* Squirrel-Constants.h in Headers */,
D00F5B8B17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1130,11 +1089,11 @@
D0C22BEA179CC00E00158214 /* Sources */,
D0C22BEB179CC00E00158214 /* Frameworks */,
D0C22BEC179CC00E00158214 /* Resources */,
D0C22BED179CC00E00158214 /* ShellScript */,
);
buildRules = (
);
dependencies = (
53B7002F181E9456000FFBD0 /* PBXTargetDependency */,
D0F3F03317E83924003AFA6B /* PBXTargetDependency */,
D0F3F03517E83924003AFA6B /* PBXTargetDependency */,
D08D4E4C17B4520C0012B22D /* PBXTargetDependency */,
@@ -1175,10 +1134,6 @@
ProductGroup = D0A56B971804B1FD00A84EDC /* Products */;
ProjectRef = D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */;
},
{
ProductGroup = 53B70015181E944F000FFBD0 /* Products */;
ProjectRef = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
},
{
ProductGroup = D0F3F04B17E83A1D003AFA6B /* Products */;
ProjectRef = D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */;
@@ -1200,27 +1155,6 @@
/* 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;
@@ -1406,6 +1340,22 @@
};
/* 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;
@@ -1425,6 +1375,7 @@
D014AC0517B97885007D79D0 /* ShipIt-main.m in Sources */,
D014AC1917B979A8007D79D0 /* SQRLCodeSignature.m in Sources */,
D06B58B618032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
53A6040A182134F9002DB2C7 /* Squirrel-Constants.m in Sources */,
D014AC1A17B979AA007D79D0 /* NSError+SQRLVerbosityExtensions.m in Sources */,
D0D2B6271804E903000EA901 /* SQRLDirectoryManager.m in Sources */,
D0964B3E17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.m in Sources */,
@@ -1455,8 +1406,8 @@
D0EDBE9D17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.m in Sources */,
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */,
D0A56BEB1804BA3C00A84EDC /* SQRLShipItState.m in Sources */,
53A60409182134F9002DB2C7 /* Squirrel-Constants.m in Sources */,
D00F5B8C17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m in Sources */,
5374DCC7187AD0D8006B7056 /* SQRLAuthorization.m in Sources */,
D06B58B518032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1490,11 +1441,6 @@
target = 534FF35F17D8E90A0020A51A /* TestService */;
targetProxy = 534FF37017D8E9170020A51A /* PBXContainerItemProxy */;
};
53B7002F181E9456000FFBD0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = OHHTTPStubs;
targetProxy = 53B7002E181E9456000FFBD0 /* PBXContainerItemProxy */;
};
D014AC1717B9793A007D79D0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D014AC0017B97885007D79D0 /* ShipIt */;
@@ -1645,7 +1591,6 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Debug;
};
@@ -1655,7 +1600,6 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Release;
};
@@ -1665,7 +1609,6 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Test;
};
@@ -1675,7 +1618,6 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Profile;
};
-27
View File
@@ -1,27 +0,0 @@
//
// 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
@@ -1,28 +0,0 @@
//
// 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
-10
View File
@@ -9,16 +9,6 @@
#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>
// The domain for errors originating within SQRLCodeSignature.
extern NSString * const SQRLCodeSignatureErrorDomain;
// The bundle did not pass codesign verification.
extern const NSInteger SQRLCodeSignatureErrorDidNotPass;
// A static code object could not be created for the target bundle or running
// code.
extern const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode;
@class RACSignal;
// Implements the verification of Apple code signatures.
+3 -7
View File
@@ -10,11 +10,7 @@
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
NSString * const SQRLCodeSignatureErrorDomain = @"SQRLCodeSignatureErrorDomain";
const NSInteger SQRLCodeSignatureErrorDidNotPass = -1;
const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
#import "Squirrel-Constants.h"
@interface SQRLCodeSignature ()
@@ -99,7 +95,7 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
NSString *failureReason = CFBridgingRelease(SecCopyErrorMessageString(result, NULL));
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
[subscriber sendError:[NSError errorWithDomain:SQRLErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
return nil;
}
@@ -118,7 +114,7 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
if (validityError != NULL) userInfo[NSUnderlyingErrorKey] = (__bridge NSError *)validityError;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
[subscriber sendError:[NSError errorWithDomain:SQRLErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
return nil;
}
-3
View File
@@ -8,9 +8,6 @@
#import <Foundation/Foundation.h>
// The domain for errors originating within SQRLInstaller.
extern NSString * const SQRLInstallerErrorDomain;
// There was an error copying the target bundle to the backup location.
extern const NSInteger SQRLInstallerErrorBackupFailed;
+26 -5
View File
@@ -18,8 +18,7 @@
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <sys/xattr.h>
NSString * const SQRLInstallerErrorDomain = @"SQRLInstallerErrorDomain";
#import "Squirrel-Constants.h"
const NSInteger SQRLInstallerErrorBackupFailed = -1;
const NSInteger SQRLInstallerErrorReplacingTarget = -2;
@@ -211,6 +210,7 @@ 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);
@@ -235,7 +235,7 @@ typedef struct {
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
}
SEL selector = dispatchTable[tableIndex].selector;
@@ -397,6 +397,27 @@ typedef struct {
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 {
@@ -597,7 +618,7 @@ typedef struct {
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
};
return [NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorMissingInstallationData userInfo:userInfo];
return [NSError errorWithDomain:SQRLErrorDomain code:SQRLInstallerErrorMissingInstallationData userInfo:userInfo];
}
- (NSError *)errorByAddingDescription:(NSString *)description code:(NSInteger)code toError:(NSError *)error {
@@ -606,7 +627,7 @@ typedef struct {
if (description != nil) userInfo[NSLocalizedDescriptionKey] = description;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [NSError errorWithDomain:SQRLInstallerErrorDomain code:code userInfo:userInfo];
return [NSError errorWithDomain:SQRLErrorDomain code:code userInfo:userInfo];
}
@end
-6
View File
@@ -10,12 +10,6 @@
@class RACSignal;
// The domain for errors originating within SQRLShipItLauncher.
extern NSString * const SQRLShipItLauncherErrorDomain;
// The ShipIt service could not be started.
extern const NSInteger SQRLShipItLauncherErrorCouldNotStartService;
// Responsible for launching the ShipIt service to actually install an update.
@interface SQRLShipItLauncher : NSObject
+15 -18
View File
@@ -13,11 +13,7 @@
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "SQRLAuthorization.h"
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
#import "Squirrel-Constants.h"
@implementation SQRLShipItLauncher
@@ -72,7 +68,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
+ (RACSignal *)shipItAuthorization {
return [[RACSignal
createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
createSignal:^(id<RACSubscriber> subscriber) {
AuthorizationItem rightItems[] = {
{
.name = kSMRightModifySystemDaemons,
@@ -109,15 +105,16 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
[subscriber sendNext:(__bridge id)authorization];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
return nil;
return [RACDisposable disposableWithBlock:^{
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
}];
}]
setNameWithFormat:@"+shipItAuthorization"];
}
@@ -127,22 +124,22 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
zip:@[
self.shipItJobDictionary,
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
] reduce:^(NSDictionary *jobDictionary, id authorization) {
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
AuthorizationRef authorization = authorizationValue.authorization;
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
#if DEBUG
NSLog(@"Could not remove previous ShipIt job: %@", cfError);
#endif
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
NSLog(@"Could not remove previous ShipIt job: %@", error);
if (cfError != NULL) {
CFRelease(cfError);
cfError = NULL;
}
}
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, (__bridge AuthorizationRef)authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
+4 -16
View File
@@ -8,22 +8,6 @@
#import <Mantle/Mantle.h>
// The domain for errors originating within `SQRLShipItState`.
extern NSString * const SQRLShipItStateErrorDomain;
// A required property was `nil` upon initialization.
//
// The `userInfo` dictionary for this error will contain
// `SQRLShipItStatePropertyErrorKey`.
extern const NSInteger SQRLShipItStateErrorMissingRequiredProperty;
// The saved state on disk could not be unarchived, possibly because it's
// invalid.
extern const NSInteger SQRLShipItStateErrorUnarchiving;
// The state object could not be archived.
extern const NSInteger SQRLShipItStateErrorArchiving;
// Associated with an NSString indicating the required property key that did not
// have a value upon initialization.
extern NSString * const SQRLShipItStatePropertyErrorKey;
@@ -41,6 +25,9 @@ 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.
@@ -50,6 +37,7 @@ typedef enum : NSInteger {
SQRLInstallerStateBackingUp,
SQRLInstallerStateInstalling,
SQRLInstallerStateVerifyingInPlace,
SQRLInstallerStateRelaunching
} SQRLInstallerState;
@class RACSignal;
+4 -8
View File
@@ -8,14 +8,10 @@
#import "SQRLShipItState.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "Squirrel-Constants.h"
NSString * const SQRLShipItStateErrorDomain = @"SQRLShipItStateErrorDomain";
NSString * const SQRLShipItStatePropertyErrorKey = @"SQRLShipItStatePropertyErrorKey";
const NSInteger SQRLShipItStateErrorMissingRequiredProperty = 1;
const NSInteger SQRLShipItStateErrorUnarchiving = 2;
const NSInteger SQRLShipItStateErrorArchiving = 3;
@implementation SQRLShipItState
#pragma mark Lifecycle
@@ -33,7 +29,7 @@ const NSInteger SQRLShipItStateErrorArchiving = 3;
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
};
*error = [NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
*error = [NSError errorWithDomain:SQRLErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
}
return NO;
@@ -78,7 +74,7 @@ const NSInteger SQRLShipItStateErrorArchiving = 3;
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while unarchiving.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
}
return [RACSignal return:state];
@@ -97,7 +93,7 @@ const NSInteger SQRLShipItStateErrorArchiving = 3;
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
}
return [RACSignal return:data];
+2 -16
View File
@@ -134,33 +134,19 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
- (BOOL)validateUpdateURL:(NSURL **)updateURLPtr error:(NSError **)error {
NSURL *updateURL = *updateURLPtr;
if (![updateURL isKindOfClass:NSURL.class]) {
if (![updateURL isKindOfClass:NSURL.class] || updateURL.scheme == nil || updateURL.host == nil || updateURL.path == nil) {
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;
}
+5 -44
View File
@@ -6,35 +6,6 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
// The domain for errors originating within SQRLUpdater.
extern NSString * const SQRLUpdaterErrorDomain;
// The downloaded update does not contain an app bundle, or it was deleted on
// disk before we could get to it.
extern const NSInteger SQRLUpdaterErrorMissingUpdateBundle;
// An error occurred in the out-of-process updater while it was setting up.
extern const NSInteger SQRLUpdaterErrorPreparingUpdateJob;
// The code signing requirement for the running application could not be
// retrieved.
extern const NSInteger SQRLUpdaterErrorRetrievingCodeSigningRequirement;
// The server sent a response that we didn't understand.
//
// 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`.
extern const NSInteger SQRLUpdaterErrorInvalidJSON;
// Associated with the `NSData` received from the server when an error with code
// `SQRLUpdaterErrorInvalidServerResponse` is generated.
extern NSString * const SQRLUpdaterServerDataErrorKey;
@@ -62,6 +33,11 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// flattened for convenience.
@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,21 +75,6 @@ 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 invoking this method, 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)
+44 -187
View File
@@ -20,22 +20,11 @@
#import "SQRLZipArchiver.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "Squirrel-Constants.h"
NSString * const SQRLUpdaterErrorDomain = @"SQRLUpdaterErrorDomain";
NSString * const SQRLUpdaterServerDataErrorKey = @"SQRLUpdaterServerDataErrorKey";
NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey";
const NSInteger SQRLUpdaterErrorMissingUpdateBundle = 2;
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 ()
// The code signature for the running application, used to check updates before
@@ -47,16 +36,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// 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.
@@ -124,16 +103,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// 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
@@ -164,46 +133,11 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSError *error = nil;
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
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
}
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", error);
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
@weakify(self);
_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 [[enumerator.rac_sequence.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);
}
}];
}]
replayLazily]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
@strongify(self);
NSParameterAssert(self.updateRequest != nil);
@@ -212,42 +146,20 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
// Prune old updates before the first update check.
return [[[[[[[self.prunedUpdateDirectories
catch:^(NSError *error) {
NSLog(@"Error pruning old updates: %@", error);
return [RACSignal empty];
return [[[[[[NSURLConnection
rac_sendAsynchronousRequest:request]
reduceEach:^(id _, NSData *data) {
return data;
}]
then:^{
return [NSURLConnection rac_sendAsynchronousRequest:request];
}]
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];
}]
doError:^(id _) {
self.shouldRelaunch = NO;
}]
deliverOn:RACScheduler.mainThreadScheduler];
}];
@@ -304,7 +216,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
userInfo[SQRLUpdaterServerDataErrorKey] = data;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerBody userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:userInfo]];
}
Class updateClass = self.updateClass;
@@ -321,7 +233,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
userInfo[SQRLUpdaterJSONObjectErrorKey] = JSON;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidJSON userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorInvalidJSON userInfo:userInfo]];
}
return [RACSignal return:update];
@@ -360,25 +272,11 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
return [[[[NSURLConnection
return [[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
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];
reduceEach:^(id _, NSData *data) {
return data;
}]
flatten]
flattenMap:^(NSData *data) {
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:zipDownloadURL.lastPathComponent];
@@ -394,14 +292,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSLog(@"Download completed to: %@", zipOutputURL);
}]
flattenMap:^(NSURL *zipOutputURL) {
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);
}
}];
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory];
}]
then:^{
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
@@ -418,7 +309,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:@"update.XXXXXXX"];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
@@ -478,7 +369,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not locate update bundle for %@ within %@", nil), NSRunningApplication.currentApplication.bundleIdentifier, directory],
};
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:userInfo]];
}
}]
map:^(NSURL *URL) {
@@ -487,15 +378,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
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 {
@@ -516,37 +398,35 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
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 [[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
catchTo:[RACSignal empty]]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
}]
then:^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
return [state writeUsingURL:self.shipItStateURL];
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:SQRLErrorDomain 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;
@@ -555,27 +435,4 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
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]]
replay]
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
}
@end
-7
View File
@@ -10,17 +10,10 @@
@class RACSignal;
extern NSString * const SQRLZipArchiverErrorDomain;
// Associated with an NSNumber containing the code that a shell task exited
// with.
extern NSString * const SQRLZipArchiverExitCodeErrorKey;
// `SQRLZipArchiver` tried to invoke the shell and failed.
//
// Contains `SQRLZipArchiverExitStatusErrorKey` in the `userInfo` dictionary.
extern const NSInteger SQRLZipArchiverShellTaskFailed;
// Uses `ditto` on the command line to zip and unzip archives.
@interface SQRLZipArchiver : NSObject
+2 -3
View File
@@ -9,10 +9,9 @@
#import "SQRLZipArchiver.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "Squirrel-Constants.h"
NSString * const SQRLZipArchiverErrorDomain = @"SQRLZipArchiverErrorDomain";
NSString * const SQRLZipArchiverExitCodeErrorKey = @"SQRLZipArchiverExitCodeErrorKey";
const NSInteger SQRLZipArchiverShellTaskFailed = 1;
@interface SQRLZipArchiver () {
RACSubject *_taskTerminated;
@@ -143,7 +142,7 @@ const NSInteger SQRLZipArchiverShellTaskFailed = 1;
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:SQRLErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}];
}]
take:1]
+2 -31
View File
@@ -24,9 +24,6 @@
// updating will abort.
static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
// The domain for errors generated here.
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
// Waits for all instances of the target application (as described in the
// `state`) to exit, then sends completed.
static RACSignal *waitForTerminationIfNecessary(SQRLShipItState *state) {
@@ -76,10 +73,8 @@ 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) {
action = [[[installer.abortInstallationCommand
return [[[installer.abortInstallationCommand
execute:state]
initially:^{
NSLog(@"Too many attempts to install from state %i, aborting update", (int)state.installerState);
@@ -91,7 +86,7 @@ int main(int argc, const char * argv[]) {
return [RACSignal empty];
}];
} else {
action = [[[[[state
return [[[[[state
writeUsingURL:stateLocation]
initially:^{
if (freshInstall) {
@@ -111,30 +106,6 @@ int main(int argc, const char * argv[]) {
}]
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]
finally:^{
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);
+71
View File
@@ -0,0 +1,71 @@
//
// Squirrel-Constants.h
// Squirrel
//
// Created by Keith Duncan on 30/10/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
// Error domain for errors originating in Squirrel.
extern NSString * const SQRLErrorDomain;
// Error codes for errors originating in Squirrel.
//
// SQRLCodeSignatureErrorDidNotPass - The bundle did not pass codesign
// verification.
//
// SQRLCodeSignatureErrorCouldNotCreateStaticCode - A static code object could
// not be created for the target bundle or running code.
//
// SQRLShipItStateErrorMissingRequiredProperty - A required property was `nil`
// upon initialization. Includes `SQRLShipItStatePropertyErrorKey` in the
// error's `userInfo` dictionary.
//
// SQRLShipItStateErrorUnarchiving - The saved state on disk could not be
// unarchived, possibly because it's invalid.
//
// SQRLShipItStateErrorArchiving - The state object could not be archived.
//
// SQRLUpdaterErrorMissingUpdateBundle - The downloaded update does not contain
// an app bundle, or it was deleted on disk before we could get to it.
//
// SQRLUpdaterErrorPreparingUpdateJob - An error occurred in the out-of-process
// updater while it was setting up.
//
// SQRLUpdaterErrorRetrievingCodeSigningRequirement - The code signing
// requirement for the running application could not be retrieved.
//
// SQRLUpdaterErrorInvalidServerResponse - The server sent a response that we
// didn't understand. Includes `SQRLUpdaterServerDataErrorKey` in the error's
// `userInfo` dictionary.
//
// SQRLUpdaterErrorInvalidJSON - The server sent update JSON that we didn't
// understand. Includes `SQRLUpdaterJSONObjectErrorKey` in the error's
// `userInfo` dictionary.
//
// SQRLZipArchiverShellTaskFailed - `SQRLZipArchiver` tried to invoke the shell
// and failed. Includes `SQRLZipArchiverExitStatusErrorKey` in the error's
// `userInfo` dictionary.
//
// SQRLShipItLauncherErrorCouldNotStartService - The ShipIt service could not be
// started.
typedef enum : NSInteger {
SQRLCodeSignatureErrorDidNotPass = -1,
SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2,
SQRLShipItStateErrorMissingRequiredProperty = -100,
SQRLShipItStateErrorUnarchiving = -101,
SQRLShipItStateErrorArchiving = -102,
SQRLUpdaterErrorMissingUpdateBundle = -200,
SQRLUpdaterErrorPreparingUpdateJob = -201,
SQRLUpdaterErrorRetrievingCodeSigningRequirement = -202,
SQRLUpdaterErrorInvalidServerResponse = -203,
SQRLUpdaterErrorInvalidJSON = -204,
SQRLZipArchiverShellTaskFailed = -300,
SQRLShipItLauncherErrorCouldNotStartService = -400,
} SQRLErrorCode;
+11
View File
@@ -0,0 +1,11 @@
//
// Squirrel-Constants.m
// Squirrel
//
// Created by Keith Duncan on 30/10/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "Squirrel-Constants.h"
NSString * const SQRLErrorDomain = @"SQRLErrorDomain";
+11 -10
View File
@@ -7,6 +7,7 @@
//
#import "SQRLCodeSignature.h"
#import "Squirrel-Constants.h"
SpecBegin(SQRLCodeSignature)
@@ -54,7 +55,7 @@ describe(@"code signature changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -66,7 +67,7 @@ describe(@"code signature changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -80,7 +81,7 @@ describe(@"main executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorCouldNotCreateStaticCode);
});
@@ -92,7 +93,7 @@ describe(@"main executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -113,7 +114,7 @@ describe(@"helper executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -125,7 +126,7 @@ describe(@"helper executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -146,7 +147,7 @@ describe(@"resource changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -158,7 +159,7 @@ describe(@"resource changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -178,7 +179,7 @@ describe(@"framework changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -190,7 +191,7 @@ describe(@"framework changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
+1 -5
View File
@@ -60,16 +60,12 @@ 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];
@@ -110,7 +106,7 @@ it(@"should deep sign the test application", ^{
deepCodesignTestApplication();
});
xit(@"should deep verify after signing", ^{
it(@"should deep verify after signing", ^{
expect(deepVerify()).to.beFalsy();
deepCodesignTestApplication();
expect(deepVerify()).to.beTruthy();
+5 -30
View File
@@ -104,28 +104,6 @@ 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;
@@ -139,9 +117,6 @@ 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;
@@ -161,26 +136,26 @@ describe(@"signal handling", ^{
});
it(@"should handle SIGHUP", ^{
system("killall -HUP ShipIt");
system("killall -v -HUP ShipIt");
});
it(@"should handle SIGTERM", ^{
system("killall -TERM ShipIt");
system("killall -v -TERM ShipIt");
});
it(@"should handle SIGINT", ^{
system("killall -INT ShipIt");
system("killall -v -INT ShipIt");
});
it(@"should handle SIGQUIT", ^{
system("killall -QUIT ShipIt");
system("killall -v -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 -KILL ShipIt");
system("killall -v -KILL ShipIt");
// Wait at least for the launchd throttle interval.
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
+3 -6
View File
@@ -40,8 +40,7 @@ it(@"should wait until one instance terminates", ^{
expect(observedApp).will.equal(app);
expect(completed).to.beFalsy();
[app forceTerminate];
expect(app.terminated).will.beTruthy();
expect([app terminate]).to.beTruthy();
expect(completed).will.beTruthy();
});
@@ -56,12 +55,10 @@ it(@"should wait until multiple instances terminate", ^{
expect(completed).to.beFalsy();
[app1 forceTerminate];
expect(app1.terminated).will.beTruthy();
expect([app1 terminate]).to.beTruthy();
expect(completed).to.beFalsy();
[app2 forceTerminate];
expect(app2.terminated).will.beTruthy();
expect([app2 terminate]).to.beTruthy();
expect(completed).will.beTruthy();
});
-7
View File
@@ -88,11 +88,4 @@ 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
+14 -33
View File
@@ -51,10 +51,6 @@ 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:(RACSequence *)paths;
@end
@implementation SQRLTestCase
@@ -87,14 +83,15 @@ static void SQRLSignalHandler(int sig) {
[[NSData data] writeToURL:URL atomically:YES];
}
RACSequence *paths = [URLs map:^(NSURL *URL) {
return URL.path;
}];
NSArray *args = [[[URLs
map:^(NSURL *URL) {
return URL.path;
}]
startWith:@"-f"]
array];
NSTask *readShipIt = [self tailTaskWithPaths:paths];
[readShipIt launch];
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
NSTask *readShipIt = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:args];
NSAssert([readShipIt isRunning], @"Could not start task %@ with arguments: %@", readShipIt, args);
atexit_b(^{
[readShipIt terminate];
@@ -134,24 +131,6 @@ static void SQRLSignalHandler(int sig) {
[self.exampleCleanupBlocks addObject:[block copy]];
}
#pragma mark Logging
+ (NSTask *)tailTaskWithPaths:(RACSequence *)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 {
@@ -197,9 +176,7 @@ static void SQRLSignalHandler(int sig) {
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
[[NSData data] writeToURL:testAppLog atomically:YES];
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSequence return:testAppLog.path]];
[readTestApp launch];
NSTask *readTestApp = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:@[ @"-f", testAppLog.path ]];
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
[self addCleanupBlock:^{
@@ -324,7 +301,11 @@ static void SQRLSignalHandler(int sig) {
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
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);
}
NSError *lookupError = nil;
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
+71 -173
View File
@@ -6,11 +6,8 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDirectoryManager.h"
#import "SQRLZipArchiver.h"
#import "SQRLTestUpdate.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "SQRLZipArchiver.h"
SpecBegin(SQRLUpdater)
@@ -52,184 +49,85 @@ beforeEach(^{
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
});
describe(@"updating", ^{
__block NSURL *updateURL;
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
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];
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();
writeUpdate(update);
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_sequence.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 toArray];
expect(directoryURLs.count).to.equal(1);
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:directoryURLs[0] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:NULL];
expect(contents).to.equal(@[]);
});
});
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"response handling", ^{
__block NSURLRequest *localRequest = nil;
__block SQRLUpdater *updater = nil;
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();
before(^{
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"fake://host/path"]];
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
});
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
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];
}];
writeUpdate(update);
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
});
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
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];
}];
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
});
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);
});
SpecEnd
+2 -1
View File
@@ -8,6 +8,7 @@
#import "SQRLCodeSignature.h"
#import "SQRLZipArchiver.h"
#import "Squirrel-Constants.h"
SpecBegin(SQRLZipArchiver)
@@ -35,7 +36,7 @@ it(@"should fail to extract a nonexistent zip archive", ^{
NSLog(@"%@", error);
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLZipArchiverErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLZipArchiverShellTaskFailed);
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo.equal(0);
});
+6 -68
View File
@@ -1,66 +1,6 @@
# 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:
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:
```
$ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git
@@ -68,15 +8,13 @@ $ 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 merge in upstream changes later:
To bring in upstream changes later:
```
$ git fetch -p objc-build-scripts
$ git merge --ff --squash -Xsubtree=script objc-build-scripts/master
$ git merge -Xsubtree=script objc-build-scripts/master
```
[xctool]: https://github.com/facebook/xctool
+7 -69
View File
@@ -1,73 +1,11 @@
#!/bin/bash
export SCRIPT_DIR=$(dirname "$0")
SCRIPT_DIR=$(dirname "$0")
cd "$SCRIPT_DIR/.."
##
## Configuration Variables
##
set -o errexit
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
echo "*** Updating submodules..."
git submodule sync --quiet
git submodule update --init
git submodule foreach --recursive --quiet "git submodule sync --quiet && git submodule update --init"
+80 -113
View File
@@ -1,162 +1,129 @@
#!/bin/bash
export SCRIPT_DIR=$(dirname "$0")
SCRIPT_DIR=$(dirname "$0")
cd "$SCRIPT_DIR/.."
##
## Configuration Variables
##
SCHEMES="$@"
# The build configuration to use.
if [ -z "$XCCONFIGURATION" ]
then
XCCONFIGURATION="Test"
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")}
# 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
# 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 bootstrap script to run before building.
#
# If this file does not exist, it is not considered an error.
BOOTSTRAP="$SCRIPT_DIR/bootstrap"
# A bootstrap script to run before building.
#
# If this file does not exist, it is not considered an error.
: ${BOOTSTRAP="$SCRIPT_DIR/bootstrap"}
# 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
# 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
}
# Extra build settings to pass to xcodebuild.
XCODEBUILD_SETTINGS="TEST_AFTER_BUILD=YES"
##
## Code Signing
## Code-signing Setup
##
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
}
# 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
##
## Build Process
##
main ()
{
config
unlock_keychain
if [ -f "$BOOTSTRAP" ]
if [ -z "$*" ]
then
# lol recursive shell script
if [ -n "$DEFAULT_TARGETS" ]
then
echo "*** Bootstrapping..."
"$BOOTSTRAP" || exit $?
echo "$DEFAULT_TARGETS" | xargs "$SCRIPT_DIR/cibuild"
else
xcodebuild -list | awk -f "$SCRIPT_DIR/targets.awk" | xargs "$SCRIPT_DIR/cibuild"
fi
echo "*** The following schemes will be built:"
echo "$SCHEMES" | xargs -n 1 echo " "
echo
exit $?
fi
echo "$SCHEMES" | xargs -n 1 | (
local status=0
if [ -f "$BOOTSTRAP" ]
then
echo "*** Bootstrapping..."
bash "$BOOTSTRAP" || exit $?
fi
while read scheme
do
build_scheme "$scheme" || status=1
done
echo "*** The following targets will be built:"
exit $status
)
}
for target in "$@"
do
echo "$target"
done
find_pattern ()
echo "*** Cleaning all targets..."
xcodebuild -alltargets clean OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
run_xcodebuild ()
{
ls -d $1 2>/dev/null | head -n 1
}
local scheme=$1
run_xctool ()
{
if [ -n "$XCWORKSPACE" ]
then
xctool -workspace "$XCWORKSPACE" $XCTOOL_OPTIONS "$@" 2>&1
elif [ -n "$XCODEPROJ" ]
then
xctool -project "$XCODEPROJ" $XCTOOL_OPTIONS "$@" 2>&1
xcodebuild -workspace "$XCWORKSPACE" -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
else
echo "*** No workspace or project file found."
exit 1
xcodebuild -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
fi
}
parse_build ()
{
awk -f "$SCRIPT_DIR/xctool.awk" 2>&1 >/dev/null
local status=$?
return $status
}
build_scheme ()
{
local scheme=$1
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
run_xcodebuild "$scheme" 2>&1 | awk -f "$SCRIPT_DIR/xcodebuild.awk"
local awkstatus=$?
local xcstatus=${PIPESTATUS[0]}
if [ "$awkstatus" -ne "0" ]
if [ "$xcstatus" -eq "65" ]
then
# Unit tests aren't supported.
action=build
# 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
fi
if [ "$awkstatus" -eq "1" ]
then
# Build for iOS.
sdkflag="-sdk iphonesimulator"
fi
run_xctool $sdkflag -scheme "$scheme" $action
return $xcstatus
}
export -f build_scheme
export -f run_xctool
export -f parse_build
echo "*** Building..."
main
for scheme in "$@"
do
build_scheme "$scheme" || exit $?
done
-12
View File
@@ -1,12 +0,0 @@
BEGIN {
FS = "\n";
}
/Targets:/ {
while (getline && $0 != "") {
if ($0 ~ /Test/) continue;
sub(/^ +/, "");
print "'" $0 "'";
}
}
-25
View File
@@ -1,25 +0,0 @@
# 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;
}