diff --git a/code/cli/munki/shared/munkirepo/RepoFactory.swift b/code/cli/munki/shared/munkirepo/RepoFactory.swift index b3502f85..08e7679e 100644 --- a/code/cli/munki/shared/munkirepo/RepoFactory.swift +++ b/code/cli/munki/shared/munkirepo/RepoFactory.swift @@ -20,6 +20,51 @@ import Foundation +/// Loads a Repo plugin from a dylib +/// implementation lifted from +/// https://theswiftdev.com/building-and-loading-dynamic-libraries-at-runtime-in-swift/ +func loadRepoPlugin(at path: String) throws -> RepoPluginBuilder { + typealias InitFunction = @convention(c) () -> UnsafeMutableRawPointer + + let openRes = dlopen(path, RTLD_NOW | RTLD_LOCAL) + if openRes != nil { + defer { + dlclose(openRes) + } + + let symbolName = "createPlugin" + let sym = dlsym(openRes, symbolName) + + if sym != nil { + let f: InitFunction = unsafeBitCast(sym, to: InitFunction.self) + let pluginPointer = f() + let builder = Unmanaged.fromOpaque(pluginPointer).takeRetainedValue() + return builder + } else { + throw MunkiError("Could not find symbol \(symbolName) in lib: \(path)") + } + } else { + if let err = dlerror() { + throw MunkiError("Error opening lib: \(String(format: "%s", err)), path: \(path)") + } else { + throw MunkiError("Error opening lib: unknown error, path: \(path)") + } + } +} + +/// Try to load a Repo plugin from our RepoPlugins directory +func findRepoInPlugins(_ name: String, url: String) throws -> Repo? { + let pluginName = name + ".plugin" + let repoPluginsDir = (Bundle.main.bundlePath as NSString).appendingPathComponent("RepoPlugins") + let repoPluginNames = (try? FileManager.default.contentsOfDirectory(atPath: repoPluginsDir)) ?? [] + if repoPluginNames.contains(pluginName) { + let pluginPath = (repoPluginsDir as NSString).appendingPathComponent(pluginName) + let repoPlugin = try loadRepoPlugin(at: pluginPath) + return repoPlugin.connect(url) + } + return nil +} + /// Factory function that returns an instance of a specific Repo class func repoConnect(url: String, plugin: String = "FileRepo") throws -> Repo { switch plugin { @@ -28,6 +73,9 @@ func repoConnect(url: String, plugin: String = "FileRepo") throws -> Repo { case "GitFileRepo": return try GitFileRepo(url) default: + if let repo = try findRepoInPlugins(plugin, url: url) { + return repo + } throw MunkiError("No repo plugin named \"\(plugin)\"") } } diff --git a/code/cli/munki/shared/munkirepo/RepoProtocol.swift b/code/cli/munki/shared/munkirepo/RepoProtocol.swift index 09db1dc4..9d868987 100644 --- a/code/cli/munki/shared/munkirepo/RepoProtocol.swift +++ b/code/cli/munki/shared/munkirepo/RepoProtocol.swift @@ -4,6 +4,19 @@ // // Created by Greg Neagle on 5/8/25. // +// Copyright 2024-2025 Greg Neagle. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. import Foundation diff --git a/code/cli/munki/shared/munkirepo/repoutils.swift b/code/cli/munki/shared/munkirepo/repoutils.swift index d8bece41..7c1e0bb5 100644 --- a/code/cli/munki/shared/munkirepo/repoutils.swift +++ b/code/cli/munki/shared/munkirepo/repoutils.swift @@ -4,6 +4,19 @@ // // Created by Greg Neagle on 7/15/24. // +// Copyright 2024-2025 Greg Neagle. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. import Foundation