From 97771e45287d02406162866928ec4c8a5a1bf02e Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Sun, 30 Jun 2024 15:06:43 -0700 Subject: [PATCH] Initial Swfit code for CLI tools; makecatalogs should be functional --- code/cli/munki/.gitignore | 7 + .../cli/munki/makecatalogs/makecatalogs.swift | 110 ++++ .../managedsoftwareupdate.swift | 37 ++ .../cli/munki/munki.xcodeproj/project.pbxproj | 617 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 15 + code/cli/munki/munkitester/main.swift | 88 +++ code/cli/munki/shared/admin/admincommon.swift | 34 + .../munki/shared/admin/makecatalogslib.swift | 320 +++++++++ code/cli/munki/shared/cliutils.swift | 83 +++ code/cli/munki/shared/constants.swift | 42 ++ .../cli/munki/shared/munkirepo/FileRepo.swift | 282 ++++++++ .../munki/shared/munkirepo/GitFileRepo.swift | 133 ++++ .../munki/shared/munkirepo/RepoFactory.swift | 20 + code/cli/munki/shared/plistutils.swift | 76 +++ code/cli/munki/shared/prefs.swift | 291 +++++++++ code/cli/munki/shared/utils.swift | 14 + 18 files changed, 2184 insertions(+) create mode 100644 code/cli/munki/.gitignore create mode 100644 code/cli/munki/makecatalogs/makecatalogs.swift create mode 100644 code/cli/munki/managedsoftwareupdate/managedsoftwareupdate.swift create mode 100644 code/cli/munki/munki.xcodeproj/project.pbxproj create mode 100644 code/cli/munki/munki.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 code/cli/munki/munki.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 code/cli/munki/munki.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 code/cli/munki/munkitester/main.swift create mode 100644 code/cli/munki/shared/admin/admincommon.swift create mode 100644 code/cli/munki/shared/admin/makecatalogslib.swift create mode 100644 code/cli/munki/shared/cliutils.swift create mode 100644 code/cli/munki/shared/constants.swift create mode 100644 code/cli/munki/shared/munkirepo/FileRepo.swift create mode 100644 code/cli/munki/shared/munkirepo/GitFileRepo.swift create mode 100644 code/cli/munki/shared/munkirepo/RepoFactory.swift create mode 100644 code/cli/munki/shared/plistutils.swift create mode 100644 code/cli/munki/shared/prefs.swift create mode 100644 code/cli/munki/shared/utils.swift diff --git a/code/cli/munki/.gitignore b/code/cli/munki/.gitignore new file mode 100644 index 00000000..3650497f --- /dev/null +++ b/code/cli/munki/.gitignore @@ -0,0 +1,7 @@ +# .DS_Store files! +.DS_Store + +# Xcode user data +*.xcodeproj/project.xcworkspace/ +*.xcodeproj/xcuserdata/ + diff --git a/code/cli/munki/makecatalogs/makecatalogs.swift b/code/cli/munki/makecatalogs/makecatalogs.swift new file mode 100644 index 00000000..a3139fa1 --- /dev/null +++ b/code/cli/munki/makecatalogs/makecatalogs.swift @@ -0,0 +1,110 @@ +// +// makecatalogs.swift +// munki +// +// Created by Greg Neagle on 6/25/24. +// + +import Foundation +import ArgumentParser + +@main +struct MakeCatalogs: ParsableCommand { + + static let configuration = CommandConfiguration( + commandName: "makecatalogs", + abstract: "Builds Munki catalogs from pkginfo files.", + usage: "makecatalogs [options] []" + ) + + @Flag(name: [.long, .customShort("V")], help: "Print the version of the munki tools and exit.") + var version = false + + @Flag(name: .shortAndLong, help: "Disable sanity checks.") + var force = false + + @Flag(name: .shortAndLong, + help: "Skip checking of pkg existence. Useful when pkgs aren't on the same server as pkginfo, catalogs and manifests.") + var skipPkgCheck = false + + @Option(name: [.customLong("repo-url"), .customLong("repo_url")], + help: "Optional repo URL that takes precedence over the default repo_url specified via preferences.") + var repoURL = "" + + @Option(help: "Specify a custom plugin to connect to the Munki repo.") + var plugin = "FileRepo" + + @Argument(help: "Path to Munki repo") + var repo_path = "" + + var actual_repo_url = "" + + mutating func validate() throws { + if version { + // asking for version info; we don't need to validate there's a repo URL + return + } + // figure out what repo we're working with: we can get a repo URL one of three ways: + // - as a file path provided at the command line + // - as a --repo_url option + // - as a preference stored in the com.googlecode.munki.munkiimport domain + if !repo_path.isEmpty && !repoURL.isEmpty { + // user has specified _both_ repo_path and repo_url! + throw ValidationError("Please specify only one of --repo_url or !") + } + if !repo_path.isEmpty { + // convert path to file URL + if let repo_url_string = NSURL(fileURLWithPath: repo_path).absoluteString { + actual_repo_url = repo_url_string + } + } else if !repoURL.isEmpty { + actual_repo_url = repoURL + } else if let pref_repo_url = adminPref("repo_url") as? String { + actual_repo_url = pref_repo_url + } + + if actual_repo_url.isEmpty { + throw ValidationError("Please specify --repo_url or a repo path.") + } + + } + + mutating func run() throws { + if version { + print(getVersion()) + return + } + + let options = MakeCatalogOptions( + skipPkgCheck: skipPkgCheck, + force: force, + verbose: true + ) + + do { + let repo = try repoConnect(url: actual_repo_url, plugin: plugin) + // TODO: implement repo defining its own makecatalogs method + // let errors = try repo.makecatalogs(options: options) + var catalogsmaker = try CatalogsMaker(repo: repo, options: options) + let errors = catalogsmaker.makecatalogs() + if !errors.isEmpty { + for error in errors { + printStderr(error) + } + throw ExitCode(-1) + } + } catch RepoError.error(let description) { + printStderr("Repo error: \(description)") + throw ExitCode(-1) + } catch MakeCatalogsError.PkginfoAccessError(let description) { + printStderr("Pkginfo read error: \(description)") + throw ExitCode(-1) + } catch MakeCatalogsError.CatalogWriteError(let description) { + printStderr("Catalog write error: \(description)") + throw ExitCode(-1) + } catch { + printStderr("Unexpected error: \(error)") + throw ExitCode(-1) + } + } +} diff --git a/code/cli/munki/managedsoftwareupdate/managedsoftwareupdate.swift b/code/cli/munki/managedsoftwareupdate/managedsoftwareupdate.swift new file mode 100644 index 00000000..0496350a --- /dev/null +++ b/code/cli/munki/managedsoftwareupdate/managedsoftwareupdate.swift @@ -0,0 +1,37 @@ +// +// managedsoftwareupdate.swift +// munki +// +// Created by Greg Neagle on 6/24/24. +// + +import Foundation +import ArgumentParser + +@main +struct ManagedSoftwareUpdate: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "managedsoftwareupdate", + usage: "maangedsoftwareupdate [options]" + ) + + @Flag(name: [.long, .customShort("V")], + help: "Print the version of the munki tools and exit.") + var version = false + + @Flag(name: .long, + help: "Print the current configuration and exit.") + var showConfig = false + + mutating func run() throws { + if version { + print(getVersion()) + return + } + if showConfig { + printConfig() + return + } + print("Nothing much implemented yet!") + } +} diff --git a/code/cli/munki/munki.xcodeproj/project.pbxproj b/code/cli/munki/munki.xcodeproj/project.pbxproj new file mode 100644 index 00000000..bcba9fd3 --- /dev/null +++ b/code/cli/munki/munki.xcodeproj/project.pbxproj @@ -0,0 +1,617 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + C013643D2C2DC529008DB215 /* makecatalogslib.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013643B2C2DC529008DB215 /* makecatalogslib.swift */; }; + C01364402C2DCA5C008DB215 /* admincommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013643E2C2DCA5C008DB215 /* admincommon.swift */; }; + C01364412C2DCA5C008DB215 /* admincommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013643E2C2DCA5C008DB215 /* admincommon.swift */; }; + C01364432C2DD1BA008DB215 /* plistutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364422C2DD1BA008DB215 /* plistutils.swift */; }; + C01364442C2DD1BA008DB215 /* plistutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364422C2DD1BA008DB215 /* plistutils.swift */; }; + C01364452C2DD1BA008DB215 /* plistutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364422C2DD1BA008DB215 /* plistutils.swift */; }; + C01364462C2E051F008DB215 /* makecatalogslib.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013643B2C2DC529008DB215 /* makecatalogslib.swift */; }; + C013644A2C2F8EFE008DB215 /* GitFileRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364482C2F8EFE008DB215 /* GitFileRepo.swift */; }; + C013644B2C2F8EFE008DB215 /* GitFileRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364482C2F8EFE008DB215 /* GitFileRepo.swift */; }; + C013644E2C30F5D8008DB215 /* RepoFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013644C2C30F5D8008DB215 /* RepoFactory.swift */; }; + C013644F2C30F5D8008DB215 /* RepoFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013644C2C30F5D8008DB215 /* RepoFactory.swift */; }; + C01364522C311DFA008DB215 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C01364512C311DFA008DB215 /* ArgumentParser */; }; + C07A6FA92C2A82B400090743 /* managedsoftwareupdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FA82C2A82B400090743 /* managedsoftwareupdate.swift */; }; + C07A6FB02C2B22A400090743 /* prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FAF2C2B22A400090743 /* prefs.swift */; }; + C07A6FB22C2B22D300090743 /* constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FB12C2B22D300090743 /* constants.swift */; }; + C07A6FBA2C2B5ADE00090743 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FB92C2B5ADE00090743 /* main.swift */; }; + C07A6FBE2C2B5AF000090743 /* prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FAF2C2B22A400090743 /* prefs.swift */; }; + C07A6FBF2C2B5AF400090743 /* constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FB12C2B22D300090743 /* constants.swift */; }; + C07A6FC72C2B5C0700090743 /* makecatalogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FC62C2B5C0700090743 /* makecatalogs.swift */; }; + C07A6FCB2C2B5C3A00090743 /* prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FAF2C2B22A400090743 /* prefs.swift */; }; + C07A6FCC2C2B5C3D00090743 /* constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FB12C2B22D300090743 /* constants.swift */; }; + C07A6FCF2C2B62A600090743 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C07A6FCE2C2B62A600090743 /* ArgumentParser */; }; + C07A6FD22C2B654300090743 /* utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FD12C2B654300090743 /* utils.swift */; }; + C07A6FD32C2B659300090743 /* utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FD12C2B654300090743 /* utils.swift */; }; + C07A6FD42C2B659400090743 /* utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FD12C2B654300090743 /* utils.swift */; }; + C07A6FD72C2B7F2100090743 /* FileRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FD52C2B7F2100090743 /* FileRepo.swift */; }; + C07A6FD82C2B7F2100090743 /* FileRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FD52C2B7F2100090743 /* FileRepo.swift */; }; + C07A6FDA2C2CF19600090743 /* cliutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FD92C2CF19600090743 /* cliutils.swift */; }; + C07A6FDB2C2CF19600090743 /* cliutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FD92C2CF19600090743 /* cliutils.swift */; }; + C07A6FDC2C2CF19600090743 /* cliutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FD92C2CF19600090743 /* cliutils.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + C07A6FA32C2A82B400090743 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; + C07A6FB52C2B5ADE00090743 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; + C07A6FC22C2B5C0700090743 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + C013643B2C2DC529008DB215 /* makecatalogslib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = makecatalogslib.swift; sourceTree = ""; }; + C013643E2C2DCA5C008DB215 /* admincommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = admincommon.swift; sourceTree = ""; }; + C01364422C2DD1BA008DB215 /* plistutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = plistutils.swift; sourceTree = ""; }; + C01364482C2F8EFE008DB215 /* GitFileRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitFileRepo.swift; sourceTree = ""; }; + C013644C2C30F5D8008DB215 /* RepoFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoFactory.swift; sourceTree = ""; }; + C07A6FA52C2A82B400090743 /* managedsoftwareupdate */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = managedsoftwareupdate; sourceTree = BUILT_PRODUCTS_DIR; }; + C07A6FA82C2A82B400090743 /* managedsoftwareupdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = managedsoftwareupdate.swift; sourceTree = ""; }; + C07A6FAF2C2B22A400090743 /* prefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = prefs.swift; sourceTree = ""; }; + C07A6FB12C2B22D300090743 /* constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = constants.swift; sourceTree = ""; }; + C07A6FB72C2B5ADE00090743 /* munkitester */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = munkitester; sourceTree = BUILT_PRODUCTS_DIR; }; + C07A6FB92C2B5ADE00090743 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + C07A6FC42C2B5C0700090743 /* makecatalogs */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = makecatalogs; sourceTree = BUILT_PRODUCTS_DIR; }; + C07A6FC62C2B5C0700090743 /* makecatalogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = makecatalogs.swift; sourceTree = ""; }; + C07A6FD12C2B654300090743 /* utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = utils.swift; sourceTree = ""; }; + C07A6FD52C2B7F2100090743 /* FileRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRepo.swift; sourceTree = ""; }; + C07A6FD92C2CF19600090743 /* cliutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cliutils.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + C07A6FA22C2A82B400090743 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C01364522C311DFA008DB215 /* ArgumentParser in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C07A6FB42C2B5ADE00090743 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C07A6FC12C2B5C0700090743 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C07A6FCF2C2B62A600090743 /* ArgumentParser in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + C013643A2C2DC50E008DB215 /* admin */ = { + isa = PBXGroup; + children = ( + C013643B2C2DC529008DB215 /* makecatalogslib.swift */, + C013643E2C2DCA5C008DB215 /* admincommon.swift */, + ); + path = admin; + sourceTree = ""; + }; + C01364472C2F8EC0008DB215 /* munkirepo */ = { + isa = PBXGroup; + children = ( + C07A6FD52C2B7F2100090743 /* FileRepo.swift */, + C01364482C2F8EFE008DB215 /* GitFileRepo.swift */, + C013644C2C30F5D8008DB215 /* RepoFactory.swift */, + ); + path = munkirepo; + sourceTree = ""; + }; + C01364502C311DFA008DB215 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + C07A6F9C2C2A82B400090743 = { + isa = PBXGroup; + children = ( + C07A6FD02C2B631800090743 /* shared */, + C07A6FA72C2A82B400090743 /* managedsoftwareupdate */, + C07A6FB82C2B5ADE00090743 /* munkitester */, + C07A6FC52C2B5C0700090743 /* makecatalogs */, + C07A6FA62C2A82B400090743 /* Products */, + C01364502C311DFA008DB215 /* Frameworks */, + ); + sourceTree = ""; + }; + C07A6FA62C2A82B400090743 /* Products */ = { + isa = PBXGroup; + children = ( + C07A6FA52C2A82B400090743 /* managedsoftwareupdate */, + C07A6FB72C2B5ADE00090743 /* munkitester */, + C07A6FC42C2B5C0700090743 /* makecatalogs */, + ); + name = Products; + sourceTree = ""; + }; + C07A6FA72C2A82B400090743 /* managedsoftwareupdate */ = { + isa = PBXGroup; + children = ( + C07A6FA82C2A82B400090743 /* managedsoftwareupdate.swift */, + ); + path = managedsoftwareupdate; + sourceTree = ""; + }; + C07A6FB82C2B5ADE00090743 /* munkitester */ = { + isa = PBXGroup; + children = ( + C07A6FB92C2B5ADE00090743 /* main.swift */, + ); + path = munkitester; + sourceTree = ""; + }; + C07A6FC52C2B5C0700090743 /* makecatalogs */ = { + isa = PBXGroup; + children = ( + C07A6FC62C2B5C0700090743 /* makecatalogs.swift */, + ); + path = makecatalogs; + sourceTree = ""; + }; + C07A6FD02C2B631800090743 /* shared */ = { + isa = PBXGroup; + children = ( + C01364472C2F8EC0008DB215 /* munkirepo */, + C013643A2C2DC50E008DB215 /* admin */, + C07A6FAF2C2B22A400090743 /* prefs.swift */, + C07A6FB12C2B22D300090743 /* constants.swift */, + C07A6FD12C2B654300090743 /* utils.swift */, + C07A6FD92C2CF19600090743 /* cliutils.swift */, + C01364422C2DD1BA008DB215 /* plistutils.swift */, + ); + path = shared; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C07A6FA42C2A82B400090743 /* managedsoftwareupdate */ = { + isa = PBXNativeTarget; + buildConfigurationList = C07A6FAC2C2A82B400090743 /* Build configuration list for PBXNativeTarget "managedsoftwareupdate" */; + buildPhases = ( + C07A6FA12C2A82B400090743 /* Sources */, + C07A6FA22C2A82B400090743 /* Frameworks */, + C07A6FA32C2A82B400090743 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = managedsoftwareupdate; + packageProductDependencies = ( + C01364512C311DFA008DB215 /* ArgumentParser */, + ); + productName = munki; + productReference = C07A6FA52C2A82B400090743 /* managedsoftwareupdate */; + productType = "com.apple.product-type.tool"; + }; + C07A6FB62C2B5ADE00090743 /* munkitester */ = { + isa = PBXNativeTarget; + buildConfigurationList = C07A6FBB2C2B5ADE00090743 /* Build configuration list for PBXNativeTarget "munkitester" */; + buildPhases = ( + C07A6FB32C2B5ADE00090743 /* Sources */, + C07A6FB42C2B5ADE00090743 /* Frameworks */, + C07A6FB52C2B5ADE00090743 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = munkitester; + productName = munkitester; + productReference = C07A6FB72C2B5ADE00090743 /* munkitester */; + productType = "com.apple.product-type.tool"; + }; + C07A6FC32C2B5C0700090743 /* makecatalogs */ = { + isa = PBXNativeTarget; + buildConfigurationList = C07A6FC82C2B5C0700090743 /* Build configuration list for PBXNativeTarget "makecatalogs" */; + buildPhases = ( + C07A6FC02C2B5C0700090743 /* Sources */, + C07A6FC12C2B5C0700090743 /* Frameworks */, + C07A6FC22C2B5C0700090743 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = makecatalogs; + packageProductDependencies = ( + C07A6FCE2C2B62A600090743 /* ArgumentParser */, + ); + productName = makecatalogs; + productReference = C07A6FC42C2B5C0700090743 /* makecatalogs */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C07A6F9D2C2A82B400090743 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 1540; + TargetAttributes = { + C07A6FA42C2A82B400090743 = { + CreatedOnToolsVersion = 15.4; + }; + C07A6FB62C2B5ADE00090743 = { + CreatedOnToolsVersion = 15.4; + }; + C07A6FC32C2B5C0700090743 = { + CreatedOnToolsVersion = 15.4; + }; + }; + }; + buildConfigurationList = C07A6FA02C2A82B400090743 /* Build configuration list for PBXProject "munki" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = C07A6F9C2C2A82B400090743; + packageReferences = ( + C07A6FCD2C2B62A600090743 /* XCRemoteSwiftPackageReference "swift-argument-parser" */, + ); + productRefGroup = C07A6FA62C2A82B400090743 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C07A6FA42C2A82B400090743 /* managedsoftwareupdate */, + C07A6FB62C2B5ADE00090743 /* munkitester */, + C07A6FC32C2B5C0700090743 /* makecatalogs */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + C07A6FA12C2A82B400090743 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C07A6FDA2C2CF19600090743 /* cliutils.swift in Sources */, + C01364432C2DD1BA008DB215 /* plistutils.swift in Sources */, + C07A6FB02C2B22A400090743 /* prefs.swift in Sources */, + C07A6FA92C2A82B400090743 /* managedsoftwareupdate.swift in Sources */, + C07A6FB22C2B22D300090743 /* constants.swift in Sources */, + C07A6FD22C2B654300090743 /* utils.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C07A6FB32C2B5ADE00090743 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C01364442C2DD1BA008DB215 /* plistutils.swift in Sources */, + C01364462C2E051F008DB215 /* makecatalogslib.swift in Sources */, + C07A6FDB2C2CF19600090743 /* cliutils.swift in Sources */, + C013644E2C30F5D8008DB215 /* RepoFactory.swift in Sources */, + C07A6FBE2C2B5AF000090743 /* prefs.swift in Sources */, + C07A6FBA2C2B5ADE00090743 /* main.swift in Sources */, + C07A6FBF2C2B5AF400090743 /* constants.swift in Sources */, + C013644A2C2F8EFE008DB215 /* GitFileRepo.swift in Sources */, + C07A6FD32C2B659300090743 /* utils.swift in Sources */, + C07A6FD72C2B7F2100090743 /* FileRepo.swift in Sources */, + C01364402C2DCA5C008DB215 /* admincommon.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C07A6FC02C2B5C0700090743 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C07A6FDC2C2CF19600090743 /* cliutils.swift in Sources */, + C01364452C2DD1BA008DB215 /* plistutils.swift in Sources */, + C07A6FCB2C2B5C3A00090743 /* prefs.swift in Sources */, + C013644F2C30F5D8008DB215 /* RepoFactory.swift in Sources */, + C07A6FC72C2B5C0700090743 /* makecatalogs.swift in Sources */, + C07A6FCC2C2B5C3D00090743 /* constants.swift in Sources */, + C07A6FD42C2B659400090743 /* utils.swift in Sources */, + C013644B2C2F8EFE008DB215 /* GitFileRepo.swift in Sources */, + C07A6FD82C2B7F2100090743 /* FileRepo.swift in Sources */, + C01364412C2DCA5C008DB215 /* admincommon.swift in Sources */, + C013643D2C2DC529008DB215 /* makecatalogslib.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C07A6FAA2C2A82B400090743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + C07A6FAB2C2A82B400090743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + C07A6FAD2C2A82B400090743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 52ZFZKM6BK; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + C07A6FAE2C2A82B400090743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 52ZFZKM6BK; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + C07A6FBC2C2B5ADE00090743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 52ZFZKM6BK; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + C07A6FBD2C2B5ADE00090743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 52ZFZKM6BK; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + C07A6FC92C2B5C0700090743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 52ZFZKM6BK; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + C07A6FCA2C2B5C0700090743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 52ZFZKM6BK; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C07A6FA02C2A82B400090743 /* Build configuration list for PBXProject "munki" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C07A6FAA2C2A82B400090743 /* Debug */, + C07A6FAB2C2A82B400090743 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C07A6FAC2C2A82B400090743 /* Build configuration list for PBXNativeTarget "managedsoftwareupdate" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C07A6FAD2C2A82B400090743 /* Debug */, + C07A6FAE2C2A82B400090743 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C07A6FBB2C2B5ADE00090743 /* Build configuration list for PBXNativeTarget "munkitester" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C07A6FBC2C2B5ADE00090743 /* Debug */, + C07A6FBD2C2B5ADE00090743 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C07A6FC82C2B5C0700090743 /* Build configuration list for PBXNativeTarget "makecatalogs" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C07A6FC92C2B5C0700090743 /* Debug */, + C07A6FCA2C2B5C0700090743 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + C07A6FCD2C2B62A600090743 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-argument-parser.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.4.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + C01364512C311DFA008DB215 /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = C07A6FCD2C2B62A600090743 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; + C07A6FCE2C2B62A600090743 /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = C07A6FCD2C2B62A600090743 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = C07A6F9D2C2A82B400090743 /* Project object */; +} diff --git a/code/cli/munki/munki.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/code/cli/munki/munki.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/code/cli/munki/munki.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/code/cli/munki/munki.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/code/cli/munki/munki.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/code/cli/munki/munki.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/code/cli/munki/munki.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/code/cli/munki/munki.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..8491e517 --- /dev/null +++ b/code/cli/munki/munki.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "59ba1edda695b389d6c9ac1809891cd779e4024f505b0ce1a9d5202b6762e38a", + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version" : "1.4.0" + } + } + ], + "version" : 3 +} diff --git a/code/cli/munki/munkitester/main.swift b/code/cli/munki/munkitester/main.swift new file mode 100644 index 00000000..89c7b0f3 --- /dev/null +++ b/code/cli/munki/munkitester/main.swift @@ -0,0 +1,88 @@ +// +// main.swift +// munkitester +// +// Created by Greg Neagle on 6/25/24. +// + +// this is a temporary target to use to test things + +import Foundation + +/* +do { + let repo = try FileRepo("file:///Users/Shared/munki_repo") + let pkgsinfo = try repo.itemlist("pkgsinfo") + print(pkgsinfo) + let data = try repo.get("manifests/site_default") + if let plist = NSString(data: data, encoding: NSUTF8StringEncoding) { + print(plist) + } + try repo.get("manifests/site_default", toFile: "/tmp/sitr_default") +} catch { + print(error) +} +*/ + + +/* +do { + let repo = try FileRepo("file:///Users/Shared/munki_repo") + print(repo.baseurl) + print(repo.root) + let catalogsmaker = try CatalogsMaker(repo: repo) + let errors = catalogsmaker.makecatalogs() + if !errors.isEmpty { + print(catalogsmaker.errors) + } +} catch { + print(error) +} +*/ + +/* +let options = MakeCatalogOptions( + skip_payload_check: true, + force: false, + verbose: true +) + +do { + let repo = try repoConnect(url: "file:///Users/Shared/munki_repo") + var catalogsmaker = try CatalogsMaker(repo: repo, options: options) + let errors = catalogsmaker.makecatalogs() + if !errors.isEmpty { + for error in errors { + printStderr(error) + } + exit(-1) + } +} catch RepoError.error(let description) { + printStderr("Repo error: \(description)") + exit(-1) +} catch MakeCatalogsError.PkginfoAccessError(let description) { + printStderr("Pkginfo read error: \(description)") + exit(-1) +} catch MakeCatalogsError.CatalogWriteError(let description) { + printStderr("Catalog write error: \(description)") + exit(-1) +} catch { + printStderr("Unexpected error: \(error)") + exit(-1) +} +*/ + + +do { + let repo = try repoConnect( + url: "file:///Users/Shared/munki_repo", + plugin: "GitFileRepo" + ) + let localFilePath = "/Users/Shared/munki_repo/manifests/site_default" + let identifier = "manifests/foo" + try repo.put(identifier, content: Data()) + try repo.put(identifier, fromFile: localFilePath) + try repo.delete(identifier) +} catch { + print(error) +} diff --git a/code/cli/munki/shared/admin/admincommon.swift b/code/cli/munki/shared/admin/admincommon.swift new file mode 100644 index 00000000..bbf7891b --- /dev/null +++ b/code/cli/munki/shared/admin/admincommon.swift @@ -0,0 +1,34 @@ +// +// admincommon.swift +// munki +// +// Created by Greg Neagle on 6/27/24. +// + +import Foundation + +let ADMIN_BUNDLE_ID = "com.googlecode.munki.munkiimport" as CFString + +func adminPref(_ pref_name: String) -> Any? { + /* Return an admin preference. Since this uses CFPreferencesCopyAppValue, + Preferences can be defined several places. Precedence is: + - MCX/configuration profile + - ~/Library/Preferences/ByHost/com.googlecode.munki.munkiimport.XXXXXX.plist + - ~/Library/Preferences/com.googlecode.munki.munkiimport.plist + - /Library/Preferences/com.googlecode.munki.munkiimport.plist + - .GlobalPreferences defined at various levels (ByHost, user, system) + But typically these preferences are _not_ managed and are stored in the + user's preferences (~/Library/Preferences/com.googlecode.munki.munkiimport.plist) + */ + return CFPreferencesCopyAppValue(pref_name as CFString, ADMIN_BUNDLE_ID) +} + +func listItemsOfKind(_ repo: Repo, _ kind: String) throws -> [String] { + // Returns a list of items of kind. Relative pathnames are prepended + // with kind. (example: ["icons/Bar.png", "icons/Foo.png"]) + // Could throw RepoError + let itemlist = try repo.list(kind) + return itemlist.map( + { (kind as NSString).appendingPathComponent($0) } + ) +} diff --git a/code/cli/munki/shared/admin/makecatalogslib.swift b/code/cli/munki/shared/admin/makecatalogslib.swift new file mode 100644 index 00000000..fa6711c7 --- /dev/null +++ b/code/cli/munki/shared/admin/makecatalogslib.swift @@ -0,0 +1,320 @@ +// +// makecatalogslib.swift +// munki +// +// Created by Greg Neagle on 6/27/24. +// + +import Foundation +import CryptoKit + +enum MakeCatalogsError: Error { + case PkginfoAccessError(description: String) + case CatalogWriteError(description: String) +} + +struct MakeCatalogOptions { + var skipPkgCheck: Bool = false + var force: Bool = false + var verbose: Bool = false +} + +struct CatalogsMaker { + var repo: Repo + var options: MakeCatalogOptions + var pkgsinfoList: [String] + var pkgsList: [String] + var catalogs: [String:[PlistDict]] + var errors: [String] + + init(repo: Repo, + options: MakeCatalogOptions = MakeCatalogOptions() ) throws { + self.repo = repo + self.options = options + catalogs = [String:[PlistDict]]() + errors = [String]() + pkgsinfoList = [String]() + pkgsList = [String]() + try getPkgsinfoList() + try getPkgsList() + } + + mutating func getPkgsinfoList() throws { + // returns a list of pkginfo identifiers + do { + pkgsinfoList = try listItemsOfKind(repo, "pkgsinfo") + } catch is RepoError { + throw MakeCatalogsError.PkginfoAccessError( + description: "Error getting list of pkgsinfo items" ) + } + } + + mutating func getPkgsList() throws { + // returns a list of pkg identifiers + do { + pkgsList = try listItemsOfKind(repo, "pkgs") + } catch is RepoError { + throw MakeCatalogsError.PkginfoAccessError( + description: "Error getting list of pkgs items" ) + } + } + + mutating func hashIcons() -> [String:String] { + // Builds a dictionary containing hashes for all our repo icons + if options.verbose { + print("Getting list of icons...") + } + var iconHashes = [String:String]() + if var iconList = try? repo.list("icons") { + // remove plist of hashes from the list + if let index = iconList.firstIndex(of: "_icon_hashes.plist") { + iconList.remove(at: index) + } + for icon in iconList { + if options.verbose { + print("Hashing \(icon)...") + } + do { + let icondata = try repo.get("icons/" + icon) + let hashed = SHA256.hash(data: icondata) + let hashString = hashed.compactMap { String(format: "%02x", $0) }.joined() + iconHashes[icon] = hashString + } catch RepoError.error(let description) { + errors.append("RepoError reading icons/\(icon): \(description)") + } catch { + errors.append("Unexpected error reading icons/\(icon): \(error)") + } + } + } + return iconHashes + } + + func caseInsensitivePkgsListContains(_ installer_item: String) -> String? { + // returns a case-insentitive match for installer_item from pkgsList, if any + for repo_pkg in pkgsList { + if installer_item.lowercased() == repo_pkg.lowercased() { + return repo_pkg + } + } + return nil + } + + mutating func verify(_ identifier: String, _ pkginfo: PlistDict) -> Bool { + // Returns true if referenced installer items are present, + // false otherwise. Updates list of errors. + if let installer_type = pkginfo["installer_type"] as? String { + if ["nopkg", "apple_update_metadata"].contains(installer_type) { + // no associated installer item (pkg) for these types + return true + } + } + if !((pkginfo["PackageCompleteURL"] as? String ?? "").isEmpty) { + // installer item may be on a different server + return true + } + if !((pkginfo["PackageURL"] as? String ?? "").isEmpty) { + // installer item may be on a different server + return true + } + + // Build path to installer item + let installeritemlocation = pkginfo["installer_item_location"] as? String ?? "" + if installeritemlocation.isEmpty { + errors.append( + "WARNING: empty or invalid installer_item_location in \(identifier)") + return false + } + let installeritempath = "pkgs/" + installeritemlocation + + // Check if the installer item actually exists + if !(pkgsList.contains(installeritempath)) { + // didn't find it in the pkgsList; let's look case-insensitive + if let match = caseInsensitivePkgsListContains(installeritempath) { + errors.append( + "WARNING: \(identifier) refers to installer item: \(installeritemlocation). The pathname of the item in the repo has different case: \(match). This may cause issues depending on the case-sensitivity of the underlying filesystem." + ) + } else { + errors.append( + "WARNING: \(identifier) refers to missing installer item: \(installeritemlocation)" + ) + return false + } + } + + // uninstaller checking + if let uninstalleritemlocation = pkginfo["uninstaller_item_location"] as? String { + if uninstalleritemlocation.isEmpty { + errors.append( + "WARNING: empty or invalid uninstaller_item_location in \(identifier)") + return false + } + let uninstalleritempath = "pkgs/" + uninstalleritemlocation + // Check if the uninstaller item actually exists + if !(pkgsList.contains(uninstalleritempath)) { + // didn't find it in the pkgsList; let's look case-insensitive + if let match = caseInsensitivePkgsListContains(uninstalleritempath) { + errors.append( + "WARNING: \(identifier) refers to uninstaller item: \(uninstalleritemlocation). The pathname of the item in the repo has different case: \(match). This may cause issues depending on the case-sensitivity of the underlying filesystem." + ) + } else { + errors.append( + "WARNING: \(identifier) refers to missing uninstaller item: \(uninstalleritemlocation)" + ) + return false + } + } + } + // if we get here we passed all the checks + return true + } + + mutating func processPkgsinfo() { + // Processes pkginfo files and updates catalogs and errors instance variables + catalogs["all"] = [PlistDict]() + // Walk through the pkginfo files + for pkginfoIdentifier in pkgsinfoList { + // Try to read the pkginfo file + var pkginfo = PlistDict() + do { + let data = try repo.get(pkginfoIdentifier) + pkginfo = try readPlistFromData(data) as? PlistDict ?? PlistDict() + } catch { + errors.append("Unexpected error reading \(pkginfoIdentifier): \(error)") + continue + } + if !(pkginfo.keys.contains("name")) { + errors.append("WARNING: \(pkginfoIdentifier)is missing name key") + continue + } + // don't copy admin notes to catalogs + if pkginfo.keys.contains("notes") { + pkginfo["notes"] = nil + } + // strip out any keys that start with "_" + for key in pkginfo.keys { + if key.hasPrefix("_") { + pkginfo[key] = nil + } + } + // sanity checking + if !options.skipPkgCheck { + let verified = verify(pkginfoIdentifier, pkginfo) + if !verified && !options.force { + // Skip this pkginfo unless we're running with force flag + continue + } + } + // append the pkginfo to the relevant catalogs + catalogs["all"]?.append(pkginfo) + if let catalog_list = pkginfo["catalogs"] as? [String] { + if catalog_list.isEmpty { + errors.append("WARNING: \(pkginfoIdentifier)) has an empty catalogs array!") + } else { + for catalog in catalog_list { + if !catalogs.keys.contains(catalog) { + catalogs[catalog] = [PlistDict]() + } + catalogs[catalog]?.append(pkginfo) + if options.verbose { + print("Adding \(pkginfoIdentifier) to \(catalog)...") + } + } + } + } else { + errors.append("WARNING: \(pkginfoIdentifier)) has no catalogs array!") + } + } + // look for catalog names that differ only in case + var duplicateCatalogs = [String]() + for name in catalogs.keys { + let filtered_lowercase_names = catalogs.keys.filter( { $0 != name } ).map( { $0.lowercased() }) + if filtered_lowercase_names.contains(name.lowercased()) { + duplicateCatalogs.append(name) + } + } + if !duplicateCatalogs.isEmpty { + errors.append( + "WARNING: There are catalogs with names that differ only by case. " + + "This may cause issues depending on the case-sensitivity of the " + + "underlying filesystem: \(duplicateCatalogs)") + } + } + + mutating func cleanupCatalogs() { + // clear out old catalogs + do { + let catalogList = try repo.list("catalogs") + for catalogName in catalogList { + if !(catalogs.keys.contains(catalogName)) { + let catalogIdentifier = "catalogs/" + catalogName + do { + try repo.delete(catalogIdentifier) + } catch { + errors.append("Could not delete catalog \(catalogName): \(error)") + } + } + } + } catch { + errors.append("Could not get list of current catalogs to clean up: \(error)") + } + } + + mutating func makecatalogs() -> [String] { + // Assembles all pkginfo files into catalogs. + // User calling this needs to be able to write to the repo/catalogs + // directory. + // Returns a list of any errors it encountered + + // process pkgsinfo items + processPkgsinfo() + + // clean up old catalogs no longer needed + cleanupCatalogs() + + // write the new catalogs + for key in catalogs.keys { + if !(catalogs[key]?.isEmpty ?? true) { + let catalogIdentifier = "catalogs/" + key + do { + if let value = catalogs[key] { + let data = try plistToData(value) + try repo.put(catalogIdentifier, content: data) + if options.verbose { + print("Created \(catalogIdentifier)...") + } + } + } catch PlistError.writeError(let description) { + errors.append("Could not serialize catalog \(key): \(description)") + } catch RepoError.error(let description){ + errors.append("Failed to create catalog \(key): \(description)") + } catch { + errors.append("Unexpected error creating catalog \(key): \(error)") + } + } + } + + // make icon hashes + let iconHashes = hashIcons() + // create icon_hashes resource + if !iconHashes.isEmpty { + let iconHashesIdentifier = "icons/_icon_hashes.plist" + do { + let iconHashesData = try plistToData(iconHashes) + try repo.put(iconHashesIdentifier, content: iconHashesData) + if options.verbose { + print("Created \(iconHashesIdentifier)...") + } + } catch PlistError.writeError(let description) { + errors.append("Could not serialize icon hashes: \(description)") + } catch RepoError.error(let description){ + errors.append("Failed to create \(iconHashesIdentifier): \(description)") + } catch { + errors.append("Unexpected error creating \(iconHashesIdentifier): \(error)") + } + } + return errors + } +} + + + diff --git a/code/cli/munki/shared/cliutils.swift b/code/cli/munki/shared/cliutils.swift new file mode 100644 index 00000000..5a3d1b8b --- /dev/null +++ b/code/cli/munki/shared/cliutils.swift @@ -0,0 +1,83 @@ +// +// cliutils.swift +// munki +// +// Created by Greg Neagle on 6/26/24. +// + +import Foundation + +func printStderr(_ items: Any..., separator: String = " ", terminator: String = "\n") { + // similar to print() function, but prints to stderr + let output = items + .map { String(describing: $0) } + .joined(separator: separator) + terminator + + FileHandle.standardError.write(output.data(using: .utf8)!) +} + +func trimTrailingNewline(_ s: String) -> String { + var trimmedString = s + if trimmedString.last == "\n" { + trimmedString = String(trimmedString.dropLast()) + } + return trimmedString +} + +struct CLIResults { + var exitcode: Int = 0 + var output: String = "" + var error: String = "" +} + +func runCLI(_ tool: String, arguments: [String] = [], stdIn: String = "") -> CLIResults { + // runs a command line tool synchronously, returns CLIResults + // not a good choice for tools that might generate a lot of output or error output + let inPipe = Pipe.init() + let outPipe = Pipe.init() + let errorPipe = Pipe.init() + + let task = Process.init() + task.launchPath = tool + task.arguments = arguments + + task.standardInput = inPipe + task.standardOutput = outPipe + task.standardError = errorPipe + + task.launch() + if stdIn != "" { + if let data = stdIn.data(using: .utf8) { + inPipe.fileHandleForWriting.write(data) + } + } + inPipe.fileHandleForWriting.closeFile() + task.waitUntilExit() + + let outputData = outPipe.fileHandleForReading.readDataToEndOfFile() + let outputString = trimTrailingNewline(String(data: outputData, encoding: .utf8) ?? "") + outPipe.fileHandleForReading.closeFile() + + let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + let errorString = trimTrailingNewline(String(data: errorData, encoding: .utf8) ?? "") + errorPipe.fileHandleForReading.closeFile() + + return (CLIResults( + exitcode: Int(task.terminationStatus), + output: outputString, + error: errorString) + ) +} + +enum CalledProcessError: Error { + case error(description: String) +} + +func checkCall(_ tool: String, arguments: [String] = [], stdIn: String = "") throws -> String { + // like Python's subprocess.check_call + let result = runCLI(tool, arguments: arguments, stdIn: stdIn) + if result.exitcode != 0 { + throw CalledProcessError.error(description: result.error) + } + return result.output +} diff --git a/code/cli/munki/shared/constants.swift b/code/cli/munki/shared/constants.swift new file mode 100644 index 00000000..e2a31d84 --- /dev/null +++ b/code/cli/munki/shared/constants.swift @@ -0,0 +1,42 @@ +// +// constants.swift +// munki +// +// Created by Greg Neagle on 6/25/24. +// + +import Foundation + +// NOTE: it's very important that defined exit codes are never changed! +// Preflight exit codes +let EXIT_STATUS_PREFLIGHT_FAILURE = 1 +// Client config exit codes. +let EXIT_STATUS_OBJC_MISSING = 100 // no longer relevant +let EXIT_STATUS_MUNKI_DIRS_FAILURE = 101 +// Server connection exit codes. +let EXIT_STATUS_SERVER_UNAVAILABLE = 150 +// User related exit codes. +let EXIT_STATUS_INVALID_PARAMETERS = 200 +let EXIT_STATUS_ROOT_REQUIRED = 201 + +let BUNDLE_ID = "ManagedInstalls" as CFString +let DEFAULT_GUI_CACHE_AGE_SECS = 3600 +let WRITEABLE_SELF_SERVICE_MANIFEST_PATH = "/Users/Shared/.SelfServeManifest" + +let ADDITIONAL_HTTP_HEADERS_KEY = "AdditionalHttpHeaders" + +let LOGINWINDOW = "/System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow" + +let CHECKANDINSTALLATSTARTUPFLAG = "/Users/Shared/.com.googlecode.munki.checkandinstallatstartup" +let INSTALLATSTARTUPFLAG = "/Users/Shared/.com.googlecode.munki.installatstartup" +let INSTALLATLOGOUTFLAG = "/private/tmp/com.googlecode.munki.installatlogout" +let UPDATECHECKLAUNCHFILE = "/private/tmp/.com.googlecode.munki.updatecheck.launchd" +let INSTALLWITHOUTLOGOUTFILE = "/private/tmp/.com.googlecode.munki.managedinstall.launchd" + +// postinstall actions +let POSTACTION_NONE = 0 +let POSTACTION_LOGOUT = 1 +let POSTACTION_RESTART = 2 +let POSTACTION_SHUTDOWN = 4 + +typealias PlistDict = [String:Any] diff --git a/code/cli/munki/shared/munkirepo/FileRepo.swift b/code/cli/munki/shared/munkirepo/FileRepo.swift new file mode 100644 index 00000000..884a3cb3 --- /dev/null +++ b/code/cli/munki/shared/munkirepo/FileRepo.swift @@ -0,0 +1,282 @@ +// +// munkirepo.swift +// munki +// +// Created by Greg Neagle on 6/25/24. +// + +import Foundation +import NetFS + +// Base classes +enum RepoError: Error { + /// General error class for repo errors + case error(description: String) +} + +protocol Repo { + // Defines methods all repo classes must implement + init(_ url: String) throws + func list(_ kind: String) throws -> [String] + func get(_ identifier: String) throws -> Data + func get(_ identifier: String, toFile local_file_path: String) throws + func put(_ identifier: String, content: Data) throws + func put(_ identifier: String, fromFile local_file_path: String) throws + func delete(_ identifier: String) throws +} + +// utility funcs +func isDir(_ path: String) -> Bool { + let filemanager = FileManager.default + do { + let fileType = (try filemanager.attributesOfItem(atPath: path) as NSDictionary).fileType() + return fileType == FileAttributeType.typeDirectory.rawValue + } catch { + return false + } +} + +// MARK: share mounting functions +// NetFS error codes +/* + * ENETFSPWDNEEDSCHANGE -5045 + * ENETFSPWDPOLICY -5046 + * ENETFSACCOUNTRESTRICTED -5999 + * ENETFSNOSHARESAVAIL -5998 + * ENETFSNOAUTHMECHSUPP -5997 + * ENETFSNOPROTOVERSSUPP -5996 + * + * from + * kNetAuthErrorInternal -6600 + * kNetAuthErrorMountFailed -6602 + * kNetAuthErrorNoSharesAvailable -6003 + * kNetAuthErrorGuestNotSupported -6004 + * kNetAuthErrorAlreadyClosed -6005 + */ + +enum ShareMountError: Error { + case generalError(Int32) + case authorizationNeeded(Int32) +} + +func mountShare(_ shareURL: String, username: String = "", password: String = "") throws -> String { + // Mounts a share at /Volumes, optionally using credentials. + // Returns the mount point or throws an error + let cfShareURL = CFURLCreateWithString(nil, shareURL as CFString, nil) + // Set UI to reduced interaction + let open_options: NSMutableDictionary = [kNAUIOptionKey: kNAUIOptionNoUI] + // Allow mounting sub-directories of root shares + let mount_options: NSMutableDictionary = [kNetFSAllowSubMountsKey: true] + var mountpoints: Unmanaged? = nil + var result: Int32 = 0 + if !username.isEmpty { + result = NetFSMountURLSync(cfShareURL, nil, username as CFString, password as CFString, open_options as CFMutableDictionary, mount_options as CFMutableDictionary, &mountpoints) + } else { + result = NetFSMountURLSync(cfShareURL, nil, nil, nil, open_options as CFMutableDictionary, mount_options as CFMutableDictionary, &mountpoints) + } + // Check if it worked + if result != 0 { + if [-6600, EINVAL, ENOTSUP, EAUTH].contains(result) { + // -6600 is kNetAuthErrorInternal in NetFS.h 10.9+ + // EINVAL is returned if an afp share needs a login in some versions of macOS + // ENOTSUP is returned if an afp share needs a login in some versions of macOS + // EAUTH is returned if authentication fails (SMB for sure) + throw ShareMountError.authorizationNeeded(result) + } + throw ShareMountError.generalError(result) + } + let mounts = (mountpoints?.takeUnretainedValue()) as! [CFString] + return mounts[0] as String +} + +func mountShareURL(_ share_url: String) throws -> String { + do { + return try mountShare(share_url) + } catch ShareMountError.authorizationNeeded { + //pass + } catch { + throw error + } + var username = "" + print("Username: ", terminator: "") + if let input = readLine(strippingNewline: true) { + username = input + } + var password = "" + if let input = getpass("Password: ") { + password = String(cString: input, encoding: .utf8) ?? "" + } + return try mountShare(share_url, username: username, password: password) +} + + +// MARK: File repo class +class FileRepo: Repo { + // MARK: instance variables + var baseurl: String + var urlScheme: String + var root: String + var weMountedTheRepo: Bool + + // MARK: init/deinit + required init(_ url: String) throws { + baseurl = url + urlScheme = NSURL(string: url)?.scheme ?? "" + root = "" + if urlScheme == "file" { + root = NSURL(string: url)?.path ?? "" + } else { + // repo is on a fileshare that will be mounted under /Volumes + root = "/Volumes" + (NSURL(string: url)?.path ?? "") + } + weMountedTheRepo = false + try _connect() + } + + deinit { + // Destructor -- unmount the fileshare if we mounted it + if weMountedTheRepo && isDir(root) { + print("Attempting to unmount \(root)...") + let results = runCLI( + "/usr/sbin/diskutil", arguments: ["unmount", root]) + if results.exitcode == 0 { + print(results.output) + } else { + print("Exit code: \(results.exitcode)") + printStderr(results.error) + } + } + } + + // MARK: utility methods + func fullPath(_ identifier: String) -> String { + // returns the full (absolute) filesystem path to identifier + return (root as NSString).appendingPathComponent(identifier) + } + + func parentDir(_ identifier: String) -> String { + // returns the filesystem path to the parent dir of identifier + return (fullPath(identifier) as NSString).deletingLastPathComponent + } + + private func _connect() throws { + // If self.root is present, return. Otherwise, if the url scheme is not + // "file" then try to mount the share url. + if isDir(root) { + return + } + if urlScheme != "file" { + do { + print("Attempting to mount fileshare \(baseurl)...") + root = try mountShareURL(baseurl) + weMountedTheRepo = true + } catch is ShareMountError { + throw RepoError.error(description: "Error mounting repo file share") + } + } + // does root dir exist now? + if !isDir(root) { + throw RepoError.error(description: "Repo path does not exist") + } + } + + // MARK: API methods + func list(_ kind: String) throws -> [String] { + // Returns a list of identifiers for each item of kind. + // Kind might be 'catalogs', 'manifests', 'pkgsinfo', 'pkgs', or 'icons'. + // For a file-backed repo this would be a list of pathnames. + var fileList = [String]() + let searchPath = (root as NSString).appendingPathComponent(kind) + let filemanager = FileManager.default + let dirEnum = filemanager.enumerator(atPath: searchPath) + while let file = dirEnum?.nextObject() as? String { + let fullpath = (searchPath as NSString).appendingPathComponent(file) + if !isDir(fullpath) { + let basename = (file as NSString).lastPathComponent + if !basename.hasPrefix(".") { + fileList.append(file) + } + } + } + return fileList + } + + func get(_ identifier: String) throws -> Data { + // Returns the content of item with given resource_identifier. + // For a file-backed repo, a resource_identifier of + // 'pkgsinfo/apps/Firefox-52.0.plist' would return the contents of + // /pkgsinfo/apps/Firefox-52.0.plist. + // Avoid using this method with the 'pkgs' kind as it might return a + // really large blob of data. + let repoFilePath = fullPath(identifier) + if let data = FileManager.default.contents(atPath: repoFilePath) { + return data + } + throw RepoError.error(description: "Error getting contents from \(repoFilePath)") + } + + func get(_ identifier: String, toFile local_file_path: String) throws { + // Gets the contents of item with given resource_identifier and saves + // it to local_file_path. + // For a file-backed repo, a resource_identifier + // of 'pkgsinfo/apps/Firefox-52.0.plist' would copy the contents of + // /pkgsinfo/apps/Firefox-52.0.plist to a local file given by + // local_file_path. + // TODO: make this atomic + let filemanager = FileManager.default + if filemanager.fileExists(atPath: local_file_path) { + try filemanager.removeItem(atPath: local_file_path) + } + try filemanager.copyItem(atPath: fullPath(identifier), toPath: local_file_path) + } + + func put(_ identifier: String, content: Data) throws { + // Stores content on the repo based on resource_identifier. + // For a file-backed repo, a resource_identifier of + // 'pkgsinfo/apps/Firefox-52.0.plist' would result in the content being + // saved to /pkgsinfo/apps/Firefox-52.0.plist. + let filemanager = FileManager.default + let dirPath = parentDir(identifier) + if !filemanager.fileExists(atPath: dirPath) { + try filemanager.createDirectory( + atPath: dirPath, + withIntermediateDirectories: true, + attributes: [.posixPermissions: 0o755] + ) + } + if !((content as NSData).write(toFile: fullPath(identifier), atomically: true)) { + throw RepoError.error(description: "write failed") + } + } + + func put(_ identifier: String, fromFile localFilePath: String) throws { + // Copies the content of local_file_path to the repo based on + // resource_identifier. For a file-backed repo, a resource_identifier + // of 'pkgsinfo/apps/Firefox-52.0.plist' would result in the content + // being saved to /pkgsinfo/apps/Firefox-52.0.plist. + let filemanager = FileManager.default + let dirPath = parentDir(identifier) + if !filemanager.fileExists(atPath: dirPath) { + try filemanager.createDirectory( + atPath: dirPath, + withIntermediateDirectories: true, + attributes: [.posixPermissions: 0o755] + ) + } + let repoFilePath = fullPath(identifier) + if filemanager.fileExists(atPath: repoFilePath) { + // if file already exists, we have to remove it first + try filemanager.removeItem(atPath: repoFilePath) + } + try filemanager.copyItem(atPath: localFilePath, toPath: repoFilePath) + } + + func delete(_ identifier: String) throws { + // Deletes a repo object located by resource_identifier. + // For a file-backed repo, a resource_identifier of + // 'pkgsinfo/apps/Firefox-52.0.plist' would result in the deletion of + // /pkgsinfo/apps/Firefox-52.0.plist. + try FileManager.default.removeItem(atPath: fullPath(identifier)) + } + +} diff --git a/code/cli/munki/shared/munkirepo/GitFileRepo.swift b/code/cli/munki/shared/munkirepo/GitFileRepo.swift new file mode 100644 index 00000000..b2874a6e --- /dev/null +++ b/code/cli/munki/shared/munkirepo/GitFileRepo.swift @@ -0,0 +1,133 @@ +// +// GitFileRepo.swift +// munki +// +// Created by Greg Neagle on 6/28/24. +// + +import Foundation + +class GitFileRepo: FileRepo { + // A subclass of FileRepo that also does git commits for repo changes + // MARK: instance variables + var cmd: String + + // MARK: override init + required init(_ url: String) throws { + // try to get path to git binary from admin prefs or use default path + cmd = adminPref("GitBinaryPath") as? String ?? "/usr/bin/git" + // init the rest + try super.init(url) + } + + // MARK: git functions + func runGit(args: [String] = []) -> CLIResults { + return runCLI(cmd, arguments: args) + } + + func isGitIgnored(_ identifier: String) -> Bool { + // Returns True if file referred to by identifer will be ignored by Git + // (usually due to being in a .gitignore file) + let results = runGit( + args: ["-C", parentDir(identifier), + "check-ignore", fullPath(identifier)] + ) + return results.exitcode == 0 + } + + func isInGitRepo(_ identifier: String) -> Bool { + // Returns True if file referred to by identifer is in a Git repo, false otherwise. + let results = runGit( + args: ["-C", parentDir(identifier), + "status", "-z", fullPath(identifier)] + ) + return results.exitcode == 0 + } + + func gitCommit(_ identifier: String) { + // Commits the file referred to be identifier. This method will also automatically + // generate the commit log appropriate for the status of the file where + // status would be 'modified', 'new file', or 'deleted' + + // figure out the name of the tool in use + let processPath = ProcessInfo.processInfo.arguments[0] + let toolname = (processPath as NSString).lastPathComponent + + // get the current username + let username = NSUserName() + + // get the status of file at path + let statusResults = runGit( + args:["-C", parentDir(identifier), + "status", "-s", fullPath(identifier)] + ) + var action = "" + if statusResults.output.hasPrefix("A") { + action = "added" + } else if statusResults.output.hasPrefix("D") { + action = "deleted" + } else if statusResults.output.hasPrefix("M") { + action = "modified" + } else { + action = "made unexpected change to" + } + + // generate the log message + let logMessage = "\(username) \(action) '\(identifier)' via \(toolname)" + // do the commit + print("Doing git commit: \(logMessage)") + let results = runGit( + args: ["-C", parentDir(identifier), + "commit", "-m", logMessage] + ) + if results.exitcode != 0 { + printStderr("Failed to commit changes to \(identifier)") + printStderr(results.error) + } + } + + func _gitAddOrRemove(_ identifier: String, _ operation: String) { + // Does a git add or rm of a file at path. "operation" must be either "add" or "rm" + if isInGitRepo(identifier) { + if !isGitIgnored(identifier) { + let results = runGit( + args: ["-C", parentDir(identifier), + operation, fullPath(identifier)] + ) + if results.exitcode == 0 { + gitCommit(identifier) + } else { + printStderr("git error: \(results.error)") + } + } + } else { + printStderr("\(identifier) is not in a git repo.") + } + } + + func gitAdd(_ identifier: String) { + // Adds and commits file at path + _gitAddOrRemove(identifier, "add") + } + + func gitDelete(_ identifier: String) { + // Deletes file at path and commits the result + _gitAddOrRemove(identifier, "rm") + } + + // MARK: override FileRepo API methods + override func put(_ identifier: String, content: Data) throws { + try super.put(identifier, content: content) + gitAdd(identifier) + } + + override func put(_ identifier: String, fromFile local_path: String) throws { + try super.put(identifier, fromFile: local_path) + gitAdd(identifier) + } + + override func delete(_ identifier: String) throws { + try super.delete(identifier) + gitDelete(identifier) + } +} diff --git a/code/cli/munki/shared/munkirepo/RepoFactory.swift b/code/cli/munki/shared/munkirepo/RepoFactory.swift new file mode 100644 index 00000000..188d7b16 --- /dev/null +++ b/code/cli/munki/shared/munkirepo/RepoFactory.swift @@ -0,0 +1,20 @@ +// +// RepoFactory.swift +// munki +// +// Created by Greg Neagle on 6/29/24. +// + +import Foundation + +func repoConnect(url: String, plugin: String = "FileRepo") throws -> Repo { + // Factory function that returns an instance of a specific Repo class + switch plugin { + case "FileRepo": + return try FileRepo(url) + case "GitFileRepo": + return try GitFileRepo(url) + default: + throw RepoError.error(description: "No repo plugin named \"\(plugin)\"") + } +} diff --git a/code/cli/munki/shared/plistutils.swift b/code/cli/munki/shared/plistutils.swift new file mode 100644 index 00000000..dc5585c1 --- /dev/null +++ b/code/cli/munki/shared/plistutils.swift @@ -0,0 +1,76 @@ +// +// plistutils.swift +// munki +// +// Created by Greg Neagle on 6/27/24. + +import Foundation + +enum PlistError: Error { + case readError(description: String) + case writeError(description: String) +} + +func deserialize(_ data: Data?) throws -> Any? { + if data != nil { + do { + let dataObject = try PropertyListSerialization.propertyList( + from: data!, + options: PropertyListSerialization.MutabilityOptions.mutableContainers, + format: nil) + return dataObject + } catch { + throw PlistError.readError(description: "\(error)") + } + } + return nil +} + +func readPlist(_ filepath: String) throws -> Any? { + return try deserialize(NSData(contentsOfFile: filepath) as Data?) +} + +func readPlistFromData(_ data: Data) throws -> Any? { + return try deserialize(data) +} + +func readPlistFromString(_ stringData: String) throws -> Any? { + return try deserialize(stringData.data(using: String.Encoding.utf8)) +} + +func serialize(_ plist: Any) throws -> Data { + do { + let plistData = try PropertyListSerialization.data( + fromPropertyList: plist, + format: PropertyListSerialization.PropertyListFormat.xml, + options: 0) + return plistData + } catch { + throw PlistError.writeError(description: "\(error)") + } +} + +func writePlist(_ dataObject: Any, toFile filepath: String) throws { + do { + let data = try serialize(dataObject) as NSData + if !(data.write(toFile: filepath, atomically: true)) { + throw PlistError.writeError(description: "write failed") + } + } catch { + throw PlistError.writeError(description: "\(error)") + } +} + +func plistToData(_ dataObject: Any) throws -> Data { + return try serialize(dataObject) +} + +func plistToString(_ dataObject: Any) throws -> String { + do { + let data = try serialize(dataObject) + return String(data: data, encoding: String.Encoding.utf8)! + } catch { + throw PlistError.writeError(description: "\(error)") + } +} + diff --git a/code/cli/munki/shared/prefs.swift b/code/cli/munki/shared/prefs.swift new file mode 100644 index 00000000..9d940291 --- /dev/null +++ b/code/cli/munki/shared/prefs.swift @@ -0,0 +1,291 @@ +// +// prefs.swift +// munki +// +// Created by Greg Neagle on 6/25/24. +// + +import Foundation + +let DEFAULT_INSECURE_REPO_URL = "http://munki/repo" + +// unlike the previous Python implementation, we define default +// preference values only if they are not None/nil +let DEFAULT_PREFS: [String: Any] = [ + //"AdditionalHttpHeaders": None, + "AggressiveUpdateNotificationDays": 14, + "AppleSoftwareUpdatesIncludeMajorOSUpdates": false, + "AppleSoftwareUpdatesOnly": false, + //"CatalogURL": None, + //"ClientCertificatePath": None, + "ClientIdentifier": "", + //"ClientKeyPath": None, + //"ClientResourcesFilename": None, + //"ClientResourceURL": None, + "DaysBetweenNotifications": 1, + "EmulateProfileSupport": false, + "FollowHTTPRedirects": "none", + //"HelpURL": None, + //"IconURL": None, + "IgnoreMiddleware": false, + "IgnoreSystemProxies": false, + "InstallRequiresLogout": false, + "InstallAppleSoftwareUpdates": false, + "LastNotifiedDate": NSDate.init(timeIntervalSince1970: 0), + //"LocalOnlyManifest": None, + "LogFile": "/Library/Managed Installs/Logs/ManagedSoftwareUpdate.log", + "LoggingLevel": 1, + "LogToSyslog": false, + "ManagedInstallDir": "/Library/Managed Installs", + //"ManifestURL": None, + //"PackageURL": None, + "PackageVerificationMode": "hash", + "PerformAuthRestarts": false, + //"RecoveryKeyFile": None, + "ShowOptionalInstallsForHigherOSVersions": false, + //"SoftwareRepoCACertificate": None, + //"SoftwareRepoCAPath": None, + "SoftwareRepoURL": DEFAULT_INSECURE_REPO_URL, + //"SoftwareUpdateServerURL": None, + "SuppressAutoInstall": false, + "SuppressLoginwindowInstall": false, + "SuppressStopButtonOnInstall": false, + "SuppressUserNotification": false, + "UnattendedAppleUpdates": false, + "UseClientCertificate": false, + "UseClientCertificateCNAsClientIdentifier": false, + "UseNotificationCenterDays": 3, +] + +// and since we don't define default values if they are None/nil +// we need a list of keynames we will display for --show-config +let CONFIG_KEY_NAMES = [ + "AdditionalHttpHeaders", + "AggressiveUpdateNotificationDays", + "AppleSoftwareUpdatesIncludeMajorOSUpdates", + "AppleSoftwareUpdatesOnly", + "CatalogURL", + "ClientCertificatePath", + "ClientIdentifier", + "ClientKeyPath", + "ClientResourcesFilename", + "ClientResourceURL", + "DaysBetweenNotifications", + "EmulateProfileSupport", + "FollowHTTPRedirects", + "HelpURL", + "IconURL", + "IgnoreMiddleware", + "IgnoreSystemProxies", + "InstallRequiresLogout", + "InstallAppleSoftwareUpdates", + "LocalOnlyManifest", + "LogFile", + "LoggingLevel", + "LogToSyslog", + "ManagedInstallDir", + "ManifestURL", + "PackageURL", + "PackageVerificationMode", + "PerformAuthRestarts", + "RecoveryKeyFile", + "ShowOptionalInstallsForHigherOSVersions", + "SoftwareRepoCACertificate", + "SoftwareRepoCAPath", + "SoftwareRepoURL", + "SoftwareUpdateServerURL", + "SuppressAutoInstall", + "SuppressLoginwindowInstall", + "SuppressStopButtonOnInstall", + "SuppressUserNotification", + "UnattendedAppleUpdates", + "UseClientCertificate", + "UseClientCertificateCNAsClientIdentifier", + "UseNotificationCenterDays", +] + + +func reloadPrefs() { + /* Uses CFPreferencesAppSynchronize(BUNDLE_ID) + to make sure we have the latest prefs. Call this + if you have modified /Library/Preferences/ManagedInstalls.plist + or /var/root/Library/Preferences/ManagedInstalls.plist directly */ + CFPreferencesAppSynchronize(BUNDLE_ID) +} + + +func setPref(_ prefName: String, _ prefValue: Any) { + /* Sets a preference, writing it to + /Library/Preferences/ManagedInstalls.plist. + This should normally be used only for 'bookkeeping' values; + values that control the behavior of munki may be overridden + elsewhere (by MCX, for example) */ + if let key = prefName as CFString? { + if let value = prefValue as CFPropertyList? { + CFPreferencesSetValue( + key, value, BUNDLE_ID, + kCFPreferencesAnyUser, kCFPreferencesCurrentHost) + CFPreferencesAppSynchronize(BUNDLE_ID) + } else { + // raise error about illegal value? + } + } else { + // raise error about illegal key? + } +} + + +func pref(_ prefName: String) -> Any? { + /* Return a preference. Since this uses CFPreferencesCopyAppValue, + Preferences can be defined several places. Precedence is: + - MCX/configuration profile + - /var/root/Library/Preferences/ByHost/ManagedInstalls.XXXXXX.plist + - /var/root/Library/Preferences/ManagedInstalls.plist + - /Library/Preferences/ManagedInstalls.plist + - .GlobalPreferences defined at various levels (ByHost, user, system) + - default_prefs defined here. + */ + var prefValue: Any? + prefValue = CFPreferencesCopyAppValue(prefName as CFString, BUNDLE_ID) + if prefValue == nil { + if let defaultValue = DEFAULT_PREFS[prefName] { + prefValue = defaultValue + // we're using a default value. We'll write it out to + // /Library/Preferences/.plist for admin discoverability + setPref(prefName, defaultValue) + } + } + // prior Python implementation converted dates to strings; we won't do that + /*if isinstance(pref_value, NSDate): + # convert NSDate/CFDates to strings + pref_value = str(pref_value)*/ + return prefValue +} + + +struct prefsDomain { + var file: String + var domain: CFString + var user: CFString + var host: CFString +} + + +func isEqual(_ a: CFPropertyList, _ b: CFPropertyList) -> Bool { + // attempt to compare two CFPropertyList objects that are actually one of: + // String, Number, Boolean, Date + if let aString = a as? String, let bString = b as? String { + return aString == bString + } + if let aNumber = a as? NSNumber, let bNumber = b as? NSNumber { + return aNumber == bNumber + } + if let aDate = a as? NSDate, let bDate = b as? NSDate { + return aDate == bDate + } + return false +} + + +func getConfigLevel(_ domain: String, _ prefName: String, _ value: Any?) -> String { + // Returns a string indicating where the given preference is defined + if value == nil { + return "[not set]" + } + if CFPreferencesAppValueIsForced(prefName as CFString, domain as CFString) { + return "[MANAGED]" + } + // define all the places we need to search, in priority order + let levels: [prefsDomain] = [ + prefsDomain( + file: "/var/root/Library/Preferences/ByHost/\(domain).xxxx.plist", + domain: domain as CFString, + user: kCFPreferencesCurrentUser, + host: kCFPreferencesCurrentHost + ), + prefsDomain( + file: "/var/root/Library/Preferences/\(domain).plist", + domain: domain as CFString, + user: kCFPreferencesCurrentUser, + host: kCFPreferencesAnyHost + ), + prefsDomain( + file: "/var/root/Library/Preferences/ByHost/.GlobalPreferences.xxxx.plist", + domain: ".GlobalPreferences" as CFString, + user: kCFPreferencesCurrentUser, + host: kCFPreferencesCurrentHost + ), + prefsDomain( + file: "/var/root/Library/Preferences/.GlobalPreferences.plist", + domain: ".GlobalPreferences" as CFString, + user: kCFPreferencesCurrentUser, + host: kCFPreferencesAnyHost + ), + prefsDomain( + file: "/Library/Preferences/\(domain).plist", + domain: domain as CFString, + user: kCFPreferencesAnyUser, + host: kCFPreferencesCurrentHost + ), + prefsDomain( + file: "/Library/Preferences/.GlobalPreferences.plist", + domain: ".GlobalPreferences" as CFString, + user: kCFPreferencesAnyUser, + host: kCFPreferencesCurrentHost + ) + ] + for level in levels { + if let levelValue = CFPreferencesCopyValue( + prefName as CFString, + level.domain, + level.user, + level.host + ) { + if let ourValue = value as? CFPropertyList { + if isEqual(ourValue, levelValue) { + return "[\(level.file)]" + } + } + } + } + if let value = value as? CFPropertyList, let defaultValue = DEFAULT_PREFS["pref_name"] as? CFPropertyList { + if isEqual(value, defaultValue) { + return "[default]" + } + } + return "[unknown]" +} + + +func printConfig() { + // Prints the current Munki configuration + print("Current Munki configuration:") + let maxPrefNameLen = CONFIG_KEY_NAMES.max(by: {$1.count > $0.count})?.count ?? 0 + let padding = " " + for prefName in CONFIG_KEY_NAMES.sorted() { + let value = pref(prefName) + let level = getConfigLevel(BUNDLE_ID as String, prefName, value) + var reprValue = "None" + // it's hard to distinguish a boolean from a number in a CFPropertyList item, so + // we look at the type of the default value if defined + if let numberValue = value as? NSNumber { + if DEFAULT_PREFS[prefName] is Bool { + if numberValue != 0 { + reprValue = "True" + } else { + reprValue = "False" + } + } else { + reprValue = "\(numberValue)" + } + } else if let stringValue = value as? String { + reprValue = "\"\(stringValue)\"" + } else if let arrayValue = value as? NSArray { + reprValue = "\(arrayValue)" + } + //print(('%' + str(max_pref_name_len) + 's: %5s %s ') % ( + // pref_name, repr_value, level)) + let paddedPrefName = (padding + prefName).suffix(maxPrefNameLen) + print("\(paddedPrefName): \(reprValue) \(level)") + } +} diff --git a/code/cli/munki/shared/utils.swift b/code/cli/munki/shared/utils.swift new file mode 100644 index 00000000..70f2295f --- /dev/null +++ b/code/cli/munki/shared/utils.swift @@ -0,0 +1,14 @@ +// +// utils.swift +// managedsoftwareupdate +// +// Created by Greg Neagle on 6/25/24. +// + +import Foundation + +func getVersion() -> String { + // TODO: actually read this from a file + // or figure out a way to update this at build time + return "0.0.1" +}