├── .gitignore ├── Package.resolved ├── com.scriptingosx.example.plist ├── Package.swift ├── Sources ├── prefs │ ├── Prefs.swift │ └── Preferences.swift └── plist2profile │ └── Plist2Profile.swift ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ ├── swift-prefs.xcscheme │ ├── prefs.xcscheme │ ├── swift-prefs-Package.xcscheme │ └── plist2profile.xcscheme ├── README.md ├── pkgAndNotarize.sh └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "07f2863b78650b61c3ed8b5b7b15b1b606795fc0e6a9dd21a44ef3853ca7f9cf", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-argument-parser", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-argument-parser", 8 | "state" : { 9 | "revision" : "46989693916f56d1186bd59ac15124caef896560", 10 | "version" : "1.3.1" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /com.scriptingosx.example.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sample key 1 6 | value one 7 | sample key 2 8 | second value 9 | sample key 3 10 | 11 | subkey 1 12 | sub value 13 | subkey 2 14 | other sub value 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-prefs", 8 | platforms: [ 9 | .macOS(.v13) 10 | ], 11 | products: [ 12 | .executable(name: "prefs", targets: ["prefs"]), 13 | .executable(name: "plist2profile", targets: ["plist2profile"]) 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), 17 | ], 18 | targets: [ 19 | .executableTarget( 20 | name: "prefs", 21 | dependencies: [ 22 | .product( 23 | name: "ArgumentParser", 24 | package: "swift-argument-parser" 25 | )], 26 | path: "Sources/prefs" 27 | ), 28 | .executableTarget( 29 | name: "plist2profile", 30 | dependencies: [ 31 | .product( 32 | name: "ArgumentParser", 33 | package: "swift-argument-parser" 34 | )], 35 | path: "Sources/plist2profile" 36 | ) 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /Sources/prefs/Prefs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Prefs.swift 3 | // 4 | // 5 | // Created by Armin Briegel on 2024-03-20. 6 | // 7 | 8 | import Foundation 9 | import ArgumentParser 10 | 11 | @main 12 | struct Prefs: ParsableCommand { 13 | static var configuration = CommandConfiguration( 14 | commandName: "prefs", 15 | abstract: "Shows preference/defaults settings from all levels together with their level", 16 | usage: """ 17 | prefs 18 | prefs [ ...] 19 | """, 20 | version: "0.1" 21 | ) 22 | 23 | 24 | // MARK: arguments and flags 25 | 26 | @Flag( 27 | name: [.customShort("g"), .customLong("globals")], 28 | help: "show values from GlobalPreferences files" 29 | ) 30 | var showGlobals = false 31 | 32 | @Flag( 33 | name: [.customLong("volatiles")], 34 | help: "show values from volatile domains" 35 | ) 36 | var showVolatiles = false 37 | 38 | @Flag( 39 | name: .customLong("value"), 40 | help: "show only the value, no other information" 41 | ) 42 | var showOnlyValue = false 43 | 44 | @Argument( 45 | help: ArgumentHelp( 46 | "the app identifier or preference domain", 47 | valueName: "domain" 48 | ) 49 | ) 50 | var applicationID: String 51 | 52 | @Argument(help: "preference keys to show. When no key is given all values will be shown") 53 | var keys: [String] = [] 54 | 55 | 56 | // MARK: functions 57 | func exit(_ message: Any, code: Int32) throws -> Never { 58 | print(message) 59 | throw ExitCode(code) 60 | } 61 | 62 | func printDetail(_ key: String, preferences: Preferences) { 63 | guard let value = preferences.userDefaults.object(forKey: key) else { return } 64 | let level = preferences.level(for: key) ?? "unknown" 65 | if showOnlyValue { 66 | print(value) 67 | } else { 68 | print("\(key) [\(level)]: \(value)") 69 | } 70 | } 71 | 72 | 73 | // MARK: run 74 | func run() throws { 75 | guard let preferences = Preferences(suiteName: applicationID) 76 | else { 77 | try exit("cannot get defaults for '\(applicationID)'", code: 11) 78 | } 79 | 80 | if keys.count > 0 { 81 | for key in keys { 82 | printDetail(key, preferences: preferences) 83 | } 84 | return 85 | } 86 | 87 | // cache these for performance 88 | let globalKeys = preferences.globalKeys 89 | //let volatileKeys = preferences.volatileKeys 90 | 91 | for key in preferences.allKeys { 92 | if !showGlobals && globalKeys.contains(where: {$0 == key}) { 93 | continue 94 | } 95 | 96 | if !showVolatiles && preferences.volatileKeys.contains(where: {$0 == key}) { 97 | continue 98 | } 99 | 100 | printDetail(key, preferences: preferences) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/swift-prefs.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/prefs.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 57 | 58 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/swift-prefs-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 57 | 58 | 64 | 65 | 66 | 67 | 73 | 74 | 80 | 81 | 82 | 83 | 85 | 86 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-prefs 2 | 3 | A selection of command line tools for working with setting/defaults/preferences on macOS. 4 | 5 | ## `prefs` 6 | 7 | This tool is inspired by [Greg Neagle's `fancy_defaults_read.py`](https://gist.github.com/gregneagle/010b369e86410a2f279ff8e980585c68) and a remake of [my earlier Python tool](https://github.com/scriptingosx/PrefsTool). 8 | 9 | In the simplest use case you can just pass the app identifier: 10 | 11 | ``` 12 | % prefs com.apple.screensaver 13 | moduleDict [host]: { 14 | moduleName = "Computer Name"; 15 | path = "/System/Library/Frameworks/ScreenSaver.framework/PlugIns/Computer Name.appex"; 16 | type = 0; 17 | } 18 | PrefsVersion [host]: 100 19 | idleTime [host]: 0 20 | lastDelayTime [host]: 1200 21 | tokenRemovalAction [host]: 0 22 | showClock [host]: 0 23 | CleanExit [host]: 1 24 | ``` 25 | 26 | The tool will print _all_ composited preferences keys and their type and value, with the preference domain where the value was set. The output format is: 27 | 28 | ``` 29 | prefKey [domain]: value 30 | ``` 31 | 32 | A preference domain of `managed` means the value is set with a configuration profile. 33 | 34 | While preference values set in `.GlobalPreferences.plist` in the different domains are composited into the the application defaults, they are _not_ shown by default, since there are many of them and they will make the output fairly unreadable. If you want to see them add the `--globals` (or `-g`) option: 35 | 36 | ``` 37 | % prefs --globals com.apple.screensaver 38 | ``` 39 | 40 | You can also add one or more keys after the app identifier to get just specific values: 41 | 42 | ``` 43 | % prefs com.apple.screensaver idleTime AppleLocale 44 | idleTime [host]: 0 45 | AppleLocale [global/user]: en_US@rg=nlzzzz 46 | ``` 47 | 48 | You can also add the `--value` option to show just the value in the output (might be useful when you want to get the composited value for another script. 49 | 50 | ``` 51 | % prefs --value com.apple.screensaver idleTime 52 | 1200 53 | ``` 54 | 55 | ### Known Issues 56 | 57 | - doesn't read preferences of sandboxes apps from their containers 58 | 59 | ## plist2profile 60 | 61 | This tool is a re-interpretation in Swift of [Tim Sutton's mcxToProfile](https://github.com/timsutton/mcxToProfile). 62 | 63 | It will convert a normal flat plist file into a custom mobileconfig/configuration profile that can be used for manual installation or with an MDM server. 64 | 65 | In the simplest form, use it like this: 66 | 67 | ``` 68 | % plist2profile --identifier example.settings com.example.settings.plist 69 | ``` 70 | 71 | This will generate a file named `example.settings.mobileconfig` in the current working directory which manages the preference keys in the `com.example.settings.plist` in the `com.example.settings` preference domain. You can add multiple plist files. 72 | 73 | The preference domain for the settings is determined from the file name of each plist file given (removing the `plist` file extension). 74 | 75 | You can add a display and organization name that will be used in the respective fields using the `--displayname` and `--organization` options. 76 | 77 | By default, the profile is created with a `System` scope. you can change it to `User` with the `--user` flag. 78 | 79 | There are two ways to assemble custom preference profile, the 'traditional' mcx format and a more modern format, which [Bob Gendler described in this post](https://boberito.medium.com/config-profile-and-manage-all-the-things-just-about-cafea8627d4b). This tool creates the modern format by default, but can also create the traditional format when you set the `--mcx` key. 80 | 81 | 82 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/plist2profile.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 57 | 58 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Sources/prefs/Preferences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Armin Briegel on 2024-03-20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Preferences { 11 | init?(suiteName: String) { 12 | self.suiteName = suiteName 13 | guard let ud = UserDefaults(suiteName: suiteName) else { return nil } 14 | self.userDefaults = ud 15 | } 16 | 17 | let suiteName: String 18 | let userDefaults: UserDefaults 19 | 20 | 21 | var allKeys: [String] { 22 | userDefaults.dictionaryRepresentation().map { $0.key } 23 | } 24 | 25 | var networkGlobalKeys: [String] { 26 | keylist(kCFPreferencesAnyApplication, kCFPreferencesAnyUser, kCFPreferencesAnyHost) 27 | } 28 | 29 | var systemGlobalKeys: [String] { 30 | keylist(kCFPreferencesAnyApplication, kCFPreferencesAnyUser, kCFPreferencesCurrentHost) 31 | } 32 | 33 | var hostGlobalKeys: [String] { 34 | keylist(kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost) 35 | } 36 | 37 | var userGlobalKeys: [String] { 38 | keylist(kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) 39 | } 40 | 41 | var globalKeys: [String] { 42 | var globalKeys = Set(userGlobalKeys) 43 | globalKeys.formUnion(networkGlobalKeys) 44 | globalKeys.formUnion(hostGlobalKeys) 45 | return Array(globalKeys) 46 | } 47 | 48 | var managedKeys: [String] { 49 | allKeys.filter { userDefaults.objectIsForced(forKey: $0) } 50 | } 51 | 52 | var userKeys: [String] { 53 | keylist(suiteName as CFString, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) 54 | } 55 | 56 | var networkKeys: [String] { 57 | keylist(suiteName as CFString, kCFPreferencesAnyUser, kCFPreferencesAnyHost) 58 | } 59 | 60 | var systemKeys: [String] { 61 | keylist(suiteName as CFString, kCFPreferencesAnyUser, kCFPreferencesCurrentHost) 62 | } 63 | 64 | var hostKeys: [String] { 65 | keylist(suiteName as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost) 66 | } 67 | 68 | var volatileKeys: [String] { 69 | let vDomains = userDefaults.volatileDomainNames 70 | var volatileKeys = Set() 71 | for domain in vDomains { 72 | let dict = userDefaults.volatileDomain(forName: domain) 73 | let keys = dict.map {$0.key} 74 | volatileKeys.formUnion(keys) 75 | } 76 | return Array(volatileKeys) 77 | } 78 | 79 | // MARK: functions 80 | 81 | func keylist(_ applicationID: CFString, _ userName: CFString, _ hostName: CFString) -> [String] { 82 | CFPreferencesCopyKeyList(applicationID, userName, hostName) as? [String] ?? [] 83 | } 84 | 85 | func isManaged(_ key: String) -> Bool { 86 | userDefaults.objectIsForced(forKey: key) 87 | } 88 | 89 | func level(for key: String) -> String? { 90 | if !allKeys.contains(where: {$0 == key}) { 91 | return nil 92 | } 93 | if isManaged(key) { 94 | return "managed" 95 | } 96 | if hostKeys.contains(where: {$0 == key}) { 97 | return "host" 98 | } 99 | if hostGlobalKeys.contains(where: {$0 == key}) { 100 | return "global/host" 101 | } 102 | if userKeys.contains(where: {$0 == key}) { 103 | return "user" 104 | } 105 | if userGlobalKeys.contains(where: {$0 == key}) { 106 | return "global/user" 107 | } 108 | if systemKeys.contains(where: {$0 == key}) { 109 | return "system" 110 | } 111 | if systemGlobalKeys.contains(where: {$0 == key}) { 112 | return "global/system" 113 | } 114 | if networkKeys.contains(where: {$0 == key}) { 115 | return "network" 116 | } 117 | if networkGlobalKeys.contains(where: {$0 == key}) { 118 | return "global/network" 119 | } 120 | if volatileKeys.contains(where: {$0 == key}) { 121 | return "volatile" 122 | } 123 | 124 | return nil 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /pkgAndNotarize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # pkgAndNotarize.sh 4 | 5 | # this script will 6 | # - build the swift package project executable 7 | # - sign the binary 8 | # - create a signed pkg installer file 9 | # - submit the pkg for notarization 10 | # - staple the pkg 11 | 12 | # more detail here: 13 | # https://scriptingosx.com/2023/08/build-a-notarized-package-with-a-swift-package-manager-executable/ 14 | 15 | # by Armin Briegel - Scripting OS X 16 | 17 | # Permission is granted to use this code in any way you want. 18 | # Credit would be nice, but not obligatory. 19 | # Provided "as is", without warranty of any kind, express or implied. 20 | 21 | 22 | # modify these variables for your project 23 | 24 | # Developer ID Installer cert name 25 | developer_name_and_id="Armin Briegel (JME5BW3F3R)" 26 | installer_sign_cert="Developer ID Installer: ${developer_name_and_id}" 27 | application_sign_cert="Developer ID Application: ${developer_name_and_id}" 28 | 29 | # profile name used with `notarytool --store-credentials` 30 | credential_profile="notary-scriptingosx" 31 | 32 | # build info 33 | product_name="swift-prefs-tools" 34 | binary_names=( "prefs" "plist2profile" ) 35 | 36 | # pkg info 37 | pkg_name="$product_name" 38 | identifier="com.scriptingosx.${product_name}" 39 | min_os_version="13.5" 40 | install_location="/usr/local/bin/" 41 | 42 | 43 | # don't modify below here 44 | 45 | 46 | # calculated variables 47 | SRCROOT=$(dirname ${0:A}) 48 | build_dir="$SRCROOT/.build" 49 | 50 | date +"%F %T" 51 | 52 | # build the binary 53 | 54 | #swift package clean 55 | echo 56 | echo "### building $product_name" 57 | if ! swift build --configuration release \ 58 | --arch arm64 --arch x86_64 59 | then 60 | echo "error building binary" 61 | exit 2 62 | fi 63 | 64 | if [[ ! -d $build_dir ]]; then 65 | echo "couldn't find .build directory" 66 | exit 3 67 | fi 68 | 69 | 70 | binary_source_path="${build_dir}/apple/Products/Release/${binary_names[1]}" 71 | 72 | if [[ ! -e $binary_source_path ]]; then 73 | echo "cannot find binary at $binary_source_path" 74 | exit 4 75 | fi 76 | 77 | # get version from binary 78 | version=$($binary_source_path --version) 79 | 80 | if [[ $version == "" ]]; then 81 | echo "could not get version" 82 | exit 5 83 | fi 84 | 85 | component_path="${build_dir}/${pkg_name}.pkg" 86 | product_path="${build_dir}/${pkg_name}-${version}.pkg" 87 | pkgroot="${build_dir}/pkgroot" 88 | 89 | echo 90 | echo "### Signing, Packaging and Notarizing '$product_name'" 91 | echo "Version: $version" 92 | echo "Identifier: $identifier" 93 | echo "Min OS Version: $min_os_version" 94 | echo "Developer ID: $developer_name_and_id" 95 | 96 | pkgroot="$build_dir/pkgroot" 97 | if [[ ! -d $pkgroot ]]; then 98 | mkdir -p $pkgroot 99 | fi 100 | 101 | for binary in ${binary_names}; do 102 | binary_source_path="${build_dir}/apple/Products/Release/${binary}" 103 | 104 | if [[ ! -f $binary_source_path ]]; then 105 | echo "can't find binary at $binary_path" 106 | exit 6 107 | fi 108 | 109 | cp $binary_source_path $pkgroot 110 | #scripts_dir="$SRCROOT/scripts" 111 | 112 | binary_path=$pkgroot/$binary 113 | 114 | # sign the binary 115 | echo 116 | echo "### signing '${binary}'" 117 | if ! codesign --sign $application_sign_cert \ 118 | --options runtime \ 119 | --runtime-version $min_os_version \ 120 | --timestamp \ 121 | $binary_path 122 | then 123 | echo "error signing binary '${binary}'" 124 | exit 7 125 | fi 126 | 127 | done 128 | 129 | # create the component pkg 130 | echo 131 | echo "### building component pkg file" 132 | 133 | if ! pkgbuild --root $pkgroot \ 134 | --identifier $identifier \ 135 | --version $version-$build_number \ 136 | --install-location $install_location \ 137 | --min-os-version $min_os_version \ 138 | --compression latest \ 139 | $component_path 140 | 141 | # --scripts "$scripts_dir" \ 142 | then 143 | echo "error building component" 144 | exit 8 145 | fi 146 | 147 | # create the distribution pkg 148 | echo 149 | echo "### building distribution pkg file" 150 | 151 | if ! productbuild --package "$component_path" \ 152 | --identifier "$identifier" \ 153 | --version "$version-$build_number" \ 154 | --sign "$installer_sign_cert" \ 155 | "$product_path" 156 | then 157 | echo "error building distribution archive" 158 | exit 9 159 | fi 160 | 161 | # notarize 162 | echo 163 | echo "### submitting for notarization" 164 | if ! xcrun notarytool submit "$product_path" \ 165 | --keychain-profile "$credential_profile" \ 166 | --wait 167 | then 168 | echo "error notarizing pkg" 169 | echo "use 'xcrun notarylog --keychain-profile \"$credential_profile\"' for more detail" 170 | exit 10 171 | fi 172 | 173 | # staple 174 | echo 175 | echo "### staple" 176 | if ! xcrun stapler staple "$product_path" 177 | then 178 | echo "error stapling pkg" 179 | exit 11 180 | fi 181 | 182 | # clean up component pkg 183 | rm "$component_path" 184 | 185 | # clean up pkgroot 186 | rm -rf $pkgroot 187 | 188 | echo 189 | # show result path 190 | echo "### complete" 191 | echo "$product_path" 192 | 193 | exit 0 194 | -------------------------------------------------------------------------------- /Sources/plist2profile/Plist2Profile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Plist2Profile.swift 3 | // 4 | // 5 | // Created by Armin Briegel on 2024-03-20. 6 | // 7 | 8 | import Foundation 9 | import ArgumentParser 10 | 11 | @main 12 | struct Plist2Profile: ParsableCommand { 13 | static var configuration = CommandConfiguration( 14 | commandName: "plist2profile", 15 | abstract: "converts a standard preference plist file to a mobileconfig profile", 16 | usage: "plist2profile --identifier ...", 17 | version: "0.1" 18 | ) 19 | 20 | // MARK: arguments, options, flags 21 | @Option( 22 | name: .shortAndLong, 23 | help: ArgumentHelp( 24 | "The identifier for the profile.", 25 | valueName: "identifier" 26 | ) 27 | ) 28 | var identifier: String 29 | 30 | @Argument( 31 | help: ArgumentHelp( 32 | "Path to a plist file to be added as a profile payload. Can be given more than once.", 33 | valueName: "plist" 34 | ) 35 | ) 36 | var plistPaths: [String] 37 | 38 | @Option( 39 | name: [.customShort("d"), .customLong("displayname")], 40 | help: "Display name for the profile. (default: 'plist2profile: ')" 41 | ) 42 | var displayName = "" 43 | 44 | @Option( 45 | name: [.customShort("g"), .customLong("organization")], 46 | help: "Organization field for the profile." 47 | ) 48 | var organization = "" 49 | 50 | @Option( 51 | name: [.customShort("o"), .customLong("output")], 52 | help: "Output path for profile. (default: '.mobileconfig')" 53 | ) 54 | var outputPath = "" 55 | 56 | @Flag( 57 | name: .customLong("user"), 58 | help: "Sets the scope for the profile to 'User'. (default is 'System')" 59 | ) 60 | var userScope = false 61 | 62 | @Flag( 63 | name: .customLong("mcx"), 64 | help: "Creates the profile in the traditional mcx format. (default: modern)" 65 | ) 66 | var mcx = false 67 | 68 | // MARK: variables 69 | 70 | var uuid = UUID() 71 | var payloadVersion = 1 72 | var payloadType = "Configuration" 73 | var payloadScope = "System" 74 | 75 | // TODO: missing keys for profile 76 | // removal disallowed 77 | // removalDate, duration until removal 78 | // description 79 | // 80 | // all of these should at least be grabbed when initialising from a file 81 | // 82 | 83 | // MARK: functions 84 | 85 | // TODO: can we put these functions in shared file? Can we share files between targets in a package without creating a library? 86 | 87 | func exit(_ message: Any, code: Int32 = 1) throws -> Never { 88 | print(message) 89 | throw ExitCode(code) 90 | } 91 | 92 | func isReadableFilePath(_ path: String) throws { 93 | let fm = FileManager.default 94 | var isDirectory: ObjCBool = false 95 | if !fm.fileExists(atPath: path, isDirectory: &isDirectory) { 96 | try exit("no file at path '\(path)'!", code: 66) 97 | } 98 | if isDirectory.boolValue { 99 | try exit("path '\(path)' is a directory", code: 66) 100 | } 101 | if !fm.isReadableFile(atPath: path) { 102 | try exit("cannot read file at '\(path)'!", code: 66) 103 | } 104 | } 105 | 106 | mutating func populateDefaults() { 107 | // if displayName is empty, populate 108 | if displayName.isEmpty { 109 | displayName = "plist2Profile: \(identifier)" 110 | } 111 | 112 | // if output is empty, generate file name 113 | if outputPath.isEmpty { 114 | outputPath = identifier.appending(".mobileconfig") 115 | } 116 | 117 | if userScope { 118 | payloadScope = "User" 119 | } 120 | } 121 | 122 | func validatePlists() throws { 123 | for plistPath in plistPaths { 124 | try isReadableFilePath(plistPath) 125 | } 126 | } 127 | 128 | func createModernPayload(name: String, plist: NSDictionary) throws -> NSDictionary { 129 | let mutablePayload = plist.mutableCopy() as! NSMutableDictionary 130 | // payload keys 131 | mutablePayload["PayloadIdentifier"] = name 132 | mutablePayload["PayloadType"] = name 133 | mutablePayload["PayloadDisplayName"] = displayName 134 | mutablePayload["PayloadUUID"] = UUID().uuidString 135 | mutablePayload["PayloadVersion"] = payloadVersion 136 | 137 | if !organization.isEmpty { 138 | mutablePayload["PayloadOrganization"] = organization 139 | } 140 | return mutablePayload 141 | } 142 | 143 | func createMCXPayload(name: String, plist: NSDictionary) throws -> NSDictionary { 144 | let mcxPayload = NSMutableDictionary() 145 | mcxPayload["PayloadIdentifier"] = name 146 | mcxPayload["PayloadType"] = "com.apple.ManagedClient.preferences" 147 | mcxPayload["PayloadDisplayName"] = displayName 148 | mcxPayload["PayloadUUID"] = UUID().uuidString 149 | mcxPayload["PayloadVersion"] = payloadVersion 150 | 151 | let prefSettings = NSDictionary(object: plist, forKey: "mcx_preference_settings" as NSString) 152 | let prefArray = NSArray(object: prefSettings) 153 | let forcedDict = NSDictionary(object: prefArray, forKey: "Forced" as NSString) 154 | let domainDict = NSDictionary(object: forcedDict, forKey: name as NSString) 155 | mcxPayload["PayloadContent"] = domainDict 156 | 157 | return mcxPayload 158 | } 159 | 160 | // MARK: run 161 | 162 | mutating func run() throws { 163 | // TODO: if identifer points to a mobile config file, get data from there 164 | try validatePlists() 165 | populateDefaults() 166 | 167 | // Boilerplate keys 168 | let profileDict: NSMutableDictionary = [ 169 | "PayloadIdentifier": identifier, 170 | "PayloadUUID": uuid.uuidString, 171 | "PayloadVersion": payloadVersion, 172 | "PayloadType": payloadType, 173 | "PayloadDisplayName": displayName, 174 | "PayloadScope": payloadScope 175 | ] 176 | 177 | if !organization.isEmpty { 178 | profileDict["PayloadOrganization"] = organization 179 | } 180 | 181 | let payloads = NSMutableArray() 182 | 183 | for plistPath in plistPaths { 184 | 185 | // determine filename from path 186 | let plistURL = URL(fileURLWithPath: plistPath) 187 | let plistname = plistURL.deletingPathExtension().lastPathComponent 188 | guard let plistdict = try? NSDictionary(contentsOf: plistURL, error: ()) 189 | else { 190 | try exit("file at '\(plistPath)' might not be a plist!", code: 65) 191 | } 192 | 193 | let payload = mcx ? try createMCXPayload(name: plistname, plist: plistdict) 194 | : try createModernPayload(name: plistname, plist: plistdict) 195 | 196 | payloads.add(payload) 197 | } 198 | 199 | // insert payloads array 200 | profileDict["PayloadContent"] = payloads 201 | 202 | let profileURL = URL(filePath: outputPath) 203 | try profileDict.write(to: profileURL) 204 | 205 | // TODO: sign profile after creation 206 | 207 | print(profileURL.relativePath) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Armin Briegel 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------