Skip to content

Commit 3a151a4

Browse files
committedFeb 13, 2017
Implementing binary origin
1 parent 20447f2 commit 3a151a4

20 files changed

+922
-129
lines changed
 

‎Carthage.xcodeproj/project.pbxproj

+28
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
BEA86FA01C9F91500049360B /* Tentacle.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = BEB076651C8A1FD800ABD373 /* Tentacle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3232
BEB076661C8A1FD800ABD373 /* Curry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEB076641C8A1FD800ABD373 /* Curry.framework */; };
3333
BEB076671C8A1FD800ABD373 /* Tentacle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEB076651C8A1FD800ABD373 /* Tentacle.framework */; };
34+
BF3199C01E32E078007DC0D1 /* ProjectIdentifierSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3199BF1E32E078007DC0D1 /* ProjectIdentifierSpec.swift */; };
35+
BF6E5DA91E41860700C63D39 /* successful.json in Resources */ = {isa = PBXBuildFile; fileRef = BF6E5DA81E41860700C63D39 /* successful.json */; };
36+
BF6E5DAB1E41863500C63D39 /* invalid.json in Resources */ = {isa = PBXBuildFile; fileRef = BF6E5DAA1E41863500C63D39 /* invalid.json */; };
37+
BF7A1A0B1E3A7AE9008CBCC5 /* BinaryProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A1A091E3A7188008CBCC5 /* BinaryProject.swift */; };
38+
BFFA7A631E3AAFD200CB95A7 /* BinaryProjectSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA7A621E3AAFD200CB95A7 /* BinaryProjectSpec.swift */; };
3439
CD07EB111E0EA5B200CFBEE4 /* ProjectLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD07EB101E0EA5B200CFBEE4 /* ProjectLocator.swift */; };
3540
CD07EB131E0EA90F00CFBEE4 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD07EB121E0EA90F00CFBEE4 /* Platform.swift */; };
3641
CD07EB151E0F4DEA00CFBEE4 /* ProjectLocatorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD07EB141E0F4DEA00CFBEE4 /* ProjectLocatorSpec.swift */; };
@@ -193,6 +198,11 @@
193198
BE624F471E1341E900EAEFC9 /* DuplicateDependenciesCartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = DuplicateDependenciesCartfile; sourceTree = "<group>"; };
194199
BEB076641C8A1FD800ABD373 /* Curry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Curry.framework; sourceTree = BUILT_PRODUCTS_DIR; };
195200
BEB076651C8A1FD800ABD373 /* Tentacle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Tentacle.framework; sourceTree = BUILT_PRODUCTS_DIR; };
201+
BF3199BF1E32E078007DC0D1 /* ProjectIdentifierSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectIdentifierSpec.swift; sourceTree = "<group>"; };
202+
BF6E5DA81E41860700C63D39 /* successful.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = successful.json; sourceTree = "<group>"; };
203+
BF6E5DAA1E41863500C63D39 /* invalid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = invalid.json; sourceTree = "<group>"; };
204+
BF7A1A091E3A7188008CBCC5 /* BinaryProject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinaryProject.swift; sourceTree = "<group>"; };
205+
BFFA7A621E3AAFD200CB95A7 /* BinaryProjectSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinaryProjectSpec.swift; sourceTree = "<group>"; };
196206
CD07EB101E0EA5B200CFBEE4 /* ProjectLocator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectLocator.swift; sourceTree = "<group>"; };
197207
CD07EB121E0EA90F00CFBEE4 /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = "<group>"; };
198208
CD07EB141E0F4DEA00CFBEE4 /* ProjectLocatorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectLocatorSpec.swift; sourceTree = "<group>"; };
@@ -337,6 +347,15 @@
337347
path = Source/Scripts;
338348
sourceTree = "<group>";
339349
};
350+
BF6E5DA71E4185DA00C63D39 /* BinaryOnly */ = {
351+
isa = PBXGroup;
352+
children = (
353+
BF6E5DA81E41860700C63D39 /* successful.json */,
354+
BF6E5DAA1E41863500C63D39 /* invalid.json */,
355+
);
356+
path = BinaryOnly;
357+
sourceTree = "<group>";
358+
};
340359
D0D1210F19E87861005E4BAA = {
341360
isa = PBXGroup;
342361
children = (
@@ -458,6 +477,7 @@
458477
3A0472F21C782B4000097EC7 /* Algorithms.swift */,
459478
D069CA231A4E3B2700314A85 /* Archive.swift */,
460479
CD3E530A1DE33095002C135C /* Availability.swift */,
480+
BF7A1A091E3A7188008CBCC5 /* BinaryProject.swift */,
461481
89E80E601C754FFD000F8DCB /* BuildArguments.swift */,
462482
CDF9D3701CF1E54200DF5A6F /* BuildOptions.swift */,
463483
CDE559281E12263A00ED7F5F /* BuildSettings.swift */,
@@ -506,10 +526,12 @@
506526
children = (
507527
3A0472F41C782D1D00097EC7 /* AlgorithmsSpec.swift */,
508528
D0C6E5731A57040B00A5E3E7 /* ArchiveSpec.swift */,
529+
BFFA7A621E3AAFD200CB95A7 /* BinaryProjectSpec.swift */,
509530
89E80E631C755059000F8DCB /* BuildArgumentsSpec.swift */,
510531
D0D121DF19E8999E005E4BAA /* CartfileSpec.swift */,
511532
89A8F1771C24AB3C00C0E75A /* FrameworkExtensionsSpec.swift */,
512533
CDA0B6C91C468E67006C499C /* GitSpec.swift */,
534+
BF3199BF1E32E078007DC0D1 /* ProjectIdentifierSpec.swift */,
513535
CD07EB141E0F4DEA00CFBEE4 /* ProjectLocatorSpec.swift */,
514536
549B47AF1A4F17FF002498C7 /* ProjectSpec.swift */,
515537
D01D82DC1A10B01D00F0DD94 /* ResolverSpec.swift */,
@@ -524,6 +546,7 @@
524546
D0D1217C19E87B05005E4BAA /* Supporting Files */ = {
525547
isa = PBXGroup;
526548
children = (
549+
BF6E5DA71E4185DA00C63D39 /* BinaryOnly */,
527550
14A7BF461AA810A300C70ABB /* DuplicateDependencies */,
528551
D0C6E5751A57042100A5E3E7 /* CartfilePrivateOnly.zip */,
529552
D01F8A4B19EA28FE00643E7C /* Nimble.framework */,
@@ -674,7 +697,9 @@
674697
BE624F491E13424400EAEFC9 /* DuplicateDependenciesCartfile in Resources */,
675698
D0AAAB5519FB1062007B24B3 /* TestCartfile in Resources */,
676699
D0F551DC1A0D71AB0093311F /* TestCartfile.resolved in Resources */,
700+
BF6E5DA91E41860700C63D39 /* successful.json in Resources */,
677701
D0C6E5761A57042100A5E3E7 /* CartfilePrivateOnly.zip in Resources */,
702+
BF6E5DAB1E41863500C63D39 /* invalid.json in Resources */,
678703
CDCE1CC41C170E8A00B2ED88 /* TestResolvedCartfile.resolved in Resources */,
679704
);
680705
runOnlyForDeploymentPostprocessing = 0;
@@ -759,6 +784,7 @@
759784
files = (
760785
D0AAAB4A19FAEDB4007B24B3 /* Errors.swift in Sources */,
761786
D0DE89441A0F2D9B0030A3EC /* Scannable.swift in Sources */,
787+
BF7A1A0B1E3A7AE9008CBCC5 /* BinaryProject.swift in Sources */,
762788
CD2482671E1A05F50001EFE2 /* MachOType.swift in Sources */,
763789
D01D82D71A10160700F0DD94 /* Resolver.swift in Sources */,
764790
CD07EB131E0EA90F00CFBEE4 /* Platform.swift in Sources */,
@@ -792,10 +818,12 @@
792818
89A8F1781C24AB3C00C0E75A /* FrameworkExtensionsSpec.swift in Sources */,
793819
D0DB09A419EA354200234B16 /* XcodeSpec.swift in Sources */,
794820
3A0472F61C7836EA00097EC7 /* AlgorithmsSpec.swift in Sources */,
821+
BFFA7A631E3AAFD200CB95A7 /* BinaryProjectSpec.swift in Sources */,
795822
CD07EB151E0F4DEA00CFBEE4 /* ProjectLocatorSpec.swift in Sources */,
796823
D0C6E5741A57040B00A5E3E7 /* ArchiveSpec.swift in Sources */,
797824
549B47B11A4F1A34002498C7 /* ProjectSpec.swift in Sources */,
798825
D01D82DD1A10B01D00F0DD94 /* ResolverSpec.swift in Sources */,
826+
BF3199C01E32E078007DC0D1 /* ProjectIdentifierSpec.swift in Sources */,
799827
CDA0B6CA1C468E67006C499C /* GitSpec.swift in Sources */,
800828
89E80E641C755059000F8DCB /* BuildArgumentsSpec.swift in Sources */,
801829
CD1FE6581DD85A140016639E /* Swift3Shims.swift in Sources */,

‎Documentation/Artifacts.md

+40-5
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,33 @@ Dependency specifications consist of two main parts: the [origin](#origin), and
1010

1111
#### Origin
1212

13-
The only supported origins right now are GitHub repositories (both GitHub.com and GitHub Enterprise), specified with the `github` keyword:
13+
The three supported origins right now are GitHub repositories, Git repositories, and binary-only frameworks served over `https`. Other possible origins may be added in the future. If there’s something specific you’d like to see, please [file an issue](https://github.com/Carthage/Carthage/issues/new).
14+
15+
##### GitHub Repositories
16+
17+
GitHub repositories (both GitHub.com and GitHub Enterprise) are specified with the `github` keyword:
1418

1519
```
1620
github "ReactiveCocoa/ReactiveCocoa" # GitHub.com
1721
github "https://enterprise.local/ghe/desktop/git-error-translations" # GitHub Enterprise
1822
```
1923

20-
… or other Git repositories, specified with the `git` keyword:
24+
##### Git repositories
25+
26+
Other Git repositories are specified with the `git` keyword:
2127

2228
```
2329
git "https://enterprise.local/desktop/git-error-translations2.git"
2430
```
2531

26-
Other possible origins may be added in the future. If there’s something specific you’d like to see, please [file an issue](https://github.com/Carthage/Carthage/issues/new).
32+
##### Binary only frameworks
33+
34+
Dependencies that are only available as compiled binary `.framework`s are specified with the `binary` keyword and an https address that returns a binary project specification:
35+
36+
```
37+
binary "https://my.domain.com/release/MyFramework.json"
38+
```
39+
2740

2841
#### Version requirement
2942

@@ -32,15 +45,15 @@ Carthage supports several kinds of version requirements:
3245
1. `>= 1.0` for “at least version 1.0”
3346
1. `~> 1.0` for “compatible with version 1.0”
3447
1. `== 1.0` for “exactly version 1.0”
35-
1. `"some-branch-or-tag-or-commit"` for a specific Git object (anything allowed by `git rev-parse`)
48+
1. `"some-branch-or-tag-or-commit"` for a specific Git object (anything allowed by `git rev-parse`). **Note**: This form of requirement is _not_ supported for `binary` origins.
3649

3750
If no version requirement is given, any version of the dependency is allowed.
3851

3952
Compatibility is determined according to [Semantic Versioning](http://semver.org/). This means that any version greater than or equal to 1.5.1, but less than 2.0, will be considered “compatible” with 1.5.1.
4053

4154
According to SemVer, any 0.x.y release may completely break the exported API, so it's not safe to consider them compatible with one another. Only patch versions are compatible under 0.x, meaning 0.1.1 is compatible with 0.1.2, but not 0.2. This isn't according to the SemVer spec but keeps `~>` useful for 0.x.y versions.
4255

43-
**In all cases, Carthage will pin to a tag or SHA**, and only bump the tag or SHA when `carthage update` is run again in the future. This means that following a branch (for example) still results in commits that can be independently checked out just as they were originally.
56+
**In all cases, Carthage will pin to a tag or SHA (for `git` and `github` origins) or a semantic version (for `binary` origins)**, and only bump those values when `carthage update` is run again in the future. This means that following a branch (for example) still results in commits that can be independently checked out just as they were originally.
4457

4558
#### Example Cartfile
4659

@@ -68,6 +81,9 @@ git "https://enterprise.local/desktop/git-error-translations2.git" "development"
6881
6982
# Use a local project
7083
git "file:///directory/to/project" "branch"
84+
85+
# A binary only framework
86+
binary "https://my.domain.com/release/MyFramework.json" ~> 2.3
7187
```
7288

7389
## Cartfile.private
@@ -107,3 +123,22 @@ If the `--use-submodules` flag was given when a project’s dependencies were bo
107123
This folder is created automatically by Carthage, and contains the “bare” Git repositories used for fetching and checking out dependencies, as well as prebuilt binaries that have been downloaded. Keeping all repositories in this centralized location avoids polluting individual projects with Git metadata, and allows Carthage to share one copy of each repository across all projects.
108124

109125
If you need to reclaim disk space, you can safely delete this folder, or any of the individual folders inside. The folder will be automatically repopulated the next time `carthage checkout` is run.
126+
127+
## Binary Project Specification
128+
129+
For depenencies that do not have source code available, a binary project specification can be used to list the locations and versions of compiled frameworks. This data **must** be available via `https` and could be served from a static file or dynamically.
130+
131+
* The JSON structure is a top-level dictionary with the key-value pairs of version / location.
132+
* The version **must** be a semantic version. Git branches, tags and commits are not valid.
133+
* The location **must** be an `https` url.
134+
135+
#### Example binary project specification
136+
137+
```
138+
{
139+
"1.0": "https://my.domain.com/release/1.0.0/framework.zip",
140+
"1.0.1": "https://my.domain.com/release/1.0.1/framework.zip"
141+
}
142+
143+
```
144+

‎README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Once you have Carthage [installed](#installing-carthage), you can begin adding f
4747
##### If you're building for OS X
4848

4949
1. Create a [Cartfile][] that lists the frameworks you’d like to use in your project.
50-
1. Run `carthage update`. This will fetch dependencies into a [Carthage/Checkouts][] folder and build each one.
50+
1. Run `carthage update`. This will fetch dependencies into a [Carthage/Checkouts][] folder and build each one or download a pre-compiled framework.
5151
1. On your application targets’ “General” settings tab, in the “Embedded Binaries” section, drag and drop each framework you want to use from the [Carthage/Build][] folder on disk.
5252

5353
Additionally, you'll need to copy debug symbols for debugging and crash reporting on OS X.
@@ -59,7 +59,7 @@ Additionally, you'll need to copy debug symbols for debugging and crash reportin
5959
##### If you're building for iOS, tvOS, or watchOS
6060

6161
1. Create a [Cartfile][] that lists the frameworks you’d like to use in your project.
62-
1. Run `carthage update`. This will fetch dependencies into a [Carthage/Checkouts][] folder, then build each one.
62+
1. Run `carthage update`. This will fetch dependencies into a [Carthage/Checkouts][] folder, then build each one or download a pre-compiled framework.
6363
1. On your application targets’ “General” settings tab, in the “Linked Frameworks and Libraries” section, drag and drop each framework you want to use from the [Carthage/Build][] folder on disk.
6464
1. On your application targets’ “Build Phases” settings tab, click the “+” icon and choose “New Run Script Phase”. Create a Run Script in which you specify your shell (ex: `bin/sh`), add the following contents to the script area below the shell:
6565

@@ -171,7 +171,7 @@ Tags without any version number, or with any characters following the version nu
171171

172172
### Archive prebuilt frameworks into one zip file
173173

174-
Carthage can automatically use prebuilt frameworks, instead of building from scratch, if they are attached to a [GitHub Release](https://help.github.com/articles/about-releases/) on your project’s repository.
174+
Carthage can automatically use prebuilt frameworks, instead of building from scratch, if they are attached to a [GitHub Release](https://help.github.com/articles/about-releases/) on your project’s repository or via a binary project definition file.
175175

176176
To offer prebuilt frameworks for a specific tag, the binaries for _all_ supported platforms should be zipped up together into _one_ archive, and that archive should be attached to a published Release corresponding to that tag. The attachment should include `.framework` in its name (e.g., `ReactiveCocoa.framework.zip`), to indicate to Carthage that it contains binaries.
177177

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Result
2+
3+
public struct BinaryProject {
4+
5+
public var versions: [PinnedVersion: URL]
6+
7+
public static func from(jsonData: Data, url: URL) -> Result<BinaryProject, BinaryJSONError> {
8+
9+
return Result<Any, NSError>(attempt: { try JSONSerialization.jsonObject(with: jsonData, options: [])})
10+
.mapError(BinaryJSONError.invalidJSON)
11+
.flatMap { json in
12+
let error = NSError(domain: CarthageKitBundleIdentifier,
13+
code: 1,
14+
userInfo: [NSLocalizedDescriptionKey: "Binary definition was not expected type [String: String]"])
15+
return Result(json as? [String: String], failWith: BinaryJSONError.invalidJSON(error))
16+
}
17+
.flatMap { (json: [String: String]) -> Result<BinaryProject, BinaryJSONError> in
18+
19+
var versions = [PinnedVersion: URL]()
20+
21+
for (key, value) in json {
22+
let pinnedVersion: PinnedVersion
23+
switch SemanticVersion.from(Scanner(string: key)) {
24+
case .success:
25+
pinnedVersion = PinnedVersion(key)
26+
case let .failure(error):
27+
return .failure(BinaryJSONError.invalidVersion(error))
28+
}
29+
30+
guard let binaryURL = URL(string: value) else {
31+
return .failure(BinaryJSONError.invalidURL(value))
32+
}
33+
34+
guard binaryURL.scheme == "https" else {
35+
return .failure(BinaryJSONError.nonHTTPSURL(binaryURL))
36+
}
37+
38+
versions[pinnedVersion] = binaryURL
39+
}
40+
41+
return .success(BinaryProject(versions: versions))
42+
}
43+
44+
}
45+
}
46+
47+
extension BinaryProject: Equatable {
48+
public static func ==(lhs: BinaryProject, rhs: BinaryProject) -> Bool {
49+
return lhs.versions == rhs.versions
50+
}
51+
}
52+

‎Source/CarthageKit/Cartfile.swift

+63-9
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,17 @@ public struct Cartfile {
5151

5252
switch Dependency<VersionSpecifier>.from(scanner) {
5353
case let .success(dep):
54+
if case .binary = dep.project, case .gitReference = dep.version {
55+
result = .failure(CarthageError.parseError(description: "binary dependencies cannot have a git reference for the version specifier in line: \(scanner.currentLine)"))
56+
stop = true
57+
return
58+
}
5459
dependencies.append(dep)
5560

5661
case let .failure(error):
57-
result = .failure(error)
62+
result = .failure(CarthageError(scannableError: error))
5863
stop = true
64+
return
5965
}
6066

6167
if scanner.scanString(commentIndicator, into: nil) {
@@ -164,7 +170,7 @@ public struct ResolvedCartfile {
164170
cartfile.dependencies.insert(dep)
165171

166172
case let .failure(error):
167-
result = .failure(error)
173+
result = .failure(CarthageError(scannableError: error))
168174
break scannerLoop
169175
}
170176
}
@@ -190,6 +196,9 @@ public enum ProjectIdentifier: Comparable {
190196
/// An arbitrary Git repository.
191197
case git(GitURL)
192198

199+
/// A binary-only framework
200+
case binary(URL)
201+
193202
/// The unique, user-visible name for this project.
194203
public var name: String {
195204
switch self {
@@ -198,6 +207,9 @@ public enum ProjectIdentifier: Comparable {
198207

199208
case let .git(url):
200209
return url.name ?? url.urlString
210+
211+
case let .binary(url):
212+
return url.lastPathComponent.stripping(suffix: ".json")
201213
}
202214
}
203215

@@ -216,6 +228,9 @@ public func ==(_ lhs: ProjectIdentifier, _ rhs: ProjectIdentifier) -> Bool {
216228
case let (.git(left), .git(right)):
217229
return left == right
218230

231+
case let (.binary(left), .binary(right)):
232+
return left == right
233+
219234
default:
220235
return false
221236
}
@@ -233,14 +248,17 @@ extension ProjectIdentifier: Hashable {
233248

234249
case let .git(url):
235250
return url.hashValue
251+
252+
case let .binary(url):
253+
return url.hashValue
236254
}
237255
}
238256
}
239257

240258
extension ProjectIdentifier: Scannable {
241259
/// Attempts to parse a ProjectIdentifier.
242-
public static func from(_ scanner: Scanner) -> Result<ProjectIdentifier, CarthageError> {
243-
let parser: (String) -> Result<ProjectIdentifier, CarthageError>
260+
public static func from(_ scanner: Scanner) -> Result<ProjectIdentifier, ScannableError> {
261+
let parser: (String) -> Result<ProjectIdentifier, ScannableError>
244262

245263
if scanner.scanString("github", into: nil) {
246264
parser = { repoIdentifier in
@@ -250,23 +268,35 @@ extension ProjectIdentifier: Scannable {
250268
parser = { urlString in
251269
return .success(self.git(GitURL(urlString)))
252270
}
271+
} else if scanner.scanString("binary", into: nil) {
272+
parser = { urlString in
273+
if let url = URL(string: urlString) {
274+
if url.scheme == "https" {
275+
return .success(self.binary(url))
276+
} else {
277+
return .failure(ScannableError(message: "non-https URL found for dependency type `binary`", currentLine: scanner.currentLine))
278+
}
279+
} else {
280+
return .failure(ScannableError(message: "invalid URL found for dependency type `binary`", currentLine: scanner.currentLine))
281+
}
282+
}
253283
} else {
254-
return .failure(CarthageError.parseError(description: "unexpected dependency type in line: \(scanner.currentLine)"))
284+
return .failure(ScannableError(message: "unexpected dependency type", currentLine: scanner.currentLine))
255285
}
256286

257287
if !scanner.scanString("\"", into: nil) {
258-
return .failure(CarthageError.parseError(description: "expected string after dependency type in line: \(scanner.currentLine)"))
288+
return .failure(ScannableError(message: "expected string after dependency type", currentLine: scanner.currentLine))
259289
}
260290

261291
var address: NSString? = nil
262292
if !scanner.scanUpTo("\"", into: &address) || !scanner.scanString("\"", into: nil) {
263-
return .failure(CarthageError.parseError(description: "empty or unterminated string after dependency type in line: \(scanner.currentLine)"))
293+
return .failure(ScannableError(message: "empty or unterminated string after dependency type", currentLine: scanner.currentLine))
264294
}
265295

266296
if let address = address {
267297
return parser(address as String)
268298
} else {
269-
return .failure(CarthageError.parseError(description: "empty string after dependency type in line: \(scanner.currentLine)"))
299+
return .failure(ScannableError(message: "empty string after dependency type", currentLine: scanner.currentLine))
270300
}
271301
}
272302
}
@@ -287,10 +317,34 @@ extension ProjectIdentifier: CustomStringConvertible {
287317

288318
case let .git(url):
289319
return "git \"\(url)\""
320+
321+
case let .binary(url):
322+
return "binary \"\(url.absoluteString)\""
290323
}
291324
}
292325
}
293326

327+
extension ProjectIdentifier {
328+
329+
/// Returns the URL that the project's remote repository exists at.
330+
func gitURL(preferHTTPS: Bool) -> GitURL? {
331+
switch self {
332+
case let .gitHub(repository):
333+
if preferHTTPS {
334+
return repository.httpsURL
335+
} else {
336+
return repository.sshURL
337+
}
338+
339+
case let .git(url):
340+
return url
341+
case .binary:
342+
return nil
343+
}
344+
}
345+
346+
}
347+
294348
/// Represents a single dependency of a project.
295349
public struct Dependency<V: VersionType>: Hashable {
296350
/// The project corresponding to this dependency.
@@ -315,7 +369,7 @@ public func ==<V>(_ lhs: Dependency<V>, _ rhs: Dependency<V>) -> Bool {
315369

316370
extension Dependency where V: Scannable {
317371
/// Attempts to parse a Dependency specification.
318-
public static func from(_ scanner: Scanner) -> Result<Dependency, CarthageError> {
372+
public static func from(_ scanner: Scanner) -> Result<Dependency, ScannableError> {
319373
return ProjectIdentifier.from(scanner).flatMap { identifier in
320374
return V.from(scanner).map { specifier in self.init(project: identifier, version: specifier) }
321375
}

‎Source/CarthageKit/Errors.swift

+97-6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public enum CarthageError: Error, Equatable {
4949
/// An error occurred parsing a Carthage file.
5050
case parseError(description: String)
5151

52+
/// An error occurred parsing the binary-only framework definition file
53+
case invalidBinaryJSON(URL, BinaryJSONError)
54+
5255
// An expected environment variable wasn't found.
5356
case missingEnvironmentVariable(variable: String)
5457

@@ -80,13 +83,19 @@ public enum CarthageError: Error, Equatable {
8083
case gitHubAPIRequestFailed(Client.Error)
8184

8285
case gitHubAPITimeout
83-
86+
8487
case buildFailed(TaskError, log: URL?)
8588

8689
/// An error occurred while shelling out.
8790
case taskError(TaskError)
8891
}
8992

93+
public extension CarthageError {
94+
init(scannableError: ScannableError) {
95+
self = .parseError(description: "\(scannableError)")
96+
}
97+
}
98+
9099
private func == (_ lhs: CarthageError.VersionRequirement, _ rhs: CarthageError.VersionRequirement) -> Bool {
91100
return lhs.specifier == rhs.specifier && lhs.fromProject == rhs.fromProject
92101
}
@@ -120,7 +129,10 @@ public func == (_ lhs: CarthageError, _ rhs: CarthageError) -> Bool {
120129

121130
case let (.parseError(left), .parseError(right)):
122131
return left == right
123-
132+
133+
case let (.invalidBinaryJSON(leftUrl, leftError), .invalidBinaryJSON(rightUrl, rightError)):
134+
return leftUrl == rightUrl && leftError == rightError
135+
124136
case let (.missingEnvironmentVariable(left), .missingEnvironmentVariable(right)):
125137
return left == right
126138

@@ -141,10 +153,10 @@ public func == (_ lhs: CarthageError, _ rhs: CarthageError) -> Bool {
141153

142154
case (.gitHubAPITimeout, .gitHubAPITimeout):
143155
return true
144-
156+
145157
case let (.buildFailed(la, lb), .buildFailed(ra, rb)):
146158
return la == ra && lb == rb
147-
159+
148160
case let (.taskError(left), .taskError(right)):
149161
return left == right
150162

@@ -204,6 +216,9 @@ extension CarthageError: CustomStringConvertible {
204216
case let .parseError(description):
205217
return "Parse error: \(description)"
206218

219+
case let .invalidBinaryJSON(url, error):
220+
return "Unable to parse binary-only framework JSON at \(url) due to error: \(error)"
221+
207222
case let .invalidArchitectures(description):
208223
return "Invalid architecture: \(description)"
209224

@@ -224,7 +239,7 @@ extension CarthageError: CustomStringConvertible {
224239
case let .gitHub(repository):
225240
description += "\n\nIf you believe this to be an error, please file an issue with the maintainers at \(repository.newIssueURL.absoluteString)"
226241

227-
case .git:
242+
case .git, .binary:
228243
break
229244
}
230245

@@ -273,7 +288,7 @@ extension CarthageError: CustomStringConvertible {
273288

274289
case let .unresolvedDependencies(names):
275290
return "No entry found for \(names.count > 1 ? "dependencies" : "dependency") \(names.joined(separator: ", ")) in Cartfile.resolved – please run `carthage update` if the dependency is contained in the project's Cartfile."
276-
291+
277292
case let .buildFailed(taskError, log):
278293
var message = "Build Failed\n"
279294
if case let .shellTaskFailed(task, exitCode, _) = taskError {
@@ -294,6 +309,82 @@ extension CarthageError: CustomStringConvertible {
294309
}
295310
}
296311

312+
/// Error parsing strings into types, used in Scannable protocol
313+
public struct ScannableError : Error, Equatable {
314+
315+
let message: String
316+
let currentLine: String?
317+
318+
public init(message: String, currentLine: String? = nil) {
319+
self.message = message
320+
self.currentLine = currentLine
321+
}
322+
323+
}
324+
325+
extension ScannableError: CustomStringConvertible {
326+
public var description: String {
327+
return "\(message) in line: \(currentLine)"
328+
}
329+
}
330+
331+
public func == (lhs: ScannableError, rhs: ScannableError) -> Bool {
332+
return lhs.description == rhs.description && lhs.currentLine == rhs.currentLine
333+
}
334+
335+
/// Error parsing a binary-only framework JSON file, used in CarthageError.invalidBinaryJSON.
336+
public enum BinaryJSONError: Error {
337+
338+
/// Unable to parse the JSON.
339+
case invalidJSON(NSError?)
340+
341+
/// Unable to parse a semantic version from a framework entry.
342+
case invalidVersion(ScannableError)
343+
344+
/// Unable to parse a URL from a framework entry.
345+
case invalidURL(String)
346+
347+
/// URL is non-HTTPS
348+
case nonHTTPSURL(URL)
349+
}
350+
351+
extension BinaryJSONError: CustomStringConvertible {
352+
public var description: String {
353+
switch self {
354+
case let .invalidJSON(error):
355+
return "invalid JSON: \(error)"
356+
case let .invalidVersion(error):
357+
return "unable to parse semantic version: \(error)"
358+
case let .invalidURL(string):
359+
return "invalid URL: \(string)"
360+
case let .nonHTTPSURL(url):
361+
return "specified URL '\(url)' must be HTTPS"
362+
}
363+
}
364+
}
365+
366+
extension BinaryJSONError: Equatable {
367+
public static func == (lhs: BinaryJSONError, rhs: BinaryJSONError) -> Bool {
368+
switch (lhs, rhs) {
369+
case let (.invalidJSON(left), .invalidJSON(right)):
370+
return left == right
371+
372+
case let (.invalidVersion(left), .invalidVersion(right)):
373+
return left == right
374+
375+
case let (.invalidURL(left), .invalidURL(right)):
376+
return left == right
377+
378+
case let (.nonHTTPSURL(left), .nonHTTPSURL(right)):
379+
return left == right
380+
381+
default:
382+
return false
383+
}
384+
}
385+
}
386+
387+
297388
/// A duplicate dependency, used in CarthageError.duplicateDependencies.
298389
public struct DuplicateDependency: Comparable {
299390
/// The duplicate dependency as a project.

‎Source/CarthageKit/FrameworkExtensions.swift

+47
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ extension String {
2626
observer.sendCompleted()
2727
}
2828
}
29+
30+
/// Strips off a trailing string, if present.
31+
internal func stripping(suffix: String) -> String {
32+
if hasSuffix(suffix) {
33+
let end = characters.index(endIndex, offsetBy: -suffix.characters.count)
34+
return self[startIndex..<end]
35+
} else {
36+
return self
37+
}
38+
}
2939
}
3040

3141
/// Merges `rhs` into `lhs` and returns the result.
@@ -309,6 +319,43 @@ extension Reactive where Base: FileManager {
309319
}
310320
}
311321

322+
private let defaultSessionError = NSError(domain: CarthageKitBundleIdentifier,
323+
code: 1,
324+
userInfo: nil)
325+
326+
extension Reactive where Base: URLSession {
327+
/// Returns a SignalProducer which performs a downloadTask associated with an
328+
/// `NSURLSession`
329+
///
330+
/// - parameters:
331+
/// - request: A request that will be performed when the producer is
332+
/// started
333+
///
334+
/// - returns: A producer that will execute the given request once for each
335+
/// invocation of `start()`.
336+
///
337+
/// - note: This method will not send an error event in the case of a server
338+
/// side error (i.e. when a response with status code other than
339+
/// 200...299 is received).
340+
internal func download(with request: URLRequest) -> SignalProducer<(URL, URLResponse), AnyError> {
341+
return SignalProducer { [base = self.base] observer, disposable in
342+
let task = base.downloadTask(with: request) { url, response, error in
343+
if let url = url, let response = response {
344+
observer.send(value: (url, response))
345+
observer.sendCompleted()
346+
} else {
347+
observer.send(error: AnyError(error ?? defaultSessionError))
348+
}
349+
}
350+
351+
disposable += {
352+
task.cancel()
353+
}
354+
task.resume()
355+
}
356+
}
357+
}
358+
312359
/// Creates a counted set from a sequence. The counted set is represented as a
313360
/// dictionary where the keys are elements from the sequence and values count
314361
/// how many times elements are present in the sequence.

‎Source/CarthageKit/Git.swift

+6-11
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ public struct GitURL: Equatable {
2727

2828
if let parsedURL = parsedURL, let host = parsedURL.host {
2929
// Normal, valid URL.
30-
let path = stripGitSuffix(parsedURL.carthage_path)
30+
let path = strippingGitSuffix(parsedURL.carthage_path)
3131
return "\(host)\(path)"
3232
} else if urlString.hasPrefix("/") {
3333
// Local path.
34-
return stripGitSuffix(urlString)
34+
return strippingGitSuffix(urlString)
3535
} else {
3636
// scp syntax.
3737
var strippedURLString = urlString
@@ -46,7 +46,7 @@ public struct GitURL: Equatable {
4646
strippedURLString.removeSubrange(strippedURLString.startIndex...index)
4747
}
4848

49-
var path = stripGitSuffix(strippedURLString)
49+
var path = strippingGitSuffix(strippedURLString)
5050
if !path.hasPrefix("/") {
5151
// This probably isn't strictly legit, but we'll have a forward
5252
// slash for other URL types.
@@ -64,7 +64,7 @@ public struct GitURL: Equatable {
6464
return components
6565
.last
6666
.map(String.init)
67-
.map(stripGitSuffix)
67+
.map(strippingGitSuffix)
6868
}
6969

7070
public init(_ urlString: String) {
@@ -73,13 +73,8 @@ public struct GitURL: Equatable {
7373
}
7474

7575
/// Strips any trailing .git in the given name, if one exists.
76-
public func stripGitSuffix(_ string: String) -> String {
77-
if string.hasSuffix(".git") {
78-
let end = string.characters.index(string.endIndex, offsetBy: -4)
79-
return string[string.startIndex..<end]
80-
} else {
81-
return string
82-
}
76+
public func strippingGitSuffix(_ string: String) -> String {
77+
return string.stripping(suffix: ".git")
8378
}
8479

8580
public func ==(_ lhs: GitURL, _ rhs: GitURL) -> Bool {

‎Source/CarthageKit/GitHub.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ extension Repository {
4949
/// Parses repository information out of a string of the form "owner/name"
5050
/// for the github.com, or the form "http(s)://hostname/owner/name" for
5151
/// Enterprise instances.
52-
public static func fromIdentifier(_ identifier: String) -> Result<Repository, CarthageError> {
52+
public static func fromIdentifier(_ identifier: String) -> Result<Repository, ScannableError> {
5353
// GitHub.com
5454
let range = NSRange(location: 0, length: identifier.utf16.count)
5555
if let match = NWORegex.firstMatch(in: identifier, range: range) {
5656
let owner = (identifier as NSString).substring(with: match.rangeAt(1))
5757
let name = (identifier as NSString).substring(with: match.rangeAt(2))
58-
return .success(self.init(owner: owner, name: stripGitSuffix(name)))
58+
return .success(self.init(owner: owner, name: strippingGitSuffix(name)))
5959
}
6060

6161
// GitHub Enterprise
@@ -72,14 +72,14 @@ extension Repository {
7272
// If the host name starts with “github.com”, that is not an enterprise
7373
// one.
7474
if host == "github.com" || host == "www.github.com" {
75-
return .success(self.init(owner: owner, name: stripGitSuffix(name)))
75+
return .success(self.init(owner: owner, name: strippingGitSuffix(name)))
7676
} else {
7777
let baseURL = url.deletingLastPathComponent().deletingLastPathComponent()
78-
return .success(self.init(server: .enterprise(url: baseURL), owner: owner, name: stripGitSuffix(name)))
78+
return .success(self.init(server: .enterprise(url: baseURL), owner: owner, name: strippingGitSuffix(name)))
7979
}
8080
}
8181

82-
return .failure(CarthageError.parseError(description: "invalid GitHub repository identifier \"\(identifier)\""))
82+
return .failure(ScannableError(message: "invalid GitHub repository identifier \"\(identifier)\""))
8383
}
8484
}
8585

‎Source/CarthageKit/Project.swift

+203-73
Large diffs are not rendered by default.

‎Source/CarthageKit/Scannable.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ public protocol Scannable {
1515
///
1616
/// If parsing fails, the scanner will be left at the first invalid
1717
/// character (with any partially valid input already consumed).
18-
static func from(_ scanner: Scanner) -> Result<Self, CarthageError>
18+
static func from(_ scanner: Scanner) -> Result<Self, ScannableError>
1919
}

‎Source/CarthageKit/Version.swift

+14-14
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public struct SemanticVersion: VersionType, Comparable {
4747
fileprivate static let versionCharacterSet = CharacterSet(charactersIn: "0123456789.")
4848

4949
/// Attempts to parse a semantic version from a PinnedVersion.
50-
public static func from(_ pinnedVersion: PinnedVersion) -> Result<SemanticVersion, CarthageError> {
50+
public static func from(_ pinnedVersion: PinnedVersion) -> Result<SemanticVersion, ScannableError> {
5151
let scanner = Scanner(string: pinnedVersion.commitish)
5252

5353
// Skip leading characters, like "v" or "version-" or anything like
@@ -60,7 +60,7 @@ public struct SemanticVersion: VersionType, Comparable {
6060
} else {
6161
// Disallow versions like "1.0a5", because we only support
6262
// SemVer right now.
63-
return .failure(CarthageError.parseError(description: "syntax of version \"\(version)\" is unsupported"))
63+
return .failure(ScannableError(message: "syntax of version \"\(version)\" is unsupported", currentLine: scanner.currentLine))
6464
}
6565
}
6666
}
@@ -69,30 +69,30 @@ public struct SemanticVersion: VersionType, Comparable {
6969
extension SemanticVersion: Scannable {
7070
/// Attempts to parse a semantic version from a human-readable string of the
7171
/// form "a.b.c".
72-
public static func from(_ scanner: Scanner) -> Result<SemanticVersion, CarthageError> {
72+
public static func from(_ scanner: Scanner) -> Result<SemanticVersion, ScannableError> {
7373
var version: NSString? = nil
7474
guard scanner.scanCharacters(from: versionCharacterSet, into: &version), let unwrapped = version else {
75-
return .failure(.parseError(description: "expected version in line: \(scanner.currentLine)"))
75+
return .failure(ScannableError(message: "expected version", currentLine: scanner.currentLine))
7676
}
7777

7878
let components = (unwrapped as String)
7979
.characters
8080
.split(omittingEmptySubsequences: true) { $0 == "." }
8181
.map(String.init)
8282
if components.isEmpty {
83-
return .failure(.parseError(description: "expected version in line: \(scanner.currentLine)"))
83+
return .failure(ScannableError(message: "expected version", currentLine: scanner.currentLine))
8484
}
8585

8686
func parseVersion(at index: Int) -> Int? {
8787
return components.count > index ? Int(components[index]) : nil
8888
}
8989

9090
guard let major = parseVersion(at: 0) else {
91-
return .failure(.parseError(description: "expected major version number in \"\(unwrapped)\""))
91+
return .failure(ScannableError(message: "expected major version number", currentLine: scanner.currentLine))
9292
}
9393

9494
guard let minor = parseVersion(at: 1) else {
95-
return .failure(.parseError(description: "expected minor version number in \"\(unwrapped)\""))
95+
return .failure(ScannableError(message: "expected minor version number", currentLine: scanner.currentLine))
9696
}
9797

9898
let patch = parseVersion(at: 2) ?? 0
@@ -140,18 +140,18 @@ public func ==(_ lhs: PinnedVersion, _ rhs: PinnedVersion) -> Bool {
140140
}
141141

142142
extension PinnedVersion: Scannable {
143-
public static func from(_ scanner: Scanner) -> Result<PinnedVersion, CarthageError> {
143+
public static func from(_ scanner: Scanner) -> Result<PinnedVersion, ScannableError> {
144144
if !scanner.scanString("\"", into: nil) {
145-
return .failure(CarthageError.parseError(description: "expected pinned version in line: \(scanner.currentLine)"))
145+
return .failure(ScannableError(message: "expected pinned version", currentLine: scanner.currentLine))
146146
}
147147

148148
var commitish: NSString? = nil
149149
if !scanner.scanUpTo("\"", into: &commitish) || commitish == nil {
150-
return .failure(CarthageError.parseError(description: "empty pinned version in line: \(scanner.currentLine)"))
150+
return .failure(ScannableError(message: "empty pinned version", currentLine: scanner.currentLine))
151151
}
152152

153153
if !scanner.scanString("\"", into: nil) {
154-
return .failure(CarthageError.parseError(description: "unterminated pinned version in line: \(scanner.currentLine)"))
154+
return .failure(ScannableError(message: "unterminated pinned version", currentLine: scanner.currentLine))
155155
}
156156

157157
return .success(self.init(commitish! as String))
@@ -251,7 +251,7 @@ public func ==(_ lhs: VersionSpecifier, _ rhs: VersionSpecifier) -> Bool {
251251

252252
extension VersionSpecifier: Scannable {
253253
/// Attempts to parse a VersionSpecifier.
254-
public static func from(_ scanner: Scanner) -> Result<VersionSpecifier, CarthageError> {
254+
public static func from(_ scanner: Scanner) -> Result<VersionSpecifier, ScannableError> {
255255
if scanner.scanString("==", into: nil) {
256256
return SemanticVersion.from(scanner).map { .exactly($0) }
257257
} else if scanner.scanString(">=", into: nil) {
@@ -261,11 +261,11 @@ extension VersionSpecifier: Scannable {
261261
} else if scanner.scanString("\"", into: nil) {
262262
var refName: NSString? = nil
263263
if !scanner.scanUpTo("\"", into: &refName) || refName == nil {
264-
return .failure(CarthageError.parseError(description: "expected Git reference name in line: \(scanner.currentLine)"))
264+
return .failure(ScannableError(message: "expected Git reference name", currentLine: scanner.currentLine))
265265
}
266266

267267
if !scanner.scanString("\"", into: nil) {
268-
return .failure(CarthageError.parseError(description: "unterminated Git reference name in line: \(scanner.currentLine)"))
268+
return .failure(ScannableError(message: "unterminated Git reference name", currentLine: scanner.currentLine))
269269
}
270270

271271
return .success(.gitReference(refName! as String))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nope.gif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"1.0": "https://my.domain.com/release/1.0.0/framework.zip",
3+
"1.0.1": "https://my.domain.com/release/1.0.1/framework.zip"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import Foundation
2+
import Nimble
3+
import Quick
4+
5+
@testable import CarthageKit
6+
7+
class BinaryProjectSpec: QuickSpec {
8+
override func spec() {
9+
10+
describe("from") {
11+
12+
let testUrl = URL(string: "http://my.domain.com")!
13+
14+
it("should parse") {
15+
let jsonData = (
16+
"{" +
17+
"\"1.0\": \"https://my.domain.com/release/1.0.0/framework.zip\"," +
18+
"\"1.0.1\": \"https://my.domain.com/release/1.0.1/framework.zip\"" +
19+
"}"
20+
).data(using: .utf8)!
21+
22+
let actualBinaryProject = BinaryProject.from(jsonData: jsonData, url: testUrl).value
23+
24+
let expectedBinaryProject = BinaryProject(versions: [
25+
PinnedVersion("1.0"): URL(string: "https://my.domain.com/release/1.0.0/framework.zip")!,
26+
PinnedVersion("1.0.1"): URL(string: "https://my.domain.com/release/1.0.1/framework.zip")!,
27+
])
28+
29+
expect(actualBinaryProject).to(equal(expectedBinaryProject))
30+
}
31+
32+
it("should fail if string is not JSON") {
33+
let jsonData = "definitely not JSON".data(using: .utf8)!
34+
35+
let actualError = BinaryProject.from(jsonData: jsonData, url: testUrl).error
36+
37+
switch actualError {
38+
case .some(.invalidJSON(_)): break
39+
default:
40+
fail("Expected invalidJSON error")
41+
}
42+
}
43+
44+
it("should fail if string is not a dictionary of strings") {
45+
let jsonData = "[\"this\", \"is\", \"not\", \"a\", \"dictionary\"]".data(using: .utf8)!
46+
47+
let actualError = BinaryProject.from(jsonData: jsonData, url: testUrl).error
48+
49+
expect(actualError).to(equal(BinaryJSONError.invalidJSON(NSError(domain: CarthageKitBundleIdentifier,
50+
code: 1,
51+
userInfo: [NSLocalizedDescriptionKey: "Binary definition was not expected type [String: String]"]))))
52+
}
53+
54+
it("should fail with an invalid semantic version") {
55+
let jsonData = "{ \"1.a\": \"https://my.domain.com/release/1.0.0/framework.zip\" }".data(using: .utf8)!
56+
57+
let actualError = BinaryProject.from(jsonData: jsonData, url: testUrl).error
58+
59+
expect(actualError).to(equal(BinaryJSONError.invalidVersion(ScannableError(message: "expected minor version number", currentLine: "1.a"))))
60+
}
61+
62+
it("should fail with a non-parseable URL") {
63+
let jsonData = "{ \"1.0\": \"💩\" }".data(using: .utf8)!
64+
65+
let actualError = BinaryProject.from(jsonData: jsonData, url: testUrl).error
66+
67+
expect(actualError).to(equal(BinaryJSONError.invalidURL("💩")))
68+
}
69+
70+
it("should fail with a non HTTPS url") {
71+
let jsonData = "{ \"1.0\": \"http://my.domain.com/framework.zip\" }".data(using: .utf8)!
72+
73+
let actualError = BinaryProject.from(jsonData: jsonData, url: testUrl).error
74+
75+
expect(actualError).to(equal(BinaryJSONError.nonHTTPSURL(URL(string: "http://my.domain.com/framework.zip")!)))
76+
}
77+
78+
}
79+
80+
}
81+
}

‎Source/CarthageKitTests/CartfileSpec.swift

+8
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ class CartfileSpec: QuickSpec {
145145
let dupe5 = dupes[2]
146146
expect(dupe5) == ProjectIdentifier.gitHub(Repository(owner: "5", name: "5"))
147147
}
148+
149+
it("should not allow a binary framework with git reference") {
150+
151+
let testCartfile = "binary \"https://server.com/myproject\" \"gitreference\""
152+
let result = Cartfile.from(string: testCartfile)
153+
154+
expect(result.error).to(equal(CarthageError.parseError(description: "binary dependencies cannot have a git reference for the version specifier in line: binary \"https://server.com/myproject\" \"gitreference\"")))
155+
}
148156
}
149157
}
150158

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import CarthageKit
2+
import Foundation
3+
import Nimble
4+
import Quick
5+
import Tentacle
6+
7+
class ProjectIdentifierSpec: QuickSpec {
8+
override func spec() {
9+
10+
var dependencyType: String!
11+
12+
sharedExamples("invalid dependency") { (sharedExampleContext: @escaping SharedExampleContext) in
13+
14+
beforeEach {
15+
guard let type = sharedExampleContext()["dependencyType"] as? String else {
16+
fail("no dependency type")
17+
return
18+
}
19+
20+
dependencyType = type
21+
}
22+
23+
it("should fail without dependency") {
24+
let scanner = Scanner(string: dependencyType)
25+
26+
let error = ProjectIdentifier.from(scanner).error
27+
28+
let expectedError = ScannableError(message: "expected string after dependency type", currentLine: dependencyType)
29+
expect(error).to(equal(expectedError))
30+
}
31+
32+
it("should fail without closing quote on dependency") {
33+
let scanner = Scanner(string: "\(dependencyType!) \"dependency")
34+
35+
let error = ProjectIdentifier.from(scanner).error
36+
37+
let expectedError = ScannableError(message: "empty or unterminated string after dependency type", currentLine: "\(dependencyType!) \"dependency")
38+
expect(error).to(equal(expectedError))
39+
}
40+
41+
it("should fail with empty dependency") {
42+
let scanner = Scanner(string: "\(dependencyType!) \" \"")
43+
44+
let error = ProjectIdentifier.from(scanner).error
45+
46+
let expectedError = ScannableError(message: "empty or unterminated string after dependency type", currentLine: "\(dependencyType!) \" \"")
47+
expect(error).to(equal(expectedError))
48+
}
49+
}
50+
51+
describe("name") {
52+
context ("github") {
53+
54+
it("should equal the name of a github.com repo") {
55+
let projectIdentifier = ProjectIdentifier.gitHub(Repository(owner: "owner", name: "name"))
56+
57+
expect(projectIdentifier.name).to(equal("name"))
58+
}
59+
60+
it("should equal the name of an enterprise github repo") {
61+
let enterpriseRepo = Repository(
62+
server: .enterprise(url: URL(string: "http://server.com")!),
63+
owner: "owner",
64+
name: "name")
65+
66+
let projectIdentifier = ProjectIdentifier.gitHub(enterpriseRepo)
67+
68+
expect(projectIdentifier.name).to(equal("name"))
69+
}
70+
}
71+
72+
context("git") {
73+
74+
it("should be the last component of the URL") {
75+
let projectIdentifier = ProjectIdentifier.git(GitURL("ssh://server.com/myproject"))
76+
77+
expect(projectIdentifier.name).to(equal("myproject"))
78+
}
79+
80+
it("should not include the trailing git suffix") {
81+
let projectIdentifier = ProjectIdentifier.git(GitURL("ssh://server.com/myproject.git"))
82+
83+
expect(projectIdentifier.name).to(equal("myproject"))
84+
}
85+
86+
it("should be the entire URL string if there is no last component") {
87+
let projectIdentifier = ProjectIdentifier.git(GitURL("whatisthisurleven"))
88+
89+
expect(projectIdentifier.name).to(equal("whatisthisurleven"))
90+
}
91+
92+
}
93+
94+
context("binary") {
95+
96+
it("should be the last component of the URL") {
97+
let projectIdentifier = ProjectIdentifier.binary(URL(string: "https://server.com/myproject")!)
98+
99+
expect(projectIdentifier.name).to(equal("myproject"))
100+
}
101+
102+
it("should not include the trailing git suffix") {
103+
let projectIdentifier = ProjectIdentifier.binary(URL(string: "https://server.com/myproject.json")!)
104+
105+
expect(projectIdentifier.name).to(equal("myproject"))
106+
}
107+
108+
}
109+
}
110+
111+
describe("from") {
112+
113+
context("github") {
114+
115+
it("should read a github.com dependency") {
116+
let scanner = Scanner(string: "github \"ReactiveCocoa/ReactiveCocoa\"")
117+
118+
let projectIdentifier = ProjectIdentifier.from(scanner).value
119+
120+
let expectedRepo = Repository(owner: "ReactiveCocoa", name: "ReactiveCocoa")
121+
expect(projectIdentifier).to(equal(ProjectIdentifier.gitHub(expectedRepo)))
122+
}
123+
124+
it("should read a github.com dependency with full url") {
125+
let scanner = Scanner(string: "github \"https://github.com/ReactiveCocoa/ReactiveCocoa\"")
126+
127+
let projectIdentifier = ProjectIdentifier.from(scanner).value
128+
129+
let expectedRepo = Repository(owner: "ReactiveCocoa", name: "ReactiveCocoa")
130+
expect(projectIdentifier).to(equal(ProjectIdentifier.gitHub(expectedRepo)))
131+
}
132+
133+
it("should read an enterprise github dependency") {
134+
let scanner = Scanner(string: "github \"http://mysupercoolinternalwebhost.com/ReactiveCocoa/ReactiveCocoa\"")
135+
136+
let projectIdentifier = ProjectIdentifier.from(scanner).value
137+
138+
let expectedRepo = Repository(
139+
server: .enterprise(url: URL(string: "http://mysupercoolinternalwebhost.com")!),
140+
owner: "ReactiveCocoa",
141+
name: "ReactiveCocoa")
142+
expect(projectIdentifier).to(equal(ProjectIdentifier.gitHub(expectedRepo)))
143+
}
144+
145+
it("should fail with invalid github.com dependency") {
146+
let scanner = Scanner(string: "github \"Whatsthis\"")
147+
148+
let error = ProjectIdentifier.from(scanner).error
149+
150+
let expectedError = ScannableError(message: "invalid GitHub repository identifier \"Whatsthis\"")
151+
expect(error).to(equal(expectedError))
152+
}
153+
154+
it("should fail with invalid enterprise github dependency") {
155+
let scanner = Scanner(string: "github \"http://mysupercoolinternalwebhost.com/ReactiveCocoa\"")
156+
157+
let error = ProjectIdentifier.from(scanner).error
158+
159+
let expectedError = ScannableError(message: "invalid GitHub repository identifier \"http://mysupercoolinternalwebhost.com/ReactiveCocoa\"")
160+
expect(error).to(equal(expectedError))
161+
}
162+
163+
itBehavesLike("invalid dependency") { ["dependencyType": "github"] }
164+
}
165+
166+
context("git") {
167+
168+
it("should read a git URL") {
169+
let scanner = Scanner(string: "git \"mygiturl\"")
170+
171+
let projectIdentifier = ProjectIdentifier.from(scanner).value
172+
173+
expect(projectIdentifier).to(equal(ProjectIdentifier.git(GitURL("mygiturl"))))
174+
}
175+
176+
itBehavesLike("invalid dependency") { ["dependencyType": "git"] }
177+
178+
}
179+
180+
context("binary") {
181+
182+
it("should read a URL") {
183+
let scanner = Scanner(string: "binary \"https://mysupercoolinternalwebhost.com/\"")
184+
185+
let projectIdentifier = ProjectIdentifier.from(scanner).value
186+
187+
expect(projectIdentifier).to(equal(ProjectIdentifier.binary(URL(string: "https://mysupercoolinternalwebhost.com/")!)))
188+
}
189+
190+
it("should fail with non-https URL") {
191+
let scanner = Scanner(string: "binary \"nope\"")
192+
193+
let error = ProjectIdentifier.from(scanner).error
194+
195+
expect(error).to(equal(ScannableError(message: "non-https URL found for dependency type `binary`", currentLine: "binary \"nope\"")))
196+
}
197+
198+
it("should fail with invalid URL") {
199+
let scanner = Scanner(string: "binary \"nop@%@#^@e\"")
200+
201+
let error = ProjectIdentifier.from(scanner).error
202+
203+
expect(error).to(equal(ScannableError(message: "invalid URL found for dependency type `binary`", currentLine: "binary \"nop@%@#^@e\"")))
204+
}
205+
206+
itBehavesLike("invalid dependency") { ["dependencyType": "binary"] }
207+
}
208+
209+
}
210+
211+
212+
}
213+
}

‎Source/CarthageKitTests/ProjectSpec.swift

+52-1
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,65 @@ class ProjectSpec: QuickSpec {
164164
assertProjectEvent(commitish: commitish) { expect($0).to(beNil()) }
165165
}
166166

167-
it ("should not fetch twice in a row, even if no commitish is given") {
167+
it("should not fetch twice in a row, even if no commitish is given") {
168168
// Clone first
169169
expect(cloneOrFetch().wait().error).to(beNil())
170170

171171
assertProjectEvent { expect($0?.isFetching) == true }
172172
assertProjectEvent(clearFetchTime: false) { expect($0).to(beNil())}
173173
}
174174
}
175+
176+
describe("downloadBinaryFrameworkDefinition") {
177+
178+
var project: Project!
179+
let testDefinitionURL = Bundle(for: type(of: self)).url(forResource: "successful", withExtension: "json")!
180+
181+
beforeEach {
182+
project = Project(directoryURL: URL(string: "file://fake")!)
183+
}
184+
185+
it("should return definition") {
186+
let actualDefinition = project.downloadBinaryFrameworkDefinition(url: testDefinitionURL).first()?.value
187+
188+
let expectedBinaryProject = BinaryProject(versions: [
189+
PinnedVersion("1.0"): URL(string: "https://my.domain.com/release/1.0.0/framework.zip")!,
190+
PinnedVersion("1.0.1"): URL(string: "https://my.domain.com/release/1.0.1/framework.zip")!,
191+
])
192+
expect(actualDefinition).to(equal(expectedBinaryProject))
193+
}
194+
195+
it("should return read failed if unable to download") {
196+
let actualError = project.downloadBinaryFrameworkDefinition(url: URL(string: "file:///thisfiledoesnotexist.json")!).first()?.error
197+
198+
switch actualError {
199+
case .some(.readFailed): break
200+
default:
201+
fail("expected read failed error")
202+
}
203+
}
204+
205+
it("should return an invalid binary JSON error if unable to parse file") {
206+
let invalidDependencyURL = Bundle(for: type(of: self)).url(forResource: "invalid", withExtension: "json")!
207+
208+
let actualError = project.downloadBinaryFrameworkDefinition(url: invalidDependencyURL).first()?.error
209+
210+
switch actualError {
211+
case .some(CarthageError.invalidBinaryJSON(invalidDependencyURL, BinaryJSONError.invalidJSON(_))): break
212+
default:
213+
fail("expected invalid binary JSON error")
214+
}
215+
}
216+
217+
it("should broadcast downloading framework definition event") {
218+
var events = [ProjectEvent]()
219+
project.projectEvents.observeValues { events.append($0) }
220+
221+
_ = project.downloadBinaryFrameworkDefinition(url: testDefinitionURL).first()
222+
223+
expect(events).to(equal([ProjectEvent.downloadingBinaryFrameworkDefinition(ProjectIdentifier.binary(testDefinitionURL), testDefinitionURL)]))
224+
}
225+
}
175226
}
176227
}
177228

‎Source/carthage/BuildVersion.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public func remoteVersion() -> SemanticVersion? {
2424
.map { _, releases in releases.first! }
2525
.mapError(CarthageError.gitHubAPIRequestFailed)
2626
.attemptMap { release -> Result<SemanticVersion, CarthageError> in
27-
return SemanticVersion.from(Scanner(string: release.tag))
27+
return SemanticVersion.from(Scanner(string: release.tag)).mapError(CarthageError.init(scannableError:))
2828
}
2929
.timeout(after: 0.5, raising: CarthageError.gitHubAPITimeout, on: QueueScheduler.main)
3030
.first()

‎Source/carthage/Extensions.swift

+3
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ internal struct ProjectEventSink {
104104
case let .checkingOut(project, revision):
105105
carthage.println(formatting.bullets + "Checking out " + formatting.projectName(project.name) + " at " + formatting.quote(revision))
106106

107+
case let .downloadingBinaryFrameworkDefinition(project, url):
108+
carthage.println(formatting.bullets + "Downloading binary-only framework " + formatting.projectName(project.name) + " at " + formatting.quote(url.absoluteString))
109+
107110
case let .downloadingBinaries(project, release):
108111
carthage.println(formatting.bullets + "Downloading " + formatting.projectName(project.name) + ".framework binary at " + formatting.quote(release))
109112

0 commit comments

Comments
 (0)
Please sign in to comment.