diff --git a/Launchd Package Creator.xcodeproj/project.pbxproj b/Launchd Package Creator.xcodeproj/project.pbxproj index 70793f0..12e9e71 100644 --- a/Launchd Package Creator.xcodeproj/project.pbxproj +++ b/Launchd Package Creator.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ 34496CB42260DD740054265A /* launchd-package-creator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "launchd-package-creator.entitlements"; sourceTree = ""; }; 34496CEF226518780054265A /* extras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = extras.swift; sourceTree = ""; }; 34496CF3226F4CBA0054265A /* create_daemon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = create_daemon.swift; sourceTree = ""; }; + 34FB881822A95B40008E7728 /* Launchd Package Creator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Launchd Package Creator.entitlements"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -101,6 +102,7 @@ 34496C87225FF03F0054265A /* Launchd Package Creator */ = { isa = PBXGroup; children = ( + 34FB881822A95B40008E7728 /* Launchd Package Creator.entitlements */, 34496CB42260DD740054265A /* launchd-package-creator.entitlements */, 34496C88225FF03F0054265A /* AppDelegate.swift */, 34496C8A225FF03F0054265A /* ViewController.swift */, diff --git a/Launchd Package Creator/Base.lproj/Main.storyboard b/Launchd Package Creator/Base.lproj/Main.storyboard index c291861..0101808 100644 --- a/Launchd Package Creator/Base.lproj/Main.storyboard +++ b/Launchd Package Creator/Base.lproj/Main.storyboard @@ -441,11 +441,11 @@ - + - + @@ -454,7 +454,7 @@ - + @@ -463,7 +463,7 @@ - + @@ -472,7 +472,7 @@ - + @@ -481,7 +481,7 @@ - + @@ -490,7 +490,7 @@ - + @@ -499,7 +499,7 @@ - + @@ -507,157 +507,210 @@ - - - - - - - + + - - - + + - + - + - - - - + + + + - + - - - - + + + + + - - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + @@ -682,26 +735,6 @@ - - - - - - - - - - @@ -710,6 +743,9 @@ + + + @@ -726,7 +762,7 @@ - + diff --git a/Launchd Package Creator/Launchd Package Creator.entitlements b/Launchd Package Creator/Launchd Package Creator.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Launchd Package Creator/Launchd Package Creator.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/Launchd Package Creator/ViewController.swift b/Launchd Package Creator/ViewController.swift index 4cf1663..f727ea4 100644 --- a/Launchd Package Creator/ViewController.swift +++ b/Launchd Package Creator/ViewController.swift @@ -28,6 +28,8 @@ var globalPkgTempLocation: String = "" var globalSessionType: String? = nil var emptyRequiredFields = [String]() var usingApp: Bool = false +var globalTargetPathInPlist: String = "" +var globalPackageTarget: Bool = true class ViewController: NSViewController { @@ -48,6 +50,9 @@ class ViewController: NSViewController { @IBOutlet weak var sessionTypeOptions: NSPopUpButton! @IBOutlet weak var sessionTypeCheck: NSButton! @IBOutlet weak var programArgsOptLabel: NSTextField! + @IBOutlet weak var packageAtDefaultRadio: NSButton! + @IBOutlet weak var packageAtSpecifiedRadio: NSButton! + @IBOutlet weak var doNotPackageRadio: NSButton! // Create arrays of fields/buttons to clear or disable when using the "Clear" button lazy var fieldsToClear: [NSTextField] = [self.daemonIdentifier, self.daemonVersion, self.programArgs, self.targetPath, self.startIntervalSeconds, self.standardOutPathField, self.standardErrorPathField] @@ -71,8 +76,8 @@ class ViewController: NSViewController { return } + // Function to open the File Save dialog box func fileSaveDialog(title: String, allowedFileTypes: Array, source: String) { - // Open the File Save dialog box let fileSaveDialog = NSSavePanel(); fileSaveDialog.message = title; fileSaveDialog.showsResizeIndicator = true; @@ -150,7 +155,7 @@ class ViewController: NSViewController { } } - func determineProgramArgs (userSelectedTarget:String) -> String { + func determineProgramArgs(userSelectedTarget:String) -> String { let targetExtension = NSURL(fileURLWithPath: userSelectedTarget).pathExtension // Make sure the program args field is enabled initially programArgs.isEnabled = true @@ -174,13 +179,10 @@ class ViewController: NSViewController { let linesOfTarget = targetContents.components(separatedBy: "\n") let shebang = String(linesOfTarget[0]); if shebang.contains ("bash") { - // shebang contains bash programArgs.stringValue = String("/bin/bash"); } else if shebang.contains ("sh") { - // shebang contains sh programArgs.stringValue = String("/bin/sh"); } else if shebang.contains ("python") { - // shebang contains python programArgs.stringValue = String("/usr/bin/python"); } else { // Either no shebang or we don't know how to deal with the shebang in the file @@ -251,6 +253,30 @@ class ViewController: NSViewController { } } + @IBAction func packageAtDefaultAction(_ sender: Any) { + if packageAtDefaultRadio.state == NSControl.StateValue.on { + packageAtSpecifiedRadio.state = NSControl.StateValue.off + doNotPackageRadio.state = NSControl.StateValue.off + globalPackageTarget = true + } + } + + @IBAction func packageAtSpecifiedAction(_ sender: Any) { + if packageAtSpecifiedRadio.state == NSControl.StateValue.on { + packageAtDefaultRadio.state = NSControl.StateValue.off + doNotPackageRadio.state = NSControl.StateValue.off + globalPackageTarget = true + } + } + + @IBAction func doNotPackageAction(_ sender: Any) { + if doNotPackageRadio.state == NSControl.StateValue.on { + packageAtDefaultRadio.state = NSControl.StateValue.off + packageAtSpecifiedRadio.state = NSControl.StateValue.off + globalPackageTarget = false + } + } + @IBAction func startIntervalAction(_ sender: Any) { if startInterval.state == NSControl.StateValue.on { startIntervalSeconds.isEnabled = true @@ -298,6 +324,9 @@ class ViewController: NSViewController { for button in self.buttonsToClear { button.state = NSControl.StateValue.off } for field in self.fieldsToDisable { field.isEnabled = false } daemonButton.state = NSControl.StateValue.on + packageAtDefaultRadio.state = NSControl.StateValue.on + packageAtSpecifiedRadio.state = NSControl.StateValue.off + doNotPackageRadio.state = NSControl.StateValue.off agentButton.state = NSControl.StateValue.off sessionTypeOptions.isEnabled = false sessionTypeOptions.selectItem(at: 0) @@ -324,6 +353,16 @@ class ViewController: NSViewController { globalVersion = daemonVersion.stringValue globalProgramArgs = programArgs.stringValue globalTargetPath = targetPath.stringValue + globalTargetPathFileName = (globalTargetPath as NSString).lastPathComponent + + // Determine the path where the target will be packaged if applicable + if packageAtDefaultRadio.state == NSControl.StateValue.on { + globalTargetPathInPlist = "/Library/Scripts/\(globalTargetPathFileName)" + } else if packageAtSpecifiedRadio.state == NSControl.StateValue.on { + globalTargetPathInPlist = globalTargetPath + } else if doNotPackageRadio.state == NSControl.StateValue.on { + globalTargetPathInPlist = globalTargetPath + } // Determine state of runAtLoad Checkbox, populate value for plist if runAtLoad.state == NSControl.StateValue.on { @@ -363,12 +402,11 @@ class ViewController: NSViewController { globalSessionType = nil } - // Determine the filename of the target app/script, build the program args array for plist - globalTargetPathFileName = (globalTargetPath as NSString).lastPathComponent + // Build the program args array for plist if programArgs.stringValue != "" { - globalProgramArgsFull = [globalProgramArgs, "/Library/Scripts/\(globalTargetPathFileName)"] + globalProgramArgsFull = [globalProgramArgs, globalTargetPathInPlist] } else { - globalProgramArgsFull = ["/Library/Scripts/\(globalTargetPathFileName)"] + globalProgramArgsFull = [globalTargetPathInPlist] } // Let the user know LaunchAgent/LimitLoadToSessionType: Aqua is preferred for .apps @@ -391,8 +429,6 @@ class ViewController: NSViewController { // User clicked the continue button create_daemon().build(buildType: buildType) // If the intention is to build a PKG, then show the save dialog - //if buildType == "PKG" { _ = self.fileSaveDialog() } - //if buildType == "Plist" { NSWorkspace.shared.openFile(preferencesURL.path, withApplication: "TextEdit") } if buildType == "Plist" { self.fileSaveDialog(title: "Choose a save location for the launchd plist.", allowedFileTypes: ["plist"], source: preferencesURL.path) } @@ -404,8 +440,6 @@ class ViewController: NSViewController { } else { create_daemon().build(buildType: buildType) // If the intention is to build a PKG, then show the save dialog - //if buildType == "PKG" { _ = self.fileSaveDialog() } - //if buildType == "Plist" { NSWorkspace.shared.openFile(preferencesURL.path, withApplication: "TextEdit") } if buildType == "Plist" { self.fileSaveDialog(title: "Choose a save location for the launchd plist.", allowedFileTypes: ["plist"], source: preferencesURL.path) } @@ -442,6 +476,13 @@ class ViewController: NSViewController { // Set focus to Identifier field self.daemonIdentifier.becomeFirstResponder() + + globalPackageTarget = true + } + + // Disable window resizing + override func viewDidAppear() { + view.window!.styleMask.remove(.resizable) } override var representedObject: Any? { diff --git a/Launchd Package Creator/create_daemon.swift b/Launchd Package Creator/create_daemon.swift index cbcc73f..45bafdf 100644 --- a/Launchd Package Creator/create_daemon.swift +++ b/Launchd Package Creator/create_daemon.swift @@ -41,6 +41,7 @@ public class create_daemon: NSObject { var baseTempDir: URL! var sessionTempDir: URL! var componentPlistURL: URL! + var postInstallChownLines: String = "" public func build(buildType: String) { @@ -51,18 +52,20 @@ public class create_daemon: NSObject { preferencesURL = sessionTempDir.appendingPathComponent("/root/Library/\(globalDaemonFolderName)/\(globalIdentifier).plist") componentPlistURL = sessionTempDir.appendingPathComponent("/build/component.plist") - let subPaths = [ + var subPaths = [ "root/Library/\(globalDaemonFolderName)", - "root/Library/Scripts", "scripts", "build", ] + if globalPackageTarget == true { + subPaths.append("root/\((globalTargetPathInPlist as NSString).deletingLastPathComponent)") + } + subPaths.forEach { subPath in let url = sessionTempDir.appendingPathComponent(subPath) do { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) - //print("path created: \(url)") } catch let error { print("error: \(error)") } @@ -70,14 +73,22 @@ public class create_daemon: NSObject { // Create the pkg postinstall script func createPostinstall() { + if globalPackageTarget == true { + postInstallChownLines = """ + chown -R root:wheel "\(globalTargetPathInPlist)" + chmod -R 755 "\(globalTargetPathInPlist)" + """ + } else { + postInstallChownLines = "" + } + let postInstallText = """ #!/bin/bash - # Set permissions on LaunchDaemon and Script + # Set permissions on launchd \(globalDaemonType) files chown root:wheel "/Library/\(globalDaemonFolderName)/\(globalIdentifier).plist" chmod 644 "/Library/\(globalDaemonFolderName)/\(globalIdentifier).plist" - chown -R root:wheel "/Library/Scripts/\(globalTargetPathFileName)" - chmod -R 755 "/Library/Scripts/\(globalTargetPathFileName)" + \(postInstallChownLines) exit 0 """ @@ -102,22 +113,9 @@ public class create_daemon: NSObject { } } -// func encodePlist(PlistData: String, Destination: URL) { -// let preferencesToEncode = ComponentPlist(BundleIsRelocatable: false, BundleIsVersionChecked: false, BundleOverwriteAction: "upgrade", RootRelativeBundlePath: "/Library/Scripts/\(globalTargetPathFileName)") -// let encoder = PropertyListEncoder() -// encoder.outputFormat = .xml -// do { -// let data = try encoder.encode(preferencesToEncode) -// try data.write(to: Destination) -// } catch { -// // Handle error -// print(error) -// } -// // Code goes here -// } - + // Create the component plist func createComponentPlist() { - let preferencesToEncode = ComponentPlist(BundleIsRelocatable: false, BundleIsVersionChecked: false, BundleOverwriteAction: "upgrade", RootRelativeBundlePath: "/Library/Scripts/\(globalTargetPathFileName)") + let preferencesToEncode = ComponentPlist(BundleIsRelocatable: false, BundleIsVersionChecked: false, BundleOverwriteAction: "upgrade", RootRelativeBundlePath: "\(globalTargetPathInPlist)") let encoder = PropertyListEncoder() encoder.outputFormat = .xml do { @@ -131,7 +129,7 @@ public class create_daemon: NSObject { // Copy the target script/app func copyTarget() { - let destinationURL = sessionTempDir.appendingPathComponent("root/Library/Scripts/\(globalTargetPathFileName)") + let destinationURL = sessionTempDir.appendingPathComponent("root/\(globalTargetPathInPlist)") _ = extras().copyFile(source: globalTargetPath, destination: destinationURL.path) } @@ -166,24 +164,24 @@ public class create_daemon: NSObject { let pkgBuildDir = sessionTempDir.appendingPathComponent("build/") globalPkgTempLocation = "\(pkgBuildDir.path)/hello.pkg" - if usingApp == true { + // If we are packaging an app use a component plist, otherwise it is not needed + if (usingApp == true) && (globalPackageTarget == true) { + createComponentPlist() shell("/usr/bin/pkgbuild", "--quiet", "--root", "\(pkgRoot.path)", "--install-location", "/", "--scripts", "\(pkgScripts.path)", "--identifier", "\(globalIdentifier)", "--version", "\(globalVersion)", "--ownership", "recommended", "--component-plist", "\(componentPlistURL.path)", "\(globalPkgTempLocation)") - } else { shell("/usr/bin/pkgbuild", "--quiet", "--root", "\(pkgRoot.path)", "--install-location", "/", "--scripts", "\(pkgScripts.path)", "--identifier", "\(globalIdentifier)", "--version", "\(globalVersion)", "--ownership", "recommended", "\(globalPkgTempLocation)") } - } if buildType == "PKG" { createPostinstall() - createComponentPlist() - copyTarget() + if globalPackageTarget == true { + copyTarget() + } createPlist() createPKG() } else if buildType == "Plist" { createPlist() } - } } diff --git a/images/main_window.png b/images/main_window.png index fc96ae3..16193ca 100644 Binary files a/images/main_window.png and b/images/main_window.png differ