From b862182f5d202590e9f129b6d384a96769926abe Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Wed, 15 Oct 2025 09:16:24 -0700 Subject: [PATCH 01/11] Explictly add a version_comparison_key of CFBundleVersion to an installs item if the item does not have a CFBundleShortVersionString. Addresses #1277 --- code/cli/munki/shared/admin/pkginfolib.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/cli/munki/shared/admin/pkginfolib.swift b/code/cli/munki/shared/admin/pkginfolib.swift index fc43116c..de1e8502 100644 --- a/code/cli/munki/shared/admin/pkginfolib.swift +++ b/code/cli/munki/shared/admin/pkginfolib.swift @@ -115,6 +115,8 @@ func createInstallsItem(_ itempath: String) -> PlistDict { } else { info["version_comparison_key"] = "CFBundleShortVersionString" } + } else if info["CFBundleVersion"] != nil { + info["version_comparison_key"] = "CFBundleVersion" } if !info.keys.contains("CFBundleShortVersionString"), !info.keys.contains("CFBundleVersion") { From 519c1922aa55e1f61326836e305153238832cca2 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Wed, 22 Oct 2025 11:21:02 -0700 Subject: [PATCH 02/11] Bump version for future release --- code/cli/munki/shared/version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/cli/munki/shared/version.swift b/code/cli/munki/shared/version.swift index e1c360ed..c73ef88e 100644 --- a/code/cli/munki/shared/version.swift +++ b/code/cli/munki/shared/version.swift @@ -19,7 +19,7 @@ // limitations under the License. /// one single place to define a version for CLI tools -let CLI_TOOLS_VERSION = "7.0.1" +let CLI_TOOLS_VERSION = "7.0.2" let BUILD = "" /// Returns version of Munki tools From a3b03231109b443fe42ba09cf6156e81180eb7dd Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Wed, 22 Oct 2025 11:22:35 -0700 Subject: [PATCH 03/11] Support symlinks for repo file: URLs --- code/cli/munki/shared/munkirepo/FileRepo.swift | 2 +- code/cli/munki/shared/utils/fileutils.swift | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/code/cli/munki/shared/munkirepo/FileRepo.swift b/code/cli/munki/shared/munkirepo/FileRepo.swift index e88ce024..a0b23515 100644 --- a/code/cli/munki/shared/munkirepo/FileRepo.swift +++ b/code/cli/munki/shared/munkirepo/FileRepo.swift @@ -168,7 +168,7 @@ class FileRepo: Repo { } } // does root dir exist now? - if !pathIsDirectory(root) { + if !pathIsDirectory(root, followSymlinks: true) { throw MunkiError("Repo path does not exist") } } diff --git a/code/cli/munki/shared/utils/fileutils.swift b/code/cli/munki/shared/utils/fileutils.swift index a55baf15..b6a41a85 100644 --- a/code/cli/munki/shared/utils/fileutils.swift +++ b/code/cli/munki/shared/utils/fileutils.swift @@ -39,7 +39,7 @@ func pathIsRegularFile(_ path: String) -> Bool { return false } -/// Returns true if path is a symlink/ +/// Returns true if path is a symlink func pathIsSymlink(_ path: String) -> Bool { if let fileType = fileType(path) { return fileType == FileAttributeType.typeSymbolicLink.rawValue @@ -47,10 +47,17 @@ func pathIsSymlink(_ path: String) -> Bool { return false } -/// Returns true if path is a directory/ -func pathIsDirectory(_ path: String) -> Bool { +/// Returns true if path is a directory; follows symlinks if followSymlinks=true +func pathIsDirectory(_ path: String, followSymlinks: Bool = false) -> Bool { if let fileType = fileType(path) { - return fileType == FileAttributeType.typeDirectory.rawValue + if fileType == FileAttributeType.typeDirectory.rawValue { + return true + } + if followSymlinks, fileType == FileAttributeType.typeSymbolicLink.rawValue { + if let target = try? FileManager.default.destinationOfSymbolicLink(atPath: path) { + return pathIsDirectory(target) + } + } } return false } From dba4597090e3b37818cd643ee69b02f529674734 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Wed, 22 Oct 2025 12:06:24 -0700 Subject: [PATCH 04/11] Add new test for changes in fileutils pathIsDirectory() --- .../cli/munki/munki.xcodeproj/project.pbxproj | 19 +------- .../munkiCLItesting/fileUtilsTests.swift | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 code/cli/munki/munkiCLItesting/fileUtilsTests.swift diff --git a/code/cli/munki/munki.xcodeproj/project.pbxproj b/code/cli/munki/munki.xcodeproj/project.pbxproj index df486f94..fe133813 100644 --- a/code/cli/munki/munki.xcodeproj/project.pbxproj +++ b/code/cli/munki/munki.xcodeproj/project.pbxproj @@ -790,27 +790,10 @@ C0EEC9A62DA7335900F92942 /* clientcerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = clientcerts.swift; sourceTree = ""; }; /* End PBXFileReference section */ -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - C09616492DEA2C0900281B6B /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - tokenize.swift, - ); - target = C07A6FB62C2B5ADE00090743 /* munkitester */; - }; - C096164D2DEA2C0900281B6B /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - tokenize.swift, - ); - target = C0684E5B2DC736EE0091E774 /* munkiCLItesting */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - /* Begin PBXFileSystemSynchronizedRootGroup section */ C00519A22D2A5B850060DDB6 /* authrestartd */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = authrestartd; sourceTree = ""; }; C0276DB22E8C070500D443C3 /* repocheck */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = repocheck; sourceTree = ""; }; - C05DB2032DAC53150081FACD /* manifestutil */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C09616492DEA2C0900281B6B /* PBXFileSystemSynchronizedBuildFileExceptionSet */, C096164D2DEA2C0900281B6B /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = manifestutil; sourceTree = ""; }; + C05DB2032DAC53150081FACD /* manifestutil */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = manifestutil; sourceTree = ""; }; C0684DD22DBFDBD20091E774 /* precache_agent */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = precache_agent; sourceTree = ""; }; C0684E2E2DC040450091E774 /* installhelper */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = installhelper; sourceTree = ""; }; C0684E5D2DC736EE0091E774 /* munkiCLItesting */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = munkiCLItesting; sourceTree = ""; }; diff --git a/code/cli/munki/munkiCLItesting/fileUtilsTests.swift b/code/cli/munki/munkiCLItesting/fileUtilsTests.swift new file mode 100644 index 00000000..0e4ddbdb --- /dev/null +++ b/code/cli/munki/munkiCLItesting/fileUtilsTests.swift @@ -0,0 +1,43 @@ +// +// fileUtilsTests.swift +// munki +// +// Created by Greg Neagle on 10/22/25. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Testing + +struct fileUtilsTests { + @Test func pathIsDirectoryTests() throws { + // setup + let testDirectoryPath = try #require(TempDir.shared.path, "Can't get temp directory path") + try #require( + FileManager.default.createFile( + atPath: testDirectoryPath + "/test.txt", contents: nil, attributes: nil + ) != false, + "Can't create test file" + ) + try #require( + try? FileManager.default.createSymbolicLink( + atPath: testDirectoryPath + "/symlink", + withDestinationPath: testDirectoryPath + ), + "Can't create test symlink" + ) + #expect(pathIsDirectory(testDirectoryPath)) + #expect(!pathIsDirectory(testDirectoryPath + "/test.txt")) + #expect(!pathIsDirectory(testDirectoryPath + "/symlink")) + #expect(pathIsDirectory(testDirectoryPath + "/symlink", followSymlinks: true)) + } +} From f02a2959d8cc6302437e1a97b65848887ffc40e3 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Wed, 22 Oct 2025 12:06:55 -0700 Subject: [PATCH 05/11] Fix broken test --- .../cli/munki/munkiCLItesting/pkgutilsTests.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/code/cli/munki/munkiCLItesting/pkgutilsTests.swift b/code/cli/munki/munkiCLItesting/pkgutilsTests.swift index 0e47fce5..1b0f5a24 100644 --- a/code/cli/munki/munkiCLItesting/pkgutilsTests.swift +++ b/code/cli/munki/munkiCLItesting/pkgutilsTests.swift @@ -236,7 +236,10 @@ struct PackageInfoFileTests { let unwrappedPkginfoPath = try #require( pkginfoPath, "Failed to create temporary pkgInfo file" ) - let receipt = receiptFromPackageInfoFile(unwrappedPkginfoPath) + let receipt = try #require( + receiptFromPackageInfoFile(unwrappedPkginfoPath), + "Could not get receipt from pkginfo" + ) #expect((receipt["packageid"] as? String ?? "") == "com.googlecode.munki.core") } @@ -244,7 +247,10 @@ struct PackageInfoFileTests { let unwrappedPkginfoPath = try #require( pkginfoPath, "Failed to create temporary pkgInfo file" ) - let receipt = receiptFromPackageInfoFile(unwrappedPkginfoPath) + let receipt = try #require( + receiptFromPackageInfoFile(unwrappedPkginfoPath), + "Could not get receipt from pkginfo" + ) #expect((receipt["version"] as? String ?? "") == "7.0.0.5096") } @@ -252,7 +258,10 @@ struct PackageInfoFileTests { let unwrappedPkginfoPath = try #require( pkginfoPath, "Failed to create temporary pkgInfo file" ) - let receipt = receiptFromPackageInfoFile(unwrappedPkginfoPath) + let receipt = try #require( + receiptFromPackageInfoFile(unwrappedPkginfoPath), + "Could not get receipt from pkginfo" + ) #expect((receipt["installed_size"] as? Int ?? 0) == 39393) } } From 3a8c03b8bba1fa0e30f64c523da2ce017d369808 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Wed, 22 Oct 2025 12:07:33 -0700 Subject: [PATCH 06/11] Address complier warning with simplified logic --- code/cli/munki/shared/admin/pkginfolib.swift | 34 +++++++++----------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/code/cli/munki/shared/admin/pkginfolib.swift b/code/cli/munki/shared/admin/pkginfolib.swift index de1e8502..f1a2a69b 100644 --- a/code/cli/munki/shared/admin/pkginfolib.swift +++ b/code/cli/munki/shared/admin/pkginfolib.swift @@ -74,26 +74,24 @@ func createInstallsItem(_ itempath: String) -> PlistDict { } else { info["type"] = "bundle" } - if let plist = getBundleInfo(itempath) { - for key in ["CFBundleName", "CFBundleIdentifier", - "CFBundleShortVersionString", "CFBundleVersion"] - { - if let value = plist[key] as? String { - info[key] = value - } + for key in ["CFBundleName", "CFBundleIdentifier", + "CFBundleShortVersionString", "CFBundleVersion"] + { + if let value = plist[key] as? String { + info[key] = value } - if let minOSVers = plist["LSMinimumSystemVersion"] as? String { - info["minosversion"] = minOSVers - } else if let minOSVersByArch = plist["LSMinimumSystemVersionByArchitecture"] as? [String: String] { - // get the highest/latest of all the minimum os versions - let minOSVersions = minOSVersByArch.values - let versions = minOSVersions.map { MunkiVersion($0) } - if let maxVersion = versions.max() { - info["minosversion"] = maxVersion.value - } - } else if let minSysVers = plist["SystemVersionCheck:MinimumSystemVersion"] as? String { - info["minosversion"] = minSysVers + } + if let minOSVers = plist["LSMinimumSystemVersion"] as? String { + info["minosversion"] = minOSVers + } else if let minOSVersByArch = plist["LSMinimumSystemVersionByArchitecture"] as? [String: String] { + // get the highest/latest of all the minimum os versions + let minOSVersions = minOSVersByArch.values + let versions = minOSVersions.map { MunkiVersion($0) } + if let maxVersion = versions.max() { + info["minosversion"] = maxVersion.value } + } else if let minSysVers = plist["SystemVersionCheck:MinimumSystemVersion"] as? String { + info["minosversion"] = minSysVers } } else if let plist = try? readPlist(fromFile: itempath) as? PlistDict { // we must be a plist From 8e5848d9f8c956d6381a3c94f2992f86b13f1f74 Mon Sep 17 00:00:00 2001 From: Stephen Boyle Date: Thu, 23 Oct 2025 14:22:18 -0400 Subject: [PATCH 07/11] Update fetch.swift (#1281) modify etag / last-modified logic for http request: last-modified will be used if etag is not present. --- code/cli/munki/shared/network/fetch.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/code/cli/munki/shared/network/fetch.swift b/code/cli/munki/shared/network/fetch.swift index 984f1bc4..28b6ff6b 100644 --- a/code/cli/munki/shared/network/fetch.swift +++ b/code/cli/munki/shared/network/fetch.swift @@ -344,13 +344,13 @@ func getHTTPfileIfChangedAtomically( do { let data = try getXattr(named: GURL_XATTR, atPath: destinationPath) if let headers = try readPlist(fromData: data) as? [String: String] { - eTag = headers["etag"] ?? "" + // We can use onlyIfNewer if we have either etag or last-modified + if headers["etag"] != nil || headers["last-modified"] != nil { + getOnlyIfNewer = true + } } } catch { - // fall through - } - if eTag.isEmpty { - getOnlyIfNewer = false + // fall through - no cached headers } } var headers: [String: String] From 03993becebacdc0581e4fb3b6ad9db3dca8d6ecc Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Thu, 23 Oct 2025 11:52:51 -0700 Subject: [PATCH 08/11] Improved logic for using stored etag or last-modified headers to avoid re-downloading resources --- code/cli/munki/shared/network/fetch.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/code/cli/munki/shared/network/fetch.swift b/code/cli/munki/shared/network/fetch.swift index 28b6ff6b..bc44253f 100644 --- a/code/cli/munki/shared/network/fetch.swift +++ b/code/cli/munki/shared/network/fetch.swift @@ -336,11 +336,9 @@ func getHTTPfileIfChangedAtomically( resume: Bool = false, followRedirects: String = "none", ) throws -> Bool { - var eTag = "" var getOnlyIfNewer = false if pathExists(destinationPath) { - getOnlyIfNewer = true - // see if we have an etag attribute + // see if we have a stored etag or last-modified header do { let data = try getXattr(named: GURL_XATTR, atPath: destinationPath) if let headers = try readPlist(fromData: data) as? [String: String] { From 3864988b165263804a4a6765c45470582459d8bc Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Thu, 23 Oct 2025 11:53:48 -0700 Subject: [PATCH 09/11] Since we've decided we're never going to support installing Apple softwareupdates in Munki 7, simplify some logic --- code/cli/munki/managedsoftwareupdate/msuutils.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/cli/munki/managedsoftwareupdate/msuutils.swift b/code/cli/munki/managedsoftwareupdate/msuutils.swift index ae3779a5..d4584175 100644 --- a/code/cli/munki/managedsoftwareupdate/msuutils.swift +++ b/code/cli/munki/managedsoftwareupdate/msuutils.swift @@ -332,7 +332,7 @@ func doInstallTasks(doAppleUpdates: Bool = false, onlyUnattended: Bool = false) } var munkiItemsRestartAction = PostAction.none - var appleItemsRestartAction = PostAction.none + //var appleItemsRestartAction = PostAction.none if munkiUpdatesAvailable() > 0 { // install Munki updates @@ -341,7 +341,7 @@ func doInstallTasks(doAppleUpdates: Bool = false, onlyUnattended: Bool = false) if munkiUpdatesContainItemWithInstallerType("startosinstall") { Report.shared.save() // install macOS - // TODO: implement this (install macOS via startOSInstall) + // TODO: implement this (install macOS via startOSInstall) (will likely never implement) } } } @@ -353,7 +353,8 @@ func doInstallTasks(doAppleUpdates: Bool = false, onlyUnattended: Bool = false) Report.shared.save() - return max(appleItemsRestartAction, munkiItemsRestartAction) + //return max(appleItemsRestartAction, munkiItemsRestartAction) // we no longer support installing Apple updates + return munkiItemsRestartAction } /// Handle the need for a forced logout. Start our logouthelper From 61809bf452dfda7b999c0555ee496a81fd83e5e3 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Thu, 23 Oct 2025 11:54:36 -0700 Subject: [PATCH 10/11] Since we've decided we're never going to support installing Apple softwareupdates in Munki 7, simplify some code --- .../munki/managedsoftwareupdate/managedsoftwareupdate.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/code/cli/munki/managedsoftwareupdate/managedsoftwareupdate.swift b/code/cli/munki/managedsoftwareupdate/managedsoftwareupdate.swift index be42d2f2..ba16f4e7 100644 --- a/code/cli/munki/managedsoftwareupdate/managedsoftwareupdate.swift +++ b/code/cli/munki/managedsoftwareupdate/managedsoftwareupdate.swift @@ -422,10 +422,7 @@ struct ManagedSoftwareUpdate: AsyncParsableCommand { _ = forceInstallPackageCheck() // this might mark some more items as unattended // now install anything that can be done unattended munkiLog("Installing only items marked unattended because SuppressLoginwindowInstall is true.") - _ = await doInstallTasks( - doAppleUpdates: appleUpdateCount > 0, - onlyUnattended: true - ) + _ = await doInstallTasks(onlyUnattended: true) return } if getIdleSeconds() < 10 { From 7c4a87670e5c3090c56d4229b737275f4f520d48 Mon Sep 17 00:00:00 2001 From: Alex Jones Date: Mon, 27 Oct 2025 20:22:42 +0000 Subject: [PATCH 11/11] Fix bug in installhelper logic (#1284) Don't unload all Munki launchd jobs if appusage jobs are not currently loaded. --- code/cli/munki/installhelper/main.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/code/cli/munki/installhelper/main.swift b/code/cli/munki/installhelper/main.swift index 41e95bb8..5c199dc4 100644 --- a/code/cli/munki/installhelper/main.swift +++ b/code/cli/munki/installhelper/main.swift @@ -263,10 +263,7 @@ func reloadUserLaunchAgents(group: String) { // first, unload active Munki jobs var activeAgentLabels = getMunkiLaunchdLabels(uid: uid) if group == "appusage" { - if activeAgentLabels.contains(APPUSAGE_AGENT) { - // only unload APPUSAGE_AGENT - activeAgentLabels = [APPUSAGE_AGENT] - } + activeAgentLabels = activeAgentLabels.filter { $0 == APPUSAGE_AGENT } } if group == "launchd" { // unload everything but APPUSAGE_AGENT @@ -341,9 +338,7 @@ func reloadLaunchDaemons(group: String) { var activeDaemonLabels = getMunkiLaunchdLabels() if group == "appusage" { // we should only unload APPUSAGE_DAEMON if if's active - if activeDaemonLabels.contains(APPUSAGE_DAEMON) { - activeDaemonLabels = [APPUSAGE_DAEMON] - } + activeDaemonLabels = activeDaemonLabels.filter { $0 == APPUSAGE_DAEMON } } if group == "launchd" { // unload all Munki jobs _except_ APPUSAGE_DAEMON and our installhelper jobs