Skip to content

Commit d070b1e

Browse files
authored
Implement the install command on Linux (swiftlang#9)
1 parent f6ddb3a commit d070b1e

25 files changed

+1172
-171
lines changed

.github/workflows/tests.yml

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: tests
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-18.04
12+
steps:
13+
- name: Download Swift
14+
run: |
15+
wget --no-verbose "https://download.swift.org/swift-5.7-release/ubuntu1804/swift-5.7-RELEASE/swift-5.7-RELEASE-ubuntu18.04.tar.gz"
16+
tar -zxf swift-5.7-RELEASE-ubuntu18.04.tar.gz
17+
mkdir $HOME/.swift
18+
mv swift-5.7-RELEASE-ubuntu18.04/usr $HOME/.swift
19+
20+
- name: Update PATH
21+
run: echo "$HOME/.swift/usr/bin" >> $GITHUB_PATH
22+
23+
- name: Install libarchive
24+
run: sudo apt-get install -y libarchive-dev
25+
26+
- uses: actions/checkout@v3
27+
28+
- name: Build
29+
run: swift build --build-tests
30+
31+
- name: Run tests
32+
run: swift test
33+
env:
34+
SWIFTLY_PLATFORM_NAME: ubuntu1804
35+
SWIFTLY_PLATFORM_NAME_FULL: ubuntu18.04
36+
SWIFTLY_PLATFORM_NAME_PRETTY: Ubuntu 18.04
37+
SWIFTLY_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Package.swift

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let package = Package(
1414
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.2"),
1515
.package(url: "https://github.com/swift-server/async-http-client", from: "1.9.0"),
1616
.package(url: "https://github.com/apple/swift-nio.git", from: "2.38.0"),
17+
.package(url: "https://github.com/apple/swift-tools-support-core.git", from: "0.2.7"),
1718
],
1819
targets: [
1920
.executableTarget(
@@ -22,6 +23,7 @@ let package = Package(
2223
.product(name: "ArgumentParser", package: "swift-argument-parser"),
2324
.target(name: "SwiftlyCore"),
2425
.target(name: "LinuxPlatform", condition: .when(platforms: [.linux])),
26+
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
2527
]
2628
),
2729
.target(
@@ -35,6 +37,14 @@ let package = Package(
3537
name: "LinuxPlatform",
3638
dependencies: [
3739
"SwiftlyCore",
40+
"CLibArchive",
41+
]
42+
),
43+
.systemLibrary(
44+
name: "CLibArchive",
45+
pkgConfig: "libarchive",
46+
providers: [
47+
.apt(["libarchive-dev"])
3848
]
3949
),
4050
.testTarget(

Sources/CLibArchive/module.modulemap

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module CLibArchive [system] {
2+
header "shim.h"
3+
link "archive"
4+
export *
5+
}

Sources/CLibArchive/shim.h

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#include "archive.h"
2+
#include "archive_entry.h"

Sources/LinuxPlatform/Extract.swift

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import CLibArchive
2+
import Foundation
3+
4+
// The code in this file consists mainly of a Swift port of the "Complete Extractor" example included in the libarchive
5+
// documentation: https://github.com/libarchive/libarchive/wiki/Examples#a-complete-extractor
6+
7+
struct ExtractError: Error {
8+
let message: String?
9+
10+
init(archive: OpaquePointer?) {
11+
self.message = archive_error_string(archive).map { err in
12+
String(cString: err)
13+
}
14+
}
15+
16+
init(message: String) {
17+
self.message = message
18+
}
19+
}
20+
21+
/// Write the data from the given readArchive into the writeArchive.
22+
func copyData(readArchive: OpaquePointer?, writeArchive: OpaquePointer?) throws {
23+
var r = 0
24+
var buff: UnsafeRawPointer?
25+
var size = 0
26+
var offset: Int64 = 0
27+
28+
while true {
29+
r = Int(archive_read_data_block(readArchive, &buff, &size, &offset))
30+
if r == ARCHIVE_EOF {
31+
return
32+
}
33+
guard r == ARCHIVE_OK else {
34+
throw ExtractError(archive: readArchive)
35+
}
36+
r = Int(archive_write_data_block(writeArchive, buff, size, offset))
37+
guard r == ARCHIVE_OK else {
38+
throw ExtractError(archive: writeArchive)
39+
}
40+
}
41+
}
42+
43+
/// Extract the archive at the provided path. The name of each file included in the archive will be passed to
44+
/// the provided closure which will return the path the file will be written to.
45+
///
46+
/// This uses libarchive under the hood, so a wide variety of archive formats are supported (e.g. .tar.gz).
47+
internal func extractArchive(atPath archivePath: URL, transform: (String) -> URL) throws {
48+
var flags = Int32(0)
49+
flags = ARCHIVE_EXTRACT_TIME
50+
flags |= ARCHIVE_EXTRACT_PERM
51+
flags |= ARCHIVE_EXTRACT_ACL
52+
flags |= ARCHIVE_EXTRACT_FFLAGS
53+
54+
let a = archive_read_new()
55+
archive_read_support_format_all(a)
56+
archive_read_support_filter_all(a)
57+
58+
let ext = archive_write_disk_new()
59+
archive_write_disk_set_options(ext, flags)
60+
archive_write_disk_set_standard_lookup(ext)
61+
62+
defer {
63+
archive_read_close(a)
64+
archive_read_free(a)
65+
archive_write_close(ext)
66+
archive_write_free(ext)
67+
}
68+
69+
if archive_read_open_filename(a, archivePath.path, 10240) != 0 {
70+
throw ExtractError(message: "Failed to open \"\(archivePath.path)\"")
71+
}
72+
73+
while true {
74+
var r = Int32(0)
75+
var entry: OpaquePointer?
76+
r = archive_read_next_header(a, &entry)
77+
if r == ARCHIVE_EOF {
78+
break
79+
}
80+
guard r == ARCHIVE_OK else {
81+
throw ExtractError(archive: a)
82+
}
83+
84+
let currentPath = String(cString: archive_entry_pathname(entry))
85+
archive_entry_set_pathname(entry, transform(currentPath).path)
86+
r = archive_write_header(ext, entry)
87+
guard r == ARCHIVE_OK else {
88+
throw ExtractError(archive: ext)
89+
}
90+
91+
if archive_entry_size(entry) > 0 {
92+
try copyData(readArchive: a, writeArchive: ext)
93+
}
94+
95+
r = archive_write_finish_entry(ext)
96+
guard r == ARCHIVE_OK else {
97+
throw ExtractError(archive: ext)
98+
}
99+
}
100+
}

Sources/LinuxPlatform/Linux.swift

+49-9
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,59 @@ import SwiftlyCore
55
/// This implementation can be reused for any supported Linux platform.
66
/// TODO: replace dummy implementations
77
public struct Linux: Platform {
8-
public let name: String
9-
public let namePretty: String
8+
private let platform: Config.PlatformDefinition
109

11-
public init(name: String, namePretty: String) {
12-
self.name = name
13-
self.namePretty = namePretty
10+
public init(platform: Config.PlatformDefinition) {
11+
self.platform = platform
1412
}
1513

16-
public func download(version _: ToolchainVersion) async throws -> URL {
17-
throw Error(message: "TODO")
14+
public var name: String {
15+
self.platform.name
16+
}
17+
18+
public var nameFull: String {
19+
self.platform.nameFull
20+
}
21+
22+
public var namePretty: String {
23+
self.platform.namePretty
24+
}
25+
26+
public var toolchainFileExtension: String {
27+
"tar.gz"
1828
}
1929

2030
public func isSystemDependencyPresent(_: SystemDependency) -> Bool {
2131
true
2232
}
2333

24-
public func install(from _: URL, version _: ToolchainVersion) throws {}
34+
public func install(from tmpFile: URL, version: ToolchainVersion) throws {
35+
guard tmpFile.fileExists() else {
36+
throw Error(message: "\(tmpFile) doesn't exist")
37+
}
38+
39+
let toolchainsDir = swiftlyHomeDir.appendingPathComponent("toolchains")
40+
if !toolchainsDir.fileExists() {
41+
try FileManager.default.createDirectory(at: toolchainsDir, withIntermediateDirectories: false)
42+
}
43+
44+
print("Extracting toolchain...")
45+
let toolchainDir = toolchainsDir.appendingPathComponent(version.name)
46+
47+
if toolchainDir.fileExists() {
48+
try FileManager.default.removeItem(at: toolchainDir)
49+
}
50+
51+
try extractArchive(atPath: tmpFile) { name in
52+
// drop swift-a.b.c-RELEASE etc name from the extracted files.
53+
let relativePath = name.drop { c in c != "/" }.dropFirst()
54+
55+
// prepend ~/.swiftly/toolchains/<toolchain> to each file name
56+
return toolchainDir.appendingPathComponent(String(relativePath))
57+
}
58+
59+
// TODO: if config doesn't have an active toolchain, set it to that
60+
}
2561

2662
public func uninstall(version _: ToolchainVersion) throws {}
2763

@@ -39,10 +75,14 @@ public struct Linux: Platform {
3975

4076
public func currentToolchain() throws -> ToolchainVersion? { nil }
4177

78+
public func getTempFilePath() -> URL {
79+
FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())")
80+
}
81+
4282
public static let currentPlatform: any Platform = {
4383
do {
4484
let config = try Config.load()
45-
return Linux(name: config.platform.name, namePretty: config.platform.namePretty)
85+
return Linux(platform: config.platform)
4686
} catch {
4787
fatalError("error loading config: \(error)")
4888
}

0 commit comments

Comments
 (0)