Initial Swfit code for CLI tools; makecatalogs should be functional

This commit is contained in:
Greg Neagle
2024-06-30 15:06:43 -07:00
parent 76e7f4b7e8
commit 97771e4528
18 changed files with 2184 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
# .DS_Store files!
.DS_Store
# Xcode user data
*.xcodeproj/project.xcworkspace/
*.xcodeproj/xcuserdata/
@@ -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] [<repo_path>]"
)
@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 <repo_path>!")
}
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)
}
}
}
@@ -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!")
}
}
@@ -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 = "<group>"; };
C013643E2C2DCA5C008DB215 /* admincommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = admincommon.swift; sourceTree = "<group>"; };
C01364422C2DD1BA008DB215 /* plistutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = plistutils.swift; sourceTree = "<group>"; };
C01364482C2F8EFE008DB215 /* GitFileRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitFileRepo.swift; sourceTree = "<group>"; };
C013644C2C30F5D8008DB215 /* RepoFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoFactory.swift; sourceTree = "<group>"; };
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 = "<group>"; };
C07A6FAF2C2B22A400090743 /* prefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = prefs.swift; sourceTree = "<group>"; };
C07A6FB12C2B22D300090743 /* constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = constants.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
C07A6FD12C2B654300090743 /* utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = utils.swift; sourceTree = "<group>"; };
C07A6FD52C2B7F2100090743 /* FileRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRepo.swift; sourceTree = "<group>"; };
C07A6FD92C2CF19600090743 /* cliutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cliutils.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
C01364472C2F8EC0008DB215 /* munkirepo */ = {
isa = PBXGroup;
children = (
C07A6FD52C2B7F2100090743 /* FileRepo.swift */,
C01364482C2F8EFE008DB215 /* GitFileRepo.swift */,
C013644C2C30F5D8008DB215 /* RepoFactory.swift */,
);
path = munkirepo;
sourceTree = "<group>";
};
C01364502C311DFA008DB215 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
C07A6F9C2C2A82B400090743 = {
isa = PBXGroup;
children = (
C07A6FD02C2B631800090743 /* shared */,
C07A6FA72C2A82B400090743 /* managedsoftwareupdate */,
C07A6FB82C2B5ADE00090743 /* munkitester */,
C07A6FC52C2B5C0700090743 /* makecatalogs */,
C07A6FA62C2A82B400090743 /* Products */,
C01364502C311DFA008DB215 /* Frameworks */,
);
sourceTree = "<group>";
};
C07A6FA62C2A82B400090743 /* Products */ = {
isa = PBXGroup;
children = (
C07A6FA52C2A82B400090743 /* managedsoftwareupdate */,
C07A6FB72C2B5ADE00090743 /* munkitester */,
C07A6FC42C2B5C0700090743 /* makecatalogs */,
);
name = Products;
sourceTree = "<group>";
};
C07A6FA72C2A82B400090743 /* managedsoftwareupdate */ = {
isa = PBXGroup;
children = (
C07A6FA82C2A82B400090743 /* managedsoftwareupdate.swift */,
);
path = managedsoftwareupdate;
sourceTree = "<group>";
};
C07A6FB82C2B5ADE00090743 /* munkitester */ = {
isa = PBXGroup;
children = (
C07A6FB92C2B5ADE00090743 /* main.swift */,
);
path = munkitester;
sourceTree = "<group>";
};
C07A6FC52C2B5C0700090743 /* makecatalogs */ = {
isa = PBXGroup;
children = (
C07A6FC62C2B5C0700090743 /* makecatalogs.swift */,
);
path = makecatalogs;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -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
}
+88
View File
@@ -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)
}
@@ -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) }
)
}
@@ -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
}
}
+83
View File
@@ -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
}
+42
View File
@@ -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]
@@ -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 <NetAuth/NetAuthErrors.h>
* 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<CFArray>? = 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
// <repo_root>/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
// <repo_root>/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 <repo_root>/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 <repo_root>/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
// <repo_root>/pkgsinfo/apps/Firefox-52.0.plist.
try FileManager.default.removeItem(atPath: fullPath(identifier))
}
}
@@ -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)
}
}
@@ -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)\"")
}
}
+76
View File
@@ -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)")
}
}
+291
View File
@@ -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/<BUNDLE_ID>.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)")
}
}
+14
View File
@@ -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"
}