Skip to content

Commit e013f97

Browse files
committed
relative symlinks
1 parent 33d4d8e commit e013f97

File tree

5 files changed

+92
-86
lines changed

5 files changed

+92
-86
lines changed

Sources/TSCBasic/FileSystem.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ public protocol FileSystem: class {
169169
/// - Parameters:
170170
/// - path: The path at which to create the link.
171171
/// - destination: The path to which the link points to.
172-
func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath) throws
172+
/// - relative: If `relative` is true, the symlink contents will be a relative path, otherwise it will be absolute.
173+
func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws
173174

174175
// FIXME: This is obviously not a very efficient or flexible API.
175176
//
@@ -349,8 +350,9 @@ private class LocalFileSystem: FileSystem {
349350
try FileManager.default.createDirectory(atPath: path.pathString, withIntermediateDirectories: recursive, attributes: [:])
350351
}
351352

352-
func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath) throws {
353-
try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destination.pathString)
353+
func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
354+
let destString = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString
355+
try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString)
354356
}
355357

356358
func readFileContents(_ path: AbsolutePath) throws -> ByteString {
@@ -495,7 +497,7 @@ public class InMemoryFileSystem: FileSystem {
495497
private enum NodeContents {
496498
case file(ByteString)
497499
case directory(DirectoryContents)
498-
case symlink(AbsolutePath)
500+
case symlink(String)
499501

500502
/// Creates deep copy of the object.
501503
func copy() -> NodeContents {
@@ -525,6 +527,7 @@ public class InMemoryFileSystem: FileSystem {
525527
return contents
526528
}
527529
}
530+
528531
// Used to ensure that DispatchQueues are releassed when they are no longer in use.
529532
private struct WeakReference<Value: AnyObject> {
530533
weak var reference: Value?
@@ -577,6 +580,7 @@ public class InMemoryFileSystem: FileSystem {
577580
case .directory, .file:
578581
return node
579582
case .symlink(let destination):
583+
let destination = AbsolutePath(destination, relativeTo: path.parentDirectory)
580584
return followSymlink ? try getNodeInternal(destination) : node
581585
case .none:
582586
return nil
@@ -709,7 +713,7 @@ public class InMemoryFileSystem: FileSystem {
709713
contents.entries[path.basename] = Node(.directory(DirectoryContents()))
710714
}
711715

712-
public func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath) throws {
716+
public func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
713717
// Create directory to destination parent.
714718
guard let destinationParent = try getNode(path.parentDirectory) else {
715719
throw FileSystemError.noEntry
@@ -724,6 +728,8 @@ public class InMemoryFileSystem: FileSystem {
724728
throw FileSystemError.alreadyExistsAtDestination
725729
}
726730

731+
let destination = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString
732+
727733
contents.entries[path.basename] = Node(.symlink(destination))
728734
}
729735

@@ -838,7 +844,7 @@ public class InMemoryFileSystem: FileSystem {
838844
let resolvedPath: AbsolutePath
839845

840846
if case let .symlink(destination) = try getNode(path)?.contents {
841-
resolvedPath = destination
847+
resolvedPath = AbsolutePath(destination, relativeTo: path.parentDirectory)
842848
} else {
843849
resolvedPath = path
844850
}
@@ -935,10 +941,10 @@ public class RerootedFileSystemView: FileSystem {
935941
return try underlyingFileSystem.createDirectory(path, recursive: recursive)
936942
}
937943

938-
public func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath) throws {
944+
public func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
939945
let path = formUnderlyingPath(path)
940946
let destination = formUnderlyingPath(destination)
941-
return try underlyingFileSystem.createSymbolicLink(path, pointingAt: destination)
947+
return try underlyingFileSystem.createSymbolicLink(path, pointingAt: destination, relative: relative)
942948
}
943949

944950
public func readFileContents(_ path: AbsolutePath) throws -> ByteString {

Tests/TSCBasicTests/FileSystemTests.swift

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class FileSystemTests: XCTestCase {
3737

3838
// isSymlink()
3939
let sym = tempDirPath.appending(component: "hello")
40-
try! fs.createSymbolicLink(sym, pointingAt: file.path)
40+
try! fs.createSymbolicLink(sym, pointingAt: file.path, relative: false)
4141
XCTAssertTrue(fs.isSymlink(sym))
4242
XCTAssertTrue(fs.isFile(sym))
4343
XCTAssertEqual(try fs.getFileInfo(sym).fileType, .typeSymbolicLink)
@@ -46,7 +46,7 @@ class FileSystemTests: XCTestCase {
4646
// isExecutableFile
4747
let executable = tempDirPath.appending(component: "exec-foo")
4848
let executableSym = tempDirPath.appending(component: "exec-sym")
49-
try! fs.createSymbolicLink(executableSym, pointingAt: executable)
49+
try! fs.createSymbolicLink(executableSym, pointingAt: executable, relative: false)
5050
let stream = BufferedOutputByteStream()
5151
stream <<< """
5252
#!/bin/sh
@@ -84,6 +84,74 @@ class FileSystemTests: XCTestCase {
8484
}
8585
}
8686

87+
func testResolvingSymlinks() {
88+
// Make sure the root path resolves to itself.
89+
XCTAssertEqual(resolveSymlinks(AbsolutePath.root), AbsolutePath.root)
90+
91+
// For the rest of the tests we'll need a temporary directory.
92+
try! withTemporaryDirectory(removeTreeOnDeinit: true) { path in
93+
// FIXME: it would be better to not need to resolve symbolic links, but we end up relying on /tmp -> /private/tmp.
94+
let tmpDirPath = resolveSymlinks(path)
95+
96+
// Create a symbolic link and directory.
97+
let slnkPath = tmpDirPath.appending(component: "slnk")
98+
let fldrPath = tmpDirPath.appending(component: "fldr")
99+
100+
// Create a symbolic link pointing at the (so far non-existent) directory.
101+
try! localFileSystem.createSymbolicLink(slnkPath, pointingAt: fldrPath, relative: true)
102+
103+
// Resolving the symlink should not yet change anything.
104+
XCTAssertEqual(resolveSymlinks(slnkPath), slnkPath)
105+
106+
// Create a directory to be the referent of the symbolic link.
107+
try! makeDirectories(fldrPath)
108+
109+
// Resolving the symlink should now point at the directory.
110+
XCTAssertEqual(resolveSymlinks(slnkPath), fldrPath)
111+
112+
// Resolving the directory should still not change anything.
113+
XCTAssertEqual(resolveSymlinks(fldrPath), fldrPath)
114+
}
115+
}
116+
117+
func testSymlinksNotWalked() {
118+
try! withTemporaryDirectory(removeTreeOnDeinit: true) { path in
119+
// FIXME: it would be better to not need to resolve symbolic links, but we end up relying on /tmp -> /private/tmp.
120+
let tmpDirPath = resolveSymlinks(path)
121+
122+
try! makeDirectories(tmpDirPath.appending(component: "foo"))
123+
try! makeDirectories(tmpDirPath.appending(components: "bar", "baz", "goo"))
124+
try! localFileSystem.createSymbolicLink(tmpDirPath.appending(components: "foo", "symlink"), pointingAt: tmpDirPath.appending(component: "bar"), relative: true)
125+
126+
XCTAssertTrue(localFileSystem.isSymlink(tmpDirPath.appending(components: "foo", "symlink")))
127+
XCTAssertEqual(resolveSymlinks(tmpDirPath.appending(components: "foo", "symlink")), tmpDirPath.appending(component: "bar"))
128+
XCTAssertTrue(localFileSystem.isDirectory(resolveSymlinks(tmpDirPath.appending(components: "foo", "symlink", "baz"))))
129+
130+
let results = try! walk(tmpDirPath.appending(component: "foo")).map{ $0 }
131+
132+
XCTAssertEqual(results, [tmpDirPath.appending(components: "foo", "symlink")])
133+
}
134+
}
135+
136+
func testWalkingADirectorySymlinkResolvesOnce() {
137+
try! withTemporaryDirectory(removeTreeOnDeinit: true) { tmpDirPath in
138+
try! makeDirectories(tmpDirPath.appending(components: "foo", "bar"))
139+
try! makeDirectories(tmpDirPath.appending(components: "abc", "bar"))
140+
try! localFileSystem.createSymbolicLink(tmpDirPath.appending(component: "symlink"), pointingAt: tmpDirPath.appending(component: "foo"), relative: true)
141+
try! localFileSystem.createSymbolicLink(tmpDirPath.appending(components: "foo", "baz"), pointingAt: tmpDirPath.appending(component: "abc"), relative: true)
142+
143+
XCTAssertTrue(localFileSystem.isSymlink(tmpDirPath.appending(component: "symlink")))
144+
145+
let results = try! walk(tmpDirPath.appending(component: "symlink")).map{ $0 }.sorted()
146+
147+
// we recurse a symlink to a directory, so this should work,
148+
// but `abc` should not show because `baz` is a symlink too
149+
// and that should *not* be followed
150+
151+
XCTAssertEqual(results, [tmpDirPath.appending(components: "symlink", "bar"), tmpDirPath.appending(components: "symlink", "baz")])
152+
}
153+
}
154+
87155
func testLocalExistsSymlink() throws {
88156
try testWithTemporaryDirectory { tmpdir in
89157
let fs = TSCBasic.localFileSystem
@@ -94,7 +162,7 @@ class FileSystemTests: XCTestCase {
94162

95163
// Source and target exist.
96164

97-
try fs.createSymbolicLink(source, pointingAt: target)
165+
try fs.createSymbolicLink(source, pointingAt: target, relative: false)
98166
XCTAssertEqual(fs.exists(source), true)
99167
XCTAssertEqual(fs.exists(source, followSymlink: true), true)
100168
XCTAssertEqual(fs.exists(source, followSymlink: false), true)
@@ -385,7 +453,7 @@ class FileSystemTests: XCTestCase {
385453

386454
// Source and target exist.
387455

388-
try fs.createSymbolicLink(source, pointingAt: target)
456+
try fs.createSymbolicLink(source, pointingAt: target, relative: false)
389457
XCTAssertEqual(fs.exists(source), true)
390458
XCTAssertEqual(fs.exists(source, followSymlink: true), true)
391459
XCTAssertEqual(fs.exists(source, followSymlink: false), true)
@@ -598,7 +666,7 @@ class FileSystemTests: XCTestCase {
598666

599667
// Source and target exist.
600668

601-
try fs.createSymbolicLink(source, pointingAt: target)
669+
try fs.createSymbolicLink(source, pointingAt: target, relative: false)
602670
XCTAssertEqual(fs.exists(source), true)
603671
XCTAssertEqual(fs.exists(source, followSymlink: true), true)
604672
XCTAssertEqual(fs.exists(source, followSymlink: false), true)
@@ -631,7 +699,7 @@ class FileSystemTests: XCTestCase {
631699
try fs.createDirectory(dir, recursive: true)
632700
try fs.writeFileContents(foo, bytes: "")
633701
try fs.writeFileContents(bar, bytes: "")
634-
try fs.createSymbolicLink(sym, pointingAt: foo)
702+
try fs.createSymbolicLink(sym, pointingAt: foo, relative: false)
635703

636704
// Set foo to unwritable.
637705
try fs.chmod(.userUnWritable, path: foo)

Tests/TSCBasicTests/POSIXTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ class POSIXTests : XCTestCase {
2626
XCTAssertTrue(localFileSystem.isDirectory(dirPath))
2727

2828
let sym = dirPath.appending(component: "hello")
29-
try localFileSystem.createSymbolicLink(sym, pointingAt: file.path)
29+
try localFileSystem.createSymbolicLink(sym, pointingAt: file.path, relative: false)
3030
XCTAssertTrue(localFileSystem.exists(sym))
3131
XCTAssertTrue(localFileSystem.isFile(sym))
3232
XCTAssertFalse(localFileSystem.isDirectory(sym))
3333

3434
try withTemporaryDirectory(removeTreeOnDeinit: true) { dir2Path in
3535
let dirSym = dirPath.appending(component: "dir2")
36-
try localFileSystem.createSymbolicLink(dirSym, pointingAt: dir2Path)
36+
try localFileSystem.createSymbolicLink(dirSym, pointingAt: dir2Path, relative: false)
3737
XCTAssertTrue(localFileSystem.exists(dirSym))
3838
XCTAssertFalse(localFileSystem.isFile(dirSym))
3939
XCTAssertTrue(localFileSystem.isDirectory(dirSym))

Tests/TSCBasicTests/PathShimTests.swift

Lines changed: 1 addition & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,7 @@ import XCTest
1414
import TSCBasic
1515

1616
class PathShimTests : XCTestCase {
17-
18-
func testResolvingSymlinks() {
19-
// Make sure the root path resolves to itself.
20-
XCTAssertEqual(resolveSymlinks(AbsolutePath.root), AbsolutePath.root)
21-
22-
// For the rest of the tests we'll need a temporary directory.
23-
try! withTemporaryDirectory(removeTreeOnDeinit: true) { path in
24-
// FIXME: it would be better to not need to resolve symbolic links, but we end up relying on /tmp -> /private/tmp.
25-
let tmpDirPath = resolveSymlinks(path)
26-
27-
// Create a symbolic link and directory.
28-
let slnkPath = tmpDirPath.appending(component: "slnk")
29-
let fldrPath = tmpDirPath.appending(component: "fldr")
30-
31-
// Create a symbolic link pointing at the (so far non-existent) directory.
32-
try! localFileSystem.createSymbolicLink(slnkPath, pointingAt: fldrPath)
33-
34-
// Resolving the symlink should not yet change anything.
35-
XCTAssertEqual(resolveSymlinks(slnkPath), slnkPath)
36-
37-
// Create a directory to be the referent of the symbolic link.
38-
try! makeDirectories(fldrPath)
39-
40-
// Resolving the symlink should now point at the directory.
41-
XCTAssertEqual(resolveSymlinks(slnkPath), fldrPath)
42-
43-
// Resolving the directory should still not change anything.
44-
XCTAssertEqual(resolveSymlinks(fldrPath), fldrPath)
45-
}
46-
}
47-
17+
4818
func testRescursiveDirectoryCreation() {
4919
// For the tests we'll need a temporary directory.
5020
try! withTemporaryDirectory(removeTreeOnDeinit: true) { path in
@@ -105,42 +75,4 @@ class WalkTests : XCTestCase {
10575
}
10676
XCTAssertEqual(expected, [])
10777
}
108-
109-
func testSymlinksNotWalked() {
110-
try! withTemporaryDirectory(removeTreeOnDeinit: true) { path in
111-
// FIXME: it would be better to not need to resolve symbolic links, but we end up relying on /tmp -> /private/tmp.
112-
let tmpDirPath = resolveSymlinks(path)
113-
114-
try! makeDirectories(tmpDirPath.appending(component: "foo"))
115-
try! makeDirectories(tmpDirPath.appending(components: "bar", "baz", "goo"))
116-
try! localFileSystem.createSymbolicLink(tmpDirPath.appending(components: "foo", "symlink"), pointingAt: tmpDirPath.appending(component: "bar"))
117-
118-
XCTAssertTrue(localFileSystem.isSymlink(tmpDirPath.appending(components: "foo", "symlink")))
119-
XCTAssertEqual(resolveSymlinks(tmpDirPath.appending(components: "foo", "symlink")), tmpDirPath.appending(component: "bar"))
120-
XCTAssertTrue(localFileSystem.isDirectory(resolveSymlinks(tmpDirPath.appending(components: "foo", "symlink", "baz"))))
121-
122-
let results = try! walk(tmpDirPath.appending(component: "foo")).map{ $0 }
123-
124-
XCTAssertEqual(results, [tmpDirPath.appending(components: "foo", "symlink")])
125-
}
126-
}
127-
128-
func testWalkingADirectorySymlinkResolvesOnce() {
129-
try! withTemporaryDirectory(removeTreeOnDeinit: true) { tmpDirPath in
130-
try! makeDirectories(tmpDirPath.appending(components: "foo", "bar"))
131-
try! makeDirectories(tmpDirPath.appending(components: "abc", "bar"))
132-
try! localFileSystem.createSymbolicLink(tmpDirPath.appending(component: "symlink"), pointingAt: tmpDirPath.appending(component: "foo"))
133-
try! localFileSystem.createSymbolicLink(tmpDirPath.appending(components: "foo", "baz"), pointingAt: tmpDirPath.appending(component: "abc"))
134-
135-
XCTAssertTrue(localFileSystem.isSymlink(tmpDirPath.appending(component: "symlink")))
136-
137-
let results = try! walk(tmpDirPath.appending(component: "symlink")).map{ $0 }.sorted()
138-
139-
// we recurse a symlink to a directory, so this should work,
140-
// but `abc` should not show because `baz` is a symlink too
141-
// and that should *not* be followed
142-
143-
XCTAssertEqual(results, [tmpDirPath.appending(components: "symlink", "bar"), tmpDirPath.appending(components: "symlink", "baz")])
144-
}
145-
}
14678
}

Tests/TSCUtilityTests/DownloaderTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ class FailingFileSystem: FileSystem {
517517
fatalError("unexpected call")
518518
}
519519

520-
func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath) throws {
520+
func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
521521
fatalError("unexpected call")
522522
}
523523

0 commit comments

Comments
 (0)