Skip to content

Commit 1707e1b

Browse files
authored
Use {install-root}/libexec/container-plugins for plugins. (#341)
- Use a directory that's separate from user data, as user-installed plugins have a distinct lifecycle. - Closes #340.
1 parent 78d4422 commit 1707e1b

5 files changed

Lines changed: 87 additions & 16 deletions

File tree

Sources/APIServer/APIServer.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,28 @@ struct APIServer: AsyncParsableCommand {
129129
}
130130

131131
private func initializePluginLoader(log: Logger) throws -> PluginLoader {
132-
// create user-installed plugins directory if it doesn't exist
133-
let pluginsURL = PluginLoader.userPluginsDir(root: Self.appRoot)
134-
try FileManager.default.createDirectory(at: pluginsURL, withIntermediateDirectories: true)
132+
let installRoot = CommandLine.executablePathUrl
133+
.deletingLastPathComponent()
134+
.appendingPathComponent("..")
135+
.standardized
136+
let pluginsURL = PluginLoader.userPluginsDir(root: installRoot)
137+
var directoryExists: ObjCBool = false
138+
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
139+
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
135140

136141
// plugins built into the application installed as a macOS app bundle
137142
let appBundlePluginsURL = Bundle.main.resourceURL?.appending(path: "plugins")
138143

139144
// plugins built into the application installed as a Unix-like application
140-
let installRootPluginsURL = CommandLine.executableDirectoryUrl.appendingPathComponent("../libexec/container/plugins")
145+
let installRootPluginsURL =
146+
installRoot
147+
.appendingPathComponent("libexec")
148+
.appendingPathComponent("container")
149+
.appendingPathComponent("plugins")
150+
.standardized
141151

142152
let pluginDirectories = [
143-
pluginsURL,
153+
userPluginsURL,
144154
appBundlePluginsURL,
145155
installRootPluginsURL,
146156
].compactMap { $0 }
@@ -150,6 +160,7 @@ struct APIServer: AsyncParsableCommand {
150160
AppBundlePluginFactory(),
151161
]
152162

163+
log.info("PLUGINS: \(pluginDirectories)")
153164
let statePath = PluginLoader.defaultPluginResourcePath(root: Self.appRoot)
154165
try FileManager.default.createDirectory(at: statePath, withIntermediateDirectories: true)
155166
return PluginLoader(pluginDirectories: pluginDirectories, pluginFactories: pluginFactories, defaultResourcePath: statePath, log: log)

Sources/CLI/Application.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,32 @@ struct Application: AsyncParsableCommand {
9494
}()
9595

9696
static let pluginLoader: PluginLoader = {
97-
// create user-installed plugins directory if it doesn't exist
98-
let pluginsURL = PluginLoader.userPluginsDir(root: Self.appRoot)
99-
try! FileManager.default.createDirectory(at: pluginsURL, withIntermediateDirectories: true)
97+
let installRoot = CommandLine.executablePathUrl
98+
.deletingLastPathComponent()
99+
.appendingPathComponent("..")
100+
.standardized
101+
let pluginsURL = PluginLoader.userPluginsDir(root: installRoot)
102+
var directoryExists: ObjCBool = false
103+
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
104+
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
105+
106+
// plugins built into the application installed as a macOS app bundle
107+
let appBundlePluginsURL = Bundle.main.resourceURL?.appending(path: "plugins")
108+
109+
// plugins built into the application installed as a Unix-like application
110+
let installRootPluginsURL =
111+
installRoot
112+
.appendingPathComponent("libexec")
113+
.appendingPathComponent("container")
114+
.appendingPathComponent("plugins")
115+
.standardized
116+
100117
let pluginDirectories = [
101-
pluginsURL
102-
]
118+
userPluginsURL,
119+
appBundlePluginsURL,
120+
installRootPluginsURL,
121+
].compactMap { $0 }
122+
103123
let pluginFactories = [
104124
DefaultPluginFactory()
105125
]

Sources/ContainerPlugin/CommandLine+Executable.swift

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,20 @@
1717
import Foundation
1818

1919
extension CommandLine {
20-
public static var executableDirectoryUrl: URL {
21-
let executablePath = Self.arguments[0]
22-
let executableUrl = URL(filePath: executablePath)
23-
let executableDirectoryUrl = executableUrl.deletingLastPathComponent()
24-
return executableDirectoryUrl.standardized
20+
public static var executablePathUrl: URL {
21+
/// _NSGetExecutablePath with a zero-length buffer returns the needed buffer length
22+
var bufferSize: Int32 = 0
23+
var buffer = [CChar](repeating: 0, count: Int(bufferSize))
24+
_ = _NSGetExecutablePath(&buffer, &bufferSize)
25+
26+
/// Create the buffer and get the path
27+
buffer = [CChar](repeating: 0, count: Int(bufferSize))
28+
guard _NSGetExecutablePath(&buffer, &bufferSize) == 0 else {
29+
fatalError("UNEXPECTED: failed to get executable path")
30+
}
31+
32+
/// Return the path with the executable file component removed the last component and
33+
let executablePath = String(cString: &buffer)
34+
return URL(filePath: executablePath)
2535
}
2636
}

Sources/ContainerPlugin/PluginLoader.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ public struct PluginLoader: Sendable {
4444
}
4545

4646
static public func userPluginsDir(root: URL) -> URL {
47-
root.appending(path: "user-plugins")
47+
root
48+
.appending(path: "libexec")
49+
.appending(path: "container-plugins")
50+
.resolvingSymlinksInPath()
4851
}
4952
}
5053

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
import Testing
19+
20+
@testable import ContainerPlugin
21+
22+
struct CommandLineExecutableTest {
23+
@Test
24+
func testCLIPluginConfigLoad() async throws {
25+
#expect(CommandLine.executablePathUrl.lastPathComponent == "swiftpm-testing-helper")
26+
}
27+
}

0 commit comments

Comments
 (0)