Skip to content

Commit c410d6d

Browse files
authored
Add subcommands to install and uninstall packages (#6768)
This adds 2 following subcommands, - `swift package install` - `swift package uninstall` # Motivation & Modifications: The motivation stems from the desire to add tools to SwiftPM to install local executables from SwiftPM packages, other package managers already do this. This pull request adds the above-mentioned subcommands to give SwiftPM the ability able to: - Install remote executables - Remove the installed executables # Result: Users will be able to use the 2 subcommands mentioned above in order to install & uninstall executable products from locally cloned packages.
1 parent 69dc9cb commit c410d6d

File tree

4 files changed

+184
-5
lines changed

4 files changed

+184
-5
lines changed

Sources/Basics/FileSystem/FileSystem+Extensions.swift

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import struct TSCBasic.ByteString
1919
import struct TSCBasic.FileInfo
2020
import class TSCBasic.FileLock
2121
import enum TSCBasic.FileMode
22-
import enum TSCBasic.FileSystemAttribute
2322
import protocol TSCBasic.FileSystem
23+
import enum TSCBasic.FileSystemAttribute
2424
import var TSCBasic.localFileSystem
2525
import protocol TSCBasic.WritableByteStream
2626

@@ -132,7 +132,8 @@ extension FileSystem {
132132
/// - Parameters:
133133
/// - path: The path at which to create the link.
134134
/// - destination: The path to which the link points to.
135-
/// - relative: If `relative` is true, the symlink contents will be a relative path, otherwise it will be absolute.
135+
/// - relative: If `relative` is true, the symlink contents will be a relative path, otherwise it will be
136+
/// absolute.
136137
public func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
137138
try self.createSymbolicLink(path.underlying, pointingAt: destination.underlying, relative: relative)
138139
}
@@ -270,6 +271,38 @@ extension FileSystem {
270271
}
271272
}
272273

274+
extension FileSystem {
275+
private var dotSwiftPMInstalledBinsDir: AbsolutePath {
276+
get throws {
277+
try self.dotSwiftPM.appending("bin")
278+
}
279+
}
280+
281+
public func getOrCreateSwiftPMInstalledBinariesDirectory() throws -> AbsolutePath {
282+
let idiomaticInstalledBinariesDirectory = try self.dotSwiftPMInstalledBinsDir
283+
// Create idiomatic if necessary
284+
if !self.exists(idiomaticInstalledBinariesDirectory) {
285+
try self.createDirectory(idiomaticInstalledBinariesDirectory, recursive: true)
286+
}
287+
// Create ~/.swiftpm if necessary
288+
if !self.exists(try self.dotSwiftPM) {
289+
try self.createDirectory(self.dotSwiftPM, recursive: true)
290+
}
291+
// Create ~/.swiftpm/bin symlink if necessary
292+
// locking ~/.swiftpm to protect from concurrent access
293+
try self.withLock(on: self.dotSwiftPM, type: .exclusive) {
294+
if !self.exists(try self.dotSwiftPMInstalledBinsDir, followSymlink: false) {
295+
try self.createSymbolicLink(
296+
self.dotSwiftPMInstalledBinsDir,
297+
pointingAt: idiomaticInstalledBinariesDirectory,
298+
relative: false
299+
)
300+
}
301+
}
302+
return idiomaticInstalledBinariesDirectory
303+
}
304+
}
305+
273306
// MARK: - configuration
274307

275308
extension FileSystem {
@@ -320,9 +353,11 @@ extension FileSystem {
320353
}
321354
}
322355

323-
// in the case where ~/.swiftpm/configuration is not the idiomatic location (eg on macOS where its /Users/<user>/Library/org.swift.swiftpm/configuration)
356+
// in the case where ~/.swiftpm/configuration is not the idiomatic location (eg on macOS where its
357+
// /Users/<user>/Library/org.swift.swiftpm/configuration)
324358
if try idiomaticConfigurationDirectory != self.dotSwiftPMConfigurationDirectory {
325-
// copy the configuration files from old location (eg /Users/<user>/Library/org.swift.swiftpm) to new one (eg /Users/<user>/Library/org.swift.swiftpm/configuration)
359+
// copy the configuration files from old location (eg /Users/<user>/Library/org.swift.swiftpm) to new one
360+
// (eg /Users/<user>/Library/org.swift.swiftpm/configuration)
326361
// but leave them there for backwards compatibility (eg older xcode)
327362
let oldConfigDirectory = idiomaticConfigurationDirectory.parentDirectory
328363
if self.exists(oldConfigDirectory, followSymlink: false) && self.isDirectory(oldConfigDirectory) {
@@ -400,7 +435,8 @@ extension FileSystem {
400435
public func getOrCreateSwiftPMSecurityDirectory() throws -> AbsolutePath {
401436
let idiomaticSecurityDirectory = try self.swiftPMSecurityDirectory
402437

403-
// temporary 5.6, remove on next version: transition from ~/.swiftpm/security to idiomatic location + symbolic link
438+
// temporary 5.6, remove on next version: transition from ~/.swiftpm/security to idiomatic location + symbolic
439+
// link
404440
if try idiomaticSecurityDirectory != self.dotSwiftPMSecurityDirectory &&
405441
self.exists(try self.dotSwiftPMSecurityDirectory) &&
406442
self.isDirectory(try self.dotSwiftPMSecurityDirectory)

Sources/Commands/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ add_library(Commands
1717
PackageTools/EditCommands.swift
1818
PackageTools/Format.swift
1919
PackageTools/Init.swift
20+
PackageTools/InstalledPackages.swift
2021
PackageTools/Learn.swift
2122
PackageTools/PluginCommand.swift
2223
PackageTools/ResetCommands.swift
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
import CoreCommands
15+
import Foundation
16+
import PackageModel
17+
import TSCBasic
18+
19+
extension SwiftPackageTool {
20+
struct Install: SwiftCommand {
21+
static let configuration = CommandConfiguration(
22+
commandName: "experimental-install",
23+
abstract: "Offers the ability to install executable products of the current package."
24+
)
25+
26+
@OptionGroup()
27+
var globalOptions: GlobalOptions
28+
29+
@Option(help: "The name of the executable product to install")
30+
var product: String?
31+
32+
func run(_ tool: SwiftTool) throws {
33+
let swiftpmBinDir = try tool.fileSystem.getOrCreateSwiftPMInstalledBinariesDirectory()
34+
35+
let env = ProcessInfo.processInfo.environment
36+
37+
if let path = env.path, !path.contains(swiftpmBinDir.pathString), !globalOptions.logging.quiet {
38+
tool.observabilityScope.emit(
39+
warning: """
40+
PATH doesn't include \(swiftpmBinDir.pathString)! This means you won't be able to access \
41+
the installed executables by default, and will need to specify the full path.
42+
"""
43+
)
44+
}
45+
46+
let alreadyExisting = (try? InstalledPackageProduct.installedProducts(tool.fileSystem)) ?? []
47+
48+
let workspace = try tool.getActiveWorkspace()
49+
let packageRoot = try tool.getPackageRoot()
50+
51+
let packageGraph = try workspace.loadPackageGraph(
52+
rootPath: packageRoot,
53+
observabilityScope: tool.observabilityScope
54+
)
55+
56+
let possibleCandidates = packageGraph.rootPackages.flatMap(\.products)
57+
.filter { $0.type == .executable }
58+
59+
let productToInstall: Product
60+
61+
switch possibleCandidates.count {
62+
case 0:
63+
throw StringError("No Executable Products in Package.swift.")
64+
case 1:
65+
productToInstall = possibleCandidates[0].underlyingProduct
66+
default:
67+
guard let product, let first = possibleCandidates.first(where: { $0.name == product }) else {
68+
throw StringError(
69+
"""
70+
Multiple candidates found, however, no product was specified. Specify a product with the \
71+
`--product` option
72+
"""
73+
)
74+
}
75+
76+
productToInstall = first.underlyingProduct
77+
}
78+
79+
if let existingPkg = alreadyExisting.first(where: { $0.name == productToInstall.name }) {
80+
throw StringError("\(productToInstall.name) is already installed at \(existingPkg.path)")
81+
}
82+
83+
try tool.createBuildSystem(explicitProduct: productToInstall.name)
84+
.build(subset: .product(productToInstall.name))
85+
86+
let binPath = try tool.buildParameters().buildPath.appending(component: productToInstall.name)
87+
let finalBinPath = swiftpmBinDir.appending(component: binPath.basename)
88+
try tool.fileSystem.copy(from: binPath, to: finalBinPath)
89+
90+
print("Executable product `\(productToInstall.name)` was successfully installed to \(finalBinPath).")
91+
}
92+
}
93+
94+
struct Uninstall: SwiftCommand {
95+
static let configuration = CommandConfiguration(
96+
commandName: "experimental-uninstall",
97+
abstract: "Offers the ability to uninstall executable products of installed package products"
98+
)
99+
100+
@OptionGroup
101+
var globalOptions: GlobalOptions
102+
103+
@Argument(help: "Name of the executable to uninstall.")
104+
var name: String
105+
106+
func run(_ tool: SwiftTool) throws {
107+
let alreadyInstalled = (try? InstalledPackageProduct.installedProducts(tool.fileSystem)) ?? []
108+
109+
guard let removedExecutable = alreadyInstalled.first(where: { $0.name == name }) else {
110+
// The installed executable doesn't exist - let the user know, and stop here.
111+
throw StringError("No such installed executable as \(name)")
112+
}
113+
114+
try tool.fileSystem.removeFileTree(removedExecutable.path)
115+
print("Executable product `\(self.name)` was successfully uninstalled from \(removedExecutable.path).")
116+
}
117+
}
118+
}
119+
120+
private struct InstalledPackageProduct {
121+
static func installedProducts(_ fileSystem: FileSystem) throws -> [InstalledPackageProduct] {
122+
let binPath = try fileSystem.getOrCreateSwiftPMInstalledBinariesDirectory()
123+
124+
let contents = ((try? fileSystem.getDirectoryContents(binPath)) ?? [])
125+
.map { binPath.appending($0) }
126+
127+
return contents.map { path in
128+
InstalledPackageProduct(path: .init(path))
129+
}
130+
}
131+
132+
/// The name of this installed product, being the basename of the path.
133+
var name: String {
134+
self.path.basename
135+
}
136+
137+
/// Path of the executable.
138+
let path: AbsolutePath
139+
}

Sources/Commands/PackageTools/SwiftPackageTool.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public struct SwiftPackageTool: ParsableCommand {
4141
Init.self,
4242
Format.self,
4343

44+
Install.self,
45+
Uninstall.self,
46+
4447
APIDiff.self,
4548
DeprecatedAPIDiff.self,
4649
DumpSymbolGraph.self,

0 commit comments

Comments
 (0)