Compare commits

...

7 Commits

Author SHA1 Message Date
Dash2507
dac007e907 Change "pkg" to "package" (#50) 2019-06-10 17:20:05 -04:00
Max Howell
b6b4a74a26 Move deploy script to @mxcl/ops 2019-04-14 12:40:07 -04:00
Max Howell
b76db41ca4 422 means the release already exists, so… succeed 2019-04-14 10:49:17 -04:00
Max Howell
8d5d67b81b Merge pull request #48 from mxcl/travis-swift-5.0-GM
[travis] Swift 5 GM
2019-03-25 22:02:33 -04:00
Max Howell
21ddc7dc3a [travis] Swift 5 GM 2019-03-25 21:40:17 -04:00
Max Howell
f324b4a562 Merge pull request #47 from mxcl/fix-symlink-delete
Adds `kind` fixes deleting broken symlinks
2019-03-18 19:56:39 -04:00
Max Howell
0e061f9cc8 Adds kind fixes deleting broken symlinks
`delete()` and other functions would check `exists` to do certain behaviors, but `exists` will validate a symlink if the entry is a symlink, thus instead we check if the path is an actual entry now instead.
2019-03-18 19:47:49 -04:00
9 changed files with 106 additions and 201 deletions

168
.github/deploy vendored
View File

@@ -1,168 +0,0 @@
#!/usr/bin/swift sh
import func Darwin.fputs
import var Darwin.stderr
import PMKFoundation // PromiseKit/Foundation ~> 3.3
import LegibleError // @mxcl ~> 1.0
import Foundation
import PromiseKit // @mxcl ~> 6.8
import Path // mxcl/Path.swift ~> 0.15
let env = ProcessInfo.processInfo.environment
let token = env["GITHUB_TOKEN"] ?? env["GITHUB_ACCESS_TOKEN"]!
let slug = env["TRAVIS_REPO_SLUG"]!
let tag = env["TRAVIS_TAG"]!
func fatal(message: String) -> Never {
fputs("error: \(message)\n", stderr)
exit(1)
}
func fatal(error: Error) -> Never {
fatal(message: "\(error.legibleLocalizedDescription)\n\n\(error.legibleDescription)")
}
struct Repo: Decodable {
let description: String
let license: License
struct License: Decodable {
let spdx_id: String
}
}
struct Package: Decodable {
let swiftLanguageVersions: [String]
let targets: [Target]
struct Target: Decodable {
let path: String?
let type: Kind
enum Kind: String, Decodable {
case regular
case test
}
}
}
extension URLRequest {
init(github path: String) {
let url = URL(string: "https://api.github.com\(path)")!
self.init(url: url)
setValue("token \(token)", forHTTPHeaderField: "Authorization")
setValue("application/json", forHTTPHeaderField: "Content-Type")
setValue("application/json", forHTTPHeaderField: "Accept")
}
}
func description() -> Promise<Repo> {
let rq = URLRequest(github: "/repos/\(slug)")
return firstly {
URLSession.shared.dataTask(.promise, with: rq).validate()
}.map { data, _ in
try JSONDecoder().decode(Repo.self, from: data)
}
}
struct User: Decodable {
let name: String
let email: String
}
func email() -> Promise<User> {
let rq = URLRequest(github: "/user")
return firstly {
URLSession.shared.dataTask(.promise, with: rq).validate()
}.map { data, _ in
try JSONDecoder().decode(User.self, from: data)
}
}
func dumpPackage() -> Promise<Package> {
let task = Process()
task.launchPath = "/usr/bin/swift"
task.arguments = ["package", "dump-package"]
return firstly {
task.launch(.promise)
}.map { out, _ in
out.fileHandleForReading.readDataToEndOfFile()
}.map { data in
try JSONDecoder().decode(Package.self, from: data)
}
}
var defaultSwiftVersion: String {
let task = Process()
task.launchPath = "/usr/bin/swift"
task.arguments = ["--version"]
func extract(input: String) -> String {
let range = input.range(of: #"Apple Swift version \d+\.\d+"#, options: .regularExpression)!
return String(input[range].split(separator: " ").last!)
}
return try! firstly {
task.launch(.promise)
}.compactMap { out, _ in
String(data: out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
}.map { out in
extract(input: out)
}.wait()
}
func podspec(repo: Repo, user: User, pkg: Package) -> (Substring, String) {
let (owner, name) = { ($0[0], $0[1]) }(slug.split(separator: "/"))
let swiftVersion = pkg.swiftLanguageVersions.min() ?? defaultSwiftVersion
let targets = pkg.targets.filter{ $0.type == .regular }
guard targets.count == 1 else { fatal(message: "Too many targets for this script!") }
guard let sources = targets[0].path else { fatal(message: "Target has no path!") }
return (name, """
Pod::Spec.new do |s|
s.name = '\(name)'
s.author = { '\(user.name)': '\(user.email)' }
s.source = { git: "https://github.com/\(slug).git", tag: '\(tag)' }
s.version = '\(tag)'
s.summary = '\(repo.description)'
s.license = '\(repo.license.spdx_id)'
s.homepage = "https://github.com/\(slug)"
s.social_media_url = 'https://twitter.com/\(owner)'
s.osx.deployment_target = '10.10'
s.ios.deployment_target = '8.0'
s.tvos.deployment_target = '9.0'
s.watchos.deployment_target = '2.0'
s.source_files = '\(sources)/*.swift'
s.swift_version = '\(swiftVersion)'
end
""")
}
func publishRelease() throws -> Promise<Void> {
struct Input: Encodable {
let tag_name = tag
let name = tag
let body = ""
}
var rq = URLRequest(github: "/repos/\(slug)/releases")
rq.httpMethod = "POST"
rq.httpBody = try JSONEncoder().encode(Input())
return URLSession.shared.dataTask(.promise, with: rq).validate().asVoid()
}
switch CommandLine.arguments[1] {
case "generate-podspec":
firstly {
when(fulfilled: description(), email(), dumpPackage())
}.map(podspec).done { name, podspec in
try podspec.write(toFile: "\(name).podspec", atomically: false, encoding: .utf8)
exit(0)
}.catch {
fatal(error: $0)
}
case "publish-release":
try publishRelease().done {
exit(0)
}.catch {
fatal(error: $0)
}
default:
fatal(message: "invalid usage")
}
RunLoop.main.run()

View File

@@ -57,8 +57,8 @@ jobs:
script: swift test --parallel script: swift test --parallel
- <<: *linux - <<: *linux
env: SWIFT_VERSION='5.0-DEVELOPMENT-SNAPSHOT-2019-01-22-a' env: SWIFT_VERSION='5.0'
name: Linux / Swift 5.0.0-dev+2019.01.22 name: Linux / Swift 5.0.0
- stage: pretest - stage: pretest
name: Check Linux tests are syncd name: Check Linux tests are syncd
@@ -84,7 +84,10 @@ jobs:
- name: CocoaPods - name: CocoaPods
osx_image: xcode10.2 osx_image: xcode10.2
install: brew install mxcl/made/swift-sh install: |
before_script: .github/deploy generate-podspec brew install mxcl/made/swift-sh
curl -O https://raw.githubusercontent.com/mxcl/ops/master/deploy
chmod u+x deploy
before_script: ./deploy generate-podspec
script: pod trunk push script: pod trunk push
after_success: .github/deploy publish-release after_success: ./deploy publish-release

View File

@@ -1,7 +1,7 @@
// swift-tools-version:4.2 // swift-tools-version:4.2
import PackageDescription import PackageDescription
let pkg = Package( let package = Package(
name: "Path.swift", name: "Path.swift",
products: [ products: [
.library(name: "Path", targets: ["Path"]), .library(name: "Path", targets: ["Path"]),

View File

@@ -258,9 +258,15 @@ for that as the check was deemed too expensive to be worthwhile.
equality check is required. equality check is required.
* There are several symlink paths on Mac that are typically automatically * There are several symlink paths on Mac that are typically automatically
resolved by Foundation, eg. `/private`, we attempt to do the same for resolved by Foundation, eg. `/private`, we attempt to do the same for
functions that you would expect it (notably `realpath`), but we do *not* for functions that you would expect it (notably `realpath`), we *do* the same for
`Path.init`, *nor* if you are joining a path that ends up being one of these `Path.init`, but *do not* if you are joining a path that ends up being one of
paths, (eg. `Path.root.join("var/private')`). these paths, (eg. `Path.root.join("var/private')`).
If a `Path` is a symlink but the destination of the link does not exist `exists`
returns `false`. This seems to be the correct thing to do since symlinks are
meant to be an abstraction for filesystems. To instead verify that there is
no filesystem entry there at all check if `kind` is `nil`.
## We do not provide change directory functionality ## We do not provide change directory functionality

View File

@@ -83,4 +83,22 @@ public extension Path {
#endif #endif
return self return self
} }
enum Kind {
case file, symlink, directory
}
var kind: Kind? {
var buf = stat()
guard lstat(string, &buf) == 0 else {
return nil
}
if buf.st_mode & S_IFMT == S_IFLNK {
return .symlink
} else if buf.st_mode & S_IFMT == S_IFDIR {
return .directory
} else {
return .file
}
}
} }

View File

@@ -25,11 +25,11 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func copy(to: Path, overwrite: Bool = false) throws -> Path { func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile, isFile { if overwrite, let tokind = to.kind, tokind != .directory, kind != .directory {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
#if os(Linux) && !swift(>=5.1) // check if fixed #if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, to.isFile { if !overwrite, to.kind != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
@@ -61,15 +61,15 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func copy(into: Path, overwrite: Bool = false) throws -> Path { func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { if into.kind == nil {
try into.mkdir(.p) try into.mkdir(.p)
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile { if overwrite, let kind = rv.kind, kind != .directory {
try rv.delete() try FileManager.default.removeItem(at: rv.url)
} }
#if os(Linux) && !swift(>=5.1) // check if fixed #if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, rv.isFile { if !overwrite, rv.kind != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
@@ -95,7 +95,7 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func move(to: Path, overwrite: Bool = false) throws -> Path { func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile { if overwrite, let kind = to.kind, kind != .directory {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
try FileManager.default.moveItem(at: url, to: to.url) try FileManager.default.moveItem(at: url, to: to.url)
@@ -119,17 +119,21 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func move(into: Path, overwrite: Bool = false) throws -> Path { func move(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { switch into.kind {
case nil:
try into.mkdir(.p) try into.mkdir(.p)
} else if !into.isDirectory { fallthrough
throw CocoaError.error(.fileWriteFileExists) case .directory?:
}
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile { if overwrite, let rvkind = rv.kind, rvkind != .directory {
try FileManager.default.removeItem(at: rv.url) try FileManager.default.removeItem(at: rv.url)
} }
try FileManager.default.moveItem(at: url, to: rv.url) try FileManager.default.moveItem(at: url, to: rv.url)
return rv return rv
case .file?, .symlink?:
throw CocoaError.error(.fileWriteFileExists)
}
} }
/** /**
@@ -138,11 +142,12 @@ public extension Path {
*Path.swift* doesnt error if desired end result preexists. *Path.swift* doesnt error if desired end result preexists.
- Note: On UNIX will this function will succeed if the parent directory is writable and the current user has permission. - Note: On UNIX will this function will succeed if the parent directory is writable and the current user has permission.
- Note: This function will fail if the file or directory is locked - Note: This function will fail if the file or directory is locked
- Note: If entry is a symlink, deletes the symlink.
- SeeAlso: `lock()` - SeeAlso: `lock()`
*/ */
@inlinable @inlinable
func delete() throws { func delete() throws {
if exists { if kind != nil {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
} }
} }
@@ -154,7 +159,7 @@ public extension Path {
@inlinable @inlinable
@discardableResult @discardableResult
func touch() throws -> Path { func touch() throws -> Path {
if !exists { if kind == nil {
guard FileManager.default.createFile(atPath: string, contents: nil) else { guard FileManager.default.createFile(atPath: string, contents: nil) else {
throw CocoaError.error(.fileWriteUnknown) throw CocoaError.error(.fileWriteUnknown)
} }
@@ -228,14 +233,17 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func symlink(into dir: Path) throws -> Path { func symlink(into dir: Path) throws -> Path {
if !dir.exists { switch dir.kind {
case nil, .symlink?:
try dir.mkdir(.p) try dir.mkdir(.p)
} else if !dir.isDirectory { fallthrough
throw CocoaError.error(.fileWriteFileExists) case .directory?:
}
let dst = dir/basename() let dst = dir/basename()
try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string) try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string)
return dst return dst
case .file?:
throw CocoaError.error(.fileWriteFileExists)
}
} }
} }

View File

@@ -8,7 +8,10 @@ import Darwin
public extension Path { public extension Path {
//MARK: Filesystem Properties //MARK: Filesystem Properties
/// Returns true if the path represents an actual filesystem entry. /**
- Returns: `true` if the path represents an actual filesystem entry.
- Note: If `self` is a symlink the return value represents the destination.
*/
var exists: Bool { var exists: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.fileExists(atPath: string)
} }

View File

@@ -69,9 +69,18 @@ class PathTests: XCTestCase {
XCTAssertEqual((Path.root/"tmp/foo/bar").relative(to: .root/"tmp/baz"), "../foo/bar") XCTAssertEqual((Path.root/"tmp/foo/bar").relative(to: .root/"tmp/baz"), "../foo/bar")
} }
func testExists() { func testExists() throws {
XCTAssert(Path.root.exists) XCTAssert(Path.root.exists)
XCTAssert((Path.root/"bin").exists) XCTAssert((Path.root/"bin").exists)
try Path.mktemp { tmpdir in
XCTAssertTrue(tmpdir.exists)
XCTAssertFalse(try tmpdir.bar.symlink(as: tmpdir.foo).exists)
XCTAssertTrue(tmpdir.foo.kind == .symlink)
XCTAssertTrue(try tmpdir.bar.touch().symlink(as: tmpdir.baz).exists)
XCTAssertTrue(tmpdir.bar.kind == .file)
XCTAssertTrue(tmpdir.kind == .directory)
}
} }
func testIsDirectory() { func testIsDirectory() {
@@ -379,6 +388,21 @@ class PathTests: XCTestCase {
#if !os(Linux) #if !os(Linux)
XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete()) XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete())
#endif #endif
// regression test: can delete a symlink that points to a non-existent file
let bar5 = try tmpdir.bar4.symlink(as: tmpdir.bar5)
XCTAssertEqual(bar5.kind, .symlink)
XCTAssertFalse(bar5.exists)
XCTAssertNoThrow(try bar5.delete())
XCTAssertEqual(bar5.kind, nil)
// test that deleting a symlink *only* deletes the symlink
let bar7 = try tmpdir.bar6.touch().symlink(as: tmpdir.bar7)
XCTAssertEqual(bar7.kind, .symlink)
XCTAssertTrue(bar7.exists)
XCTAssertNoThrow(try bar7.delete())
XCTAssertEqual(bar7.kind, nil)
XCTAssertEqual(tmpdir.bar6.kind, .file)
} }
} }
@@ -604,4 +628,14 @@ class PathTests: XCTestCase {
let baz: String.SubSequence? = "/a/b:1".split(separator: ":").first let baz: String.SubSequence? = "/a/b:1".split(separator: ":").first
_ = baz.flatMap(Path.init) _ = baz.flatMap(Path.init)
} }
func testKind() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try foo.symlink(as: tmpdir.bar)
XCTAssertEqual(tmpdir.kind, .directory)
XCTAssertEqual(foo.kind, .file)
XCTAssertEqual(bar.kind, .symlink)
}
}
} }

View File

@@ -27,6 +27,7 @@ extension PathTests {
("testInitializerForRelativePath", testInitializerForRelativePath), ("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin), ("testJoin", testJoin),
("testKind", testKind),
("testLock", testLock), ("testLock", testLock),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),