Skip to content

Commit 29d4bea

Browse files
committed
started work on plist2profile
1 parent bab2995 commit 29d4bea

File tree

3 files changed

+180
-1
lines changed

3 files changed

+180
-1
lines changed

.swiftpm/xcode/xcshareddata/xcschemes/plist2profile.xcscheme

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@
5050
ReferencedContainer = "container:">
5151
</BuildableReference>
5252
</BuildableProductRunnable>
53+
<CommandLineArguments>
54+
<CommandLineArgument
55+
argument = "com.scriptingosx.example"
56+
isEnabled = "YES">
57+
</CommandLineArgument>
58+
<CommandLineArgument
59+
argument = "/Users/armin/BTSync/Work/Projects/setup-manager-private/Setup\ Manager/Documentation/sample-com.jamf.setupmanager.plist"
60+
isEnabled = "YES">
61+
</CommandLineArgument>
62+
</CommandLineArguments>
5363
</LaunchAction>
5464
<ProfileAction
5565
buildConfiguration = "Release"

Sources/plist2profile/Plist2Profile.swift

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,162 @@ struct Plist2Profile: ParsableCommand {
1313
static var configuration = CommandConfiguration(
1414
commandName: "plist2profile",
1515
abstract: "converts a standard preference plist file to a mobileconfig profile",
16+
usage: "plist2profile <identifier> <plist> ...",
1617
version: "0.1"
1718
)
19+
20+
// MARK: arguments,options, flags
21+
@Argument(
22+
help: ArgumentHelp(
23+
"the payload identifier for the profile",
24+
valueName: "identifier"
25+
)
26+
)
27+
var identifier: String
28+
29+
@Argument(
30+
help: ArgumentHelp(
31+
"Path to a plist to be added as a profile payload. Can be specified multiple times.",
32+
valueName: "plist"
33+
)
34+
)
35+
var plistPaths: [String]
36+
37+
@Option(
38+
name: [.customShort("g"), .customLong("organization")],
39+
help: "Cosmetic name for the organization deploying the profile."
40+
)
41+
var organization = ""
42+
43+
@Option(
44+
name: [.customShort("o"), .customLong("output")],
45+
help: "Output path for profile. Defaults to 'identifier.mobileconfig' in the current working directory."
46+
)
47+
var outputPath = ""
48+
49+
@Option(
50+
name: [.customShort("d"), .customLong("displayname")],
51+
help: "Display name for profile. Defaults to 'plist2profile: <first domain>'."
52+
)
53+
var displayName = ""
54+
55+
// TODO: option to create a modern or mcx profile
56+
57+
// MARK: variables
58+
59+
var uuid = UUID()
60+
var payloadVersion = 1
61+
var payloadType = "Configuration"
62+
var payloadScope = "System" // or "User"
63+
64+
// TODO: missing keys for profile
65+
// payload scope
66+
// removal disallowed
67+
// removalDate, duration until removal
68+
// description
69+
//
70+
// all of these should at least be grabbed when initialising from a file
71+
//
72+
73+
// MARK: functions
74+
75+
// TODO: can we put these functions in shared file? Can we share files between targets in a package without creating a library?
76+
77+
func exit(_ message: Any, code: Int32 = 1) throws -> Never {
78+
print(message)
79+
throw ExitCode(code)
80+
}
81+
82+
func isReadableFilePath(_ path: String) throws {
83+
let fm = FileManager.default
84+
var isDirectory: ObjCBool = false
85+
if !fm.fileExists(atPath: path, isDirectory: &isDirectory) {
86+
try exit("no file at path '\(path)'!", code: 66)
87+
}
88+
if isDirectory.boolValue {
89+
try exit("path '\(path)' is a directory", code: 66)
90+
}
91+
if !fm.isReadableFile(atPath: path) {
92+
try exit("cannot read file at '\(path)'!", code: 66)
93+
}
94+
}
95+
96+
mutating func populateDefaults() {
97+
// if displayName is empty, populate
98+
if displayName.isEmpty {
99+
displayName = "plist2Profile: \(identifier)"
100+
}
101+
102+
// if output is empty, generate file name
103+
if outputPath.isEmpty {
104+
outputPath = identifier.appending(".plist")
105+
}
106+
}
107+
108+
func validatePlists() throws {
109+
for plistPath in plistPaths {
110+
try isReadableFilePath(plistPath)
111+
}
112+
}
113+
114+
func createModernPayload(plistPath: String) throws -> NSDictionary {
115+
let payloadUUID = UUID()
116+
// determine filename from path
117+
let plistURL = URL(fileURLWithPath: plistPath)
118+
let plistname = plistURL.deletingPathExtension().lastPathComponent
119+
guard let payload = try? NSMutableDictionary(contentsOf: plistURL, error: ())
120+
else {
121+
try exit("file at '\(plistPath)' might not be a plist!", code: 65)
122+
}
123+
// payload keys
124+
payload["PayloadIdentifier"] = plistname
125+
payload["PayloadType"] = plistname
126+
payload["PayloadDisplayName"] = "\(displayName): \(plistname)"
127+
payload["PayloadUUID"] = payloadUUID.description
128+
payload["PayloadVersion"] = payloadVersion
129+
130+
if !organization.isEmpty {
131+
payload["PayloadOrganization"] = organization
132+
}
133+
return payload
134+
}
135+
136+
// MARK: run
137+
138+
mutating func run() throws {
139+
// TODO: if identifer points to a mobile config file, get data from there
140+
try validatePlists()
141+
populateDefaults()
18142

19-
func run() {
20143
print("Hello, plist2profile!")
144+
145+
// Boilerplate
146+
let profileDict: NSMutableDictionary = [
147+
"PayloadIdentifier": identifier,
148+
"PayloadUUID": uuid.description,
149+
"PayloadVersion": payloadVersion,
150+
"PayloadType": payloadType,
151+
"PayloadDisplayName": displayName,
152+
"PayloadScope": payloadScope
153+
]
154+
155+
if !organization.isEmpty {
156+
profileDict["PayloadOrganization"] = organization
157+
}
158+
159+
let payloads = NSMutableArray()
160+
161+
for plistPath in plistPaths {
162+
let payload = try createModernPayload(plistPath: plistPath)
163+
payloads.add(payload)
164+
165+
// insert payloads array
166+
profileDict["PayloadContent"] = payloads
167+
}
168+
169+
guard let plistData = try? PropertyListSerialization.data(fromPropertyList: profileDict, format: .xml, options: .zero)
170+
else { try exit("could generate property list", code: 73) }
171+
172+
print(String(data: plistData, encoding: .utf8) ?? "<no data>")
21173
}
22174
}

com.scriptingosx.example.plist

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>sample key 1</key>
6+
<string>value one</string>
7+
<key>sample key 2</key>
8+
<string>second value</string>
9+
<key>sample key 3</key>
10+
<dict>
11+
<key>subkey 1</key>
12+
<string>sub value</string>
13+
<key>subkey 2</key>
14+
<string>other sub value</string>
15+
</dict>
16+
</dict>
17+
</plist>

0 commit comments

Comments
 (0)