Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c89431c4a | ||
|
|
2e33ff84f9 | ||
|
|
c7915326d9 | ||
|
|
e10877e30e | ||
|
|
92f98ac82c | ||
|
|
a079880b18 | ||
|
|
2b5b52a551 | ||
|
|
ce0d740bc5 | ||
|
|
0939c6d331 | ||
|
|
d8d5021d7f | ||
|
|
6e6fc6b135 | ||
|
|
be9e933570 | ||
|
|
36e0531218 | ||
|
|
77aec66456 | ||
|
|
e8523fd74d | ||
|
|
767892c788 | ||
|
|
34e37569d7 | ||
|
|
4c93cb4508 | ||
|
|
431224c788 | ||
|
|
ea8f805825 | ||
|
|
7bb4c131e1 | ||
|
|
372a7d783a | ||
|
|
b85e03fbe9 | ||
|
|
48455f1ee4 | ||
|
|
5cd5fdb2ed | ||
|
|
aa545ed419 | ||
|
|
3d507a58a6 | ||
|
|
406728c066 | ||
|
|
53adc17554 | ||
|
|
0fb9ed48c9 | ||
|
|
1db648059a | ||
|
|
fccf29122d | ||
|
|
502c96a6df | ||
|
|
90a6a2fe16 | ||
|
|
c2869a05e9 | ||
|
|
aa69586334 | ||
|
|
8f3f5a5a72 | ||
|
|
f48ccd2a21 | ||
|
|
e21fd640b9 | ||
|
|
7bb7799b17 | ||
|
|
13a35d195b | ||
|
|
0c95e0c8c4 | ||
|
|
e5c65e530f | ||
|
|
65f191ce30 | ||
|
|
2388fa3475 | ||
|
|
99bb3e042f | ||
|
|
5b930bcdb8 | ||
|
|
7aace7204d | ||
|
|
c42950ccc4 | ||
|
|
efbb838e09 | ||
|
|
249481c842 | ||
|
|
4062563d09 | ||
|
|
55ca2cd87b | ||
|
|
6159fe07d5 | ||
|
|
2fed661399 | ||
|
|
1a9c430aca | ||
|
|
2220774e01 | ||
|
|
77f5c72415 | ||
|
|
3bd1e41270 | ||
|
|
9b92542b9c | ||
|
|
56b08482f4 | ||
|
|
c96467f35d | ||
|
|
707678e926 | ||
|
|
c50efc62a1 | ||
|
|
e4b7973685 | ||
|
|
e4c1a33251 | ||
|
|
7ba896fdde | ||
|
|
9fa097bec6 | ||
|
|
9f8cbe17c3 | ||
|
|
6f3e933d67 | ||
|
|
516ab2af65 | ||
|
|
4d385f4a33 | ||
|
|
d5456f90f2 | ||
|
|
72d768d17d | ||
|
|
91549567e1 | ||
|
|
6e9efa0be3 | ||
|
|
8709338949 | ||
|
|
f9d6c00f60 | ||
|
|
e122927c5d | ||
|
|
43d524e186 | ||
|
|
a4dc3e4a47 | ||
|
|
e5f838463b | ||
|
|
e24d538232 | ||
|
|
9170c4d106 | ||
|
|
a93cf336b8 | ||
|
|
7bc6620b45 | ||
|
|
4f0ba2c41a | ||
|
|
cfaabf7cb0 | ||
|
|
d4cd9fa88c | ||
|
|
a7a3ddbf9c |
+4
-1
@@ -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
+1
-1
Submodule External/Mantle updated: 6cbdbebc44...8aef509d18
+1
Submodule External/OHHTTPStubs added at acaf8b8af7
Vendored
+1
-1
Submodule External/ReactiveCocoa updated: c01fe91544...09e17b9b9b
@@ -13,21 +13,46 @@ 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) below.
|
||||
on server side support for determining whether an update is required, see
|
||||
[Server Support](#server-support).
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
# 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.
|
||||
|
||||
@@ -35,37 +60,89 @@ Once Squirrel is added to your project, you need to configure and start it.
|
||||
#import <Squirrel/Squirrel.h>
|
||||
|
||||
- (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];
|
||||
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];
|
||||
}
|
||||
```
|
||||
|
||||
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 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`:
|
||||
|
||||
```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);
|
||||
}
|
||||
|
||||
[sender replyToApplicationShouldTerminate:YES];
|
||||
}];
|
||||
|
||||
return NSTerminateLater;
|
||||
}
|
||||
[self.updater.updates subscribeNext:^(SQRLDownloadedUpdate *downloadedUpdate) {
|
||||
NSLog(@"An update is ready to install: %@", downloadedUpdate);
|
||||
}];
|
||||
```
|
||||
|
||||
# Update JSON Format
|
||||
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.
|
||||
|
||||
Squirrel requests the URL you provide with `Accept: application/json` and
|
||||
expects the following schema in response:
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -83,24 +160,10 @@ 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
|
||||
|
||||
# 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.
|
||||
"pub_date" if present must be formatted according to ISO 8601.
|
||||
|
||||
# User Interface
|
||||
|
||||
Squirrel does not provide an updates interface, if you want to display available
|
||||
updates, subscribe to the `SQRLUpdaterUpdateAvailableNotification` notification.
|
||||
|
||||

|
||||
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).
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -211,7 +211,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);
|
||||
@@ -398,27 +397,6 @@ 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 {
|
||||
|
||||
@@ -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) {
|
||||
createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
|
||||
AuthorizationItem rightItems[] = {
|
||||
{
|
||||
.name = kSMRightModifySystemDaemons,
|
||||
@@ -108,16 +109,15 @@ 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);
|
||||
}];
|
||||
return nil;
|
||||
}]
|
||||
setNameWithFormat:@"+shipItAuthorization"];
|
||||
}
|
||||
@@ -127,22 +127,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)];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+16
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+20
-5
@@ -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`.
|
||||
@@ -57,11 +62,6 @@ 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,6 +99,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 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)
|
||||
|
||||
+178
-41
@@ -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,6 +124,16 @@ 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
|
||||
@@ -139,11 +164,46 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
|
||||
|
||||
NSError *error = nil;
|
||||
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
|
||||
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", 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
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -152,20 +212,42 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
|
||||
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
||||
|
||||
return [[[[[[NSURLConnection
|
||||
rac_sendAsynchronousRequest:request]
|
||||
reduceEach:^(id _, NSData *data) {
|
||||
return data;
|
||||
// Prune old updates before the first update check.
|
||||
return [[[[[[[self.prunedUpdateDirectories
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error pruning old updates: %@", error);
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
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];
|
||||
}];
|
||||
|
||||
@@ -222,7 +304,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 +360,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,7 +394,14 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
|
||||
NSLog(@"Download completed to: %@", zipOutputURL);
|
||||
}]
|
||||
flattenMap:^(NSURL *zipOutputURL) {
|
||||
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL: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);
|
||||
}
|
||||
}];
|
||||
}]
|
||||
then:^{
|
||||
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
|
||||
@@ -315,7 +418,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);
|
||||
@@ -384,6 +487,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 {
|
||||
@@ -404,35 +516,37 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
|
||||
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];
|
||||
}];
|
||||
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];
|
||||
}]
|
||||
then:^{
|
||||
return self.shipItLauncher;
|
||||
@@ -441,4 +555,27 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
|
||||
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
|
||||
|
||||
+28
-2
@@ -76,8 +76,10 @@ 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
|
||||
action = [[[installer.abortInstallationCommand
|
||||
execute:state]
|
||||
initially:^{
|
||||
NSLog(@"Too many attempts to install from state %i, aborting update", (int)state.installerState);
|
||||
@@ -89,7 +91,7 @@ int main(int argc, const char * argv[]) {
|
||||
return [RACSignal empty];
|
||||
}];
|
||||
} else {
|
||||
return [[[[[state
|
||||
action = [[[[[state
|
||||
writeUsingURL:stateLocation]
|
||||
initially:^{
|
||||
if (freshInstall) {
|
||||
@@ -109,6 +111,30 @@ 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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:(RACSequence *)paths;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLTestCase
|
||||
@@ -83,15 +87,14 @@ static void SQRLSignalHandler(int sig) {
|
||||
[[NSData data] writeToURL:URL atomically:YES];
|
||||
}
|
||||
|
||||
NSArray *args = [[[URLs
|
||||
map:^(NSURL *URL) {
|
||||
return URL.path;
|
||||
}]
|
||||
startWith:@"-f"]
|
||||
array];
|
||||
RACSequence *paths = [URLs map:^(NSURL *URL) {
|
||||
return URL.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:paths];
|
||||
[readShipIt launch];
|
||||
|
||||
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
|
||||
|
||||
atexit_b(^{
|
||||
[readShipIt terminate];
|
||||
@@ -131,6 +134,24 @@ 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 {
|
||||
@@ -176,7 +197,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:[RACSequence return:testAppLog.path]];
|
||||
[readTestApp launch];
|
||||
|
||||
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
@@ -301,11 +324,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
@@ -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_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(@[]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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.checkForUpdatesCommand execute: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.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
|
||||
expect(result).to.beFalsy();
|
||||
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
|
||||
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
|
||||
+68
-6
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
BEGIN {
|
||||
FS = "\n";
|
||||
}
|
||||
|
||||
/Targets:/ {
|
||||
while (getline && $0 != "") {
|
||||
if ($0 ~ /Test/) continue;
|
||||
|
||||
sub(/^ +/, "");
|
||||
print "'" $0 "'";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user