diff --git a/.gitignore b/.gitignore index e0dcece..d4a31bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -*.pyc .DS_Store -*.pkg \ No newline at end of file +*.pkg +*.pyc +xcuserdata/ diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..bba0976 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,45 @@ +disabled_rules: # rule identifiers turned on by default to exclude from running + - todo + +#opt_in_rules: # some rules are turned off by default, so you need to opt-in +# - empty_count # Find all the available rules by running: `swiftlint rules` + +# Alternatively, specify all rules explicitly by uncommenting this option: +# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this +# - empty_parameters +# - vertical_whitespace + +#analyzer_rules: # Rules run by `swiftlint analyze` +# - explicit_self + +# If true, SwiftLint will not fail if no lintable files are found. +allow_zero_lintable_files: false + +# configurable rules can be customized from this configuration file +# line_length: 120 +# they can set both implicitly with an array +# type_body_length: +# - 300 # warning +# - 400 # error +# or they can set both explicitly +#file_length: +# warning: 500 +# error: 1200 +# naming rules can set warnings/errors for min_length and max_length +# additionally they can set excluded names +type_name: + min_length: 3 # only warning + max_length: # warning and error + warning: 30 + error: 50 + excluded: iPhone # excluded via string + allowed_symbols: ["_"] # these are allowed in type names +identifier_name: + min_length: # only min_length + error: 3 # only error + excluded: # excluded via string array + - id + - URL + - x + - y +reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary) \ No newline at end of file diff --git a/quickpkg b/quickpkg.py similarity index 100% rename from quickpkg rename to quickpkg.py diff --git a/quickpkg.xcodeproj/project.pbxproj b/quickpkg.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f93434d --- /dev/null +++ b/quickpkg.xcodeproj/project.pbxproj @@ -0,0 +1,363 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + C66ED2EB2A82AED300E2FE12 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66ED2EA2A82AED300E2FE12 /* Extensions.swift */; }; + C66ED2ED2A83737F00E2FE12 /* AppMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66ED2EC2A83737F00E2FE12 /* AppMetadata.swift */; }; + C66ED2EF2A837D8200E2FE12 /* Process-Launch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66ED2EE2A837D8200E2FE12 /* Process-Launch.swift */; }; + C66ED30D2A83CA4D00E2FE12 /* DMGHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66ED30C2A83CA4D00E2FE12 /* DMGHelper.swift */; }; + C6A91AFC2A824B2600EB41D4 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C6A91AFB2A824B2600EB41D4 /* ArgumentParser */; }; + C6A91AFE2A824B9F00EB41D4 /* quickpkg.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A91AFD2A824B9F00EB41D4 /* quickpkg.swift */; }; + C6A91B002A8266B100EB41D4 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A91AFF2A8266B100EB41D4 /* Constants.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + C6A91AEE2A824AAF00EB41D4 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + C66ED2EA2A82AED300E2FE12 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + C66ED2EC2A83737F00E2FE12 /* AppMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMetadata.swift; sourceTree = ""; }; + C66ED2EE2A837D8200E2FE12 /* Process-Launch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Process-Launch.swift"; sourceTree = ""; }; + C66ED30C2A83CA4D00E2FE12 /* DMGHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMGHelper.swift; sourceTree = ""; }; + C6A91AF02A824AAF00EB41D4 /* quickpkg */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = quickpkg; sourceTree = BUILT_PRODUCTS_DIR; }; + C6A91AFD2A824B9F00EB41D4 /* quickpkg.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = quickpkg.swift; sourceTree = ""; }; + C6A91AFF2A8266B100EB41D4 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + C6A91AED2A824AAF00EB41D4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C6A91AFC2A824B2600EB41D4 /* ArgumentParser in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + C6A91AE72A824AAF00EB41D4 = { + isa = PBXGroup; + children = ( + C6A91AF22A824AAF00EB41D4 /* quickpkg */, + C6A91AF12A824AAF00EB41D4 /* Products */, + ); + sourceTree = ""; + }; + C6A91AF12A824AAF00EB41D4 /* Products */ = { + isa = PBXGroup; + children = ( + C6A91AF02A824AAF00EB41D4 /* quickpkg */, + ); + name = Products; + sourceTree = ""; + }; + C6A91AF22A824AAF00EB41D4 /* quickpkg */ = { + isa = PBXGroup; + children = ( + C6A91AFD2A824B9F00EB41D4 /* quickpkg.swift */, + C66ED30C2A83CA4D00E2FE12 /* DMGHelper.swift */, + C66ED2EC2A83737F00E2FE12 /* AppMetadata.swift */, + C6A91AFF2A8266B100EB41D4 /* Constants.swift */, + C66ED2EA2A82AED300E2FE12 /* Extensions.swift */, + C66ED2EE2A837D8200E2FE12 /* Process-Launch.swift */, + ); + path = quickpkg; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C6A91AEF2A824AAF00EB41D4 /* quickpkg */ = { + isa = PBXNativeTarget; + buildConfigurationList = C6A91AF72A824AAF00EB41D4 /* Build configuration list for PBXNativeTarget "quickpkg" */; + buildPhases = ( + C66ED30B2A83C6AD00E2FE12 /* Swiftlint */, + C6A91AEC2A824AAF00EB41D4 /* Sources */, + C6A91AED2A824AAF00EB41D4 /* Frameworks */, + C6A91AEE2A824AAF00EB41D4 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = quickpkg; + packageProductDependencies = ( + C6A91AFB2A824B2600EB41D4 /* ArgumentParser */, + ); + productName = quickpkg; + productReference = C6A91AF02A824AAF00EB41D4 /* quickpkg */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C6A91AE82A824AAF00EB41D4 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + C6A91AEF2A824AAF00EB41D4 = { + CreatedOnToolsVersion = 14.3.1; + LastSwiftMigration = 1430; + }; + }; + }; + buildConfigurationList = C6A91AEB2A824AAF00EB41D4 /* Build configuration list for PBXProject "quickpkg" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = C6A91AE72A824AAF00EB41D4; + packageReferences = ( + C6A91AFA2A824B2600EB41D4 /* XCRemoteSwiftPackageReference "swift-argument-parser" */, + ); + productRefGroup = C6A91AF12A824AAF00EB41D4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C6A91AEF2A824AAF00EB41D4 /* quickpkg */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + C66ED30B2A83C6AD00E2FE12 /* Swiftlint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = Swiftlint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint > /dev/null; then\n swiftlint --fix && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C6A91AEC2A824AAF00EB41D4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C6A91AFE2A824B9F00EB41D4 /* quickpkg.swift in Sources */, + C66ED2EF2A837D8200E2FE12 /* Process-Launch.swift in Sources */, + C66ED30D2A83CA4D00E2FE12 /* DMGHelper.swift in Sources */, + C66ED2EB2A82AED300E2FE12 /* Extensions.swift in Sources */, + C66ED2ED2A83737F00E2FE12 /* AppMetadata.swift in Sources */, + C6A91B002A8266B100EB41D4 /* Constants.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C6A91AF52A824AAF00EB41D4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + C6A91AF62A824AAF00EB41D4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + C6A91AF82A824AAF00EB41D4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = JME5BW3F3R; + ENABLE_HARDENED_RUNTIME = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + C6A91AF92A824AAF00EB41D4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = JME5BW3F3R; + ENABLE_HARDENED_RUNTIME = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C6A91AEB2A824AAF00EB41D4 /* Build configuration list for PBXProject "quickpkg" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C6A91AF52A824AAF00EB41D4 /* Debug */, + C6A91AF62A824AAF00EB41D4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C6A91AF72A824AAF00EB41D4 /* Build configuration list for PBXNativeTarget "quickpkg" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C6A91AF82A824AAF00EB41D4 /* Debug */, + C6A91AF92A824AAF00EB41D4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + C6A91AFA2A824B2600EB41D4 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-argument-parser.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + C6A91AFB2A824B2600EB41D4 /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = C6A91AFA2A824B2600EB41D4 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = C6A91AE82A824AAF00EB41D4 /* Project object */; +} diff --git a/quickpkg.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/quickpkg.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/quickpkg.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/quickpkg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/quickpkg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/quickpkg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/quickpkg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/quickpkg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..d3fbce1 --- /dev/null +++ b/quickpkg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "59ba1edda695b389d6c9ac1809891cd779e4024f505b0ce1a9d5202b6762e38a", + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + } + ], + "version" : 3 +} diff --git a/quickpkg.xcodeproj/project.xcworkspace/xcuserdata/armin.xcuserdatad/UserInterfaceState.xcuserstate b/quickpkg.xcodeproj/project.xcworkspace/xcuserdata/armin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..c1f00d2 Binary files /dev/null and b/quickpkg.xcodeproj/project.xcworkspace/xcuserdata/armin.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/quickpkg.xcodeproj/xcshareddata/xcschemes/quickpkg.xcscheme b/quickpkg.xcodeproj/xcshareddata/xcschemes/quickpkg.xcscheme new file mode 100644 index 0000000..2359fde --- /dev/null +++ b/quickpkg.xcodeproj/xcshareddata/xcschemes/quickpkg.xcscheme @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickpkg.xcodeproj/xcuserdata/armin.xcuserdatad/xcschemes/xcschememanagement.plist b/quickpkg.xcodeproj/xcuserdata/armin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..0d9a1b5 --- /dev/null +++ b/quickpkg.xcodeproj/xcuserdata/armin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + quickpkg.xcscheme_^#shared#^_ + + orderHint + 0 + + + SuppressBuildableAutocreation + + C6A91AEF2A824AAF00EB41D4 + + primary + + + + + diff --git a/quickpkg/AppMetadata.swift b/quickpkg/AppMetadata.swift new file mode 100644 index 0000000..0c2577f --- /dev/null +++ b/quickpkg/AppMetadata.swift @@ -0,0 +1,37 @@ +// +// AppMetadata.swift +// quickpkg +// +// Created by Armin Briegel on 2023-08-09. +// + +import Foundation + +struct AppMetadata { + let name: String + let version: String + let identifier: String + let minOSVersion: String + + init?(url: URL) { + guard let appBundle = Bundle(url: url) else { return nil } + + let identifier = appBundle.bundleIdentifier + guard identifier != nil else { return nil } + self.identifier = identifier! + + self.name = appBundle.object(forInfoDictionaryKey: "CFBundleName") as? String ?? + appBundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? + url.basename + + // some apps have _empty_ CFBundleShortVersions + var version = appBundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + if version.isNilOrEmpty { + version = appBundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String + } + self.version = version ?? "" + + // since we are using the minOSVersion for building flat packages, we can default to 10.5 + self.minOSVersion = appBundle.object(forInfoDictionaryKey: "LSMinimumSystemVersion") as? String ?? "10.5" + } +} diff --git a/quickpkg/Constants.swift b/quickpkg/Constants.swift new file mode 100644 index 0000000..183b5fb --- /dev/null +++ b/quickpkg/Constants.swift @@ -0,0 +1,15 @@ +// +// Constants.swift +// quickpkg +// +// Created by Armin Briegel on 2023-08-08. +// + +import Foundation + +struct Constants { + static let version = "2.0beta" + static let supportedExtensions = ["dmg", "app", "zip", "xip"] + static let pkgbuild = "/usr/bin/pkgbuild" + static let productbuild = "/usr/bin/productbuild" +} diff --git a/quickpkg/DMGHelper.swift b/quickpkg/DMGHelper.swift new file mode 100644 index 0000000..c5c0dee --- /dev/null +++ b/quickpkg/DMGHelper.swift @@ -0,0 +1,18 @@ +// +// DMGHelper.swift +// quickpkg +// +// Created by Armin Briegel on 2023-08-09. +// + +import Foundation + +class DMGHelper { + static var shared = DMGHelper() + + var volumes: [String] = [] + + func attach(_ url: URL) { + + } +} diff --git a/quickpkg/Extensions.swift b/quickpkg/Extensions.swift new file mode 100644 index 0000000..c943045 --- /dev/null +++ b/quickpkg/Extensions.swift @@ -0,0 +1,39 @@ +// +// Extensions.swift +// quickpkg +// +// Created by Armin Briegel on 2023-08-08. +// + +import Foundation + +extension String { + var expandingTildeInPath: String { + NSString(string: self).expandingTildeInPath as String + } +} + +extension URL { + var basename: String { + self.deletingPathExtension().lastPathComponent + } +} + +extension FileManager { + var currentDirectoryURL: URL { + URL(filePath: self.currentDirectoryPath) + } + + func fileExistsAndIsDirectory(atPath path: String) -> Bool { + var isDir: ObjCBool = false + let fileExists = self.fileExists(atPath: path, isDirectory: &isDir) + return fileExists && isDir.boolValue + } +} + +// from: https://www.swiftbysundell.com/articles/extending-optionals-in-swift/ +extension Optional where Wrapped: Collection { + var isNilOrEmpty: Bool { + return self?.isEmpty ?? true + } +} diff --git a/quickpkg/Process-Launch.swift b/quickpkg/Process-Launch.swift new file mode 100644 index 0000000..9dcc621 --- /dev/null +++ b/quickpkg/Process-Launch.swift @@ -0,0 +1,118 @@ +// +// Process-Launch.swift +// ProfilesReader +// +// Created by Armin Briegel on 2023-05-04. +// + +import Foundation + +extension String { + /// convenience initialiser for optional Data; returns nil when the data object is nil */ + init?(data: Data?, encoding: String.Encoding) { + if let data = data { + self.init(data: data, encoding: encoding) + } else { + return nil + } + } +} + +// TODO: use this code when we run into threading issues: +// https://developer.apple.com/forums/thread/690310 + +extension Process { + /// container struct for the information returned from launch() + struct LaunchData { + /// the exit code of the command + let exitCode: Int + /// the contents of standard out as Data + let standardOutData: Data? + /// the contents of standard error as Data + let standardErrorData: Data? + /// the contents of standard out as a utf8 encoded String + var standardOutString: String? { String(data: standardOutData, encoding: encoding) } + /// the contents of standard error as a utf8 encoded String + var standardErrorString: String? { String(data: standardErrorData, encoding: encoding) } + + let encoding: String.Encoding = .utf8 + } + + typealias LaunchResult = Result + + /// runs the command with the arguments + /// - Parameters: + /// - path: absolute file path to the command + /// - arguments: optional array of arguments for the command + /// - terminationHandler: code block that is run when command is finished + static func launch( + path: String, + arguments: [String] = [], + terminationHandler: @escaping (LaunchResult) -> Void + ) { + let process = Process() + let outPipe = Pipe() + let errorPipe = Pipe() + process.standardOutput = outPipe + process.standardError = errorPipe + process.arguments = arguments + process.launchPath = path + process.terminationHandler = { process in + let outData = outPipe.fileHandleForReading.readDataToEndOfFile() + let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + let exitCode = Int(process.terminationStatus) + let data = LaunchData( + exitCode: exitCode, + standardOutData: outData, + standardErrorData: errorData + ) + terminationHandler(.success(data)) + } + do { + try process.run() + } catch { + terminationHandler(.failure(error)) + } + } + + /// runs the command with the arguments + /// - Parameters: + /// - url: file url with the absolute file path to the command + /// - arguments: optional array of arguments for the command + /// - terminationHandler: code block that is run when command is finished + static func launch( + _ url: URL, + arguments: [String] = [], + terminationHandler: @escaping (LaunchResult) -> Void + ) { + launch(path: url.path, arguments: arguments, terminationHandler: terminationHandler) + } + + /// runs the command with the arguments + /// - Parameters: + /// - path: absolute file path to the command + /// - arguments: optional array of arguments for the command + /// - Returns: LaunchResult struct with the data from the command + static func launch( + path: String, + arguments: [String] = [] + ) async -> (LaunchResult) { + await withCheckedContinuation { continuation in + launch(path: path, arguments: arguments) { result in + continuation.resume(returning: (result)) + } + } + } + + /// runs the command with the arguments + /// - Parameters: + /// - url: file url with the absolute file path to the command + /// - arguments: optional array of arguments for the command + /// - Returns: LaunchResult struct with the data from the command + static func launch( + _ url: URL, + arguments: [String] = [] + ) async -> (LaunchResult) { + return await launch(path: url.path, arguments: arguments) + } +} diff --git a/quickpkg/quickpkg.swift b/quickpkg/quickpkg.swift new file mode 100644 index 0000000..f43dafe --- /dev/null +++ b/quickpkg/quickpkg.swift @@ -0,0 +1,316 @@ +// +// quickpkg.swift +// quickpkg +// +// Created by Armin Briegel on 2023-08-08. +// + +import Foundation +import ArgumentParser + +@main +struct QuickPkg: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Build installer packages from apps or archives.", + usage: "quickpkg [options] ", + discussion: "Attempts to build an installation package from the input. Input can be a dmg, zip, or app.", + version: Constants.version + ) + + // MARK: Arguments and Options + + @Argument(help: ArgumentHelp( + "Path to the item to build a installer pkg from.", + valueName: "installer-item")) + var itemPath: String + + struct ScriptsOptions: ParsableArguments { + @Option(name: .customLong("scripts"), + help: "Path to a folder with scripts.") + var scriptsFolder: String? + + @Option(name: .customLong("preinstall"), + help: "Path to the preinstall script.") + var preinstall: String? + + @Option(name: .customLong("postinstall"), + help: "Path to the postinstall script.") + var postinstall: String? + } + + @OptionGroup(title: "Installation Scripts") + var scriptsOptions: ScriptsOptions + + struct SignOptions: ParsableArguments { + @Option(name: .long, help: "Adds a digital signature to the resulting package.") + var sign: String? + + @Option(name: .long, help: "Specify a specific keychain to search for the signing identity.") + var keychain: String? + + @Option(name: .long, help: "Specify an intermediate certificate to be embedded in the package.") + var cert: String? + } + + @OptionGroup(title: "Signing") + var signOptions: SignOptions + + @Option(name: .long, help: "Install-location for the resulting pkg.") + var installLocation: String = "/Applications" + + @Option(help: """ +Path where the package file will be created. +You can use '{name}', '{version}' and '{identifier}' as placeholders. +If this is a directory, then the package will be created with the default filename {name}-{version}.pkg +""") + var output: String? + + enum Ownership: String, ExpressibleByArgument { + case recommended, preserve, preserveOther = "preserve-other" + } + + @Option(name: .long, help: "Sets the ownership.") + var ownership: Ownership? + + @Flag(inversion: .prefixedNo, help: ArgumentHelp("Clean up temp files.", visibility: .hidden)) + var clean = true + + @Flag(inversion: .prefixedNo, help: "Sets BundleIsRelocatable in the PackageInfo.") + var relocatable = false + + @Flag(name: .shortAndLong, help: "Controls amount of logging output (max -vvv).") + var verbosity: Int + + // MARK: Properties + + var sourceAppURL: URL? + + lazy var tempDir: URL = { + var randomNumber = Int.random(in: 1000000...9999999) + var tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent( + "quickpkg.\(randomNumber)", + isDirectory: true + ) + while FileManager.default.fileExists(atPath: tempDir.path) { + randomNumber += 1 + tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent( + "quickpkg.\(randomNumber)", + isDirectory: true + ) + } + do { + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: false) + return tempDir + } catch { + cleanupAndExit("Could not create temporary directory at \(tempDir.path)!", code: 1) + } + }() + + lazy var payloadDir: URL = { + let payloadDir = tempDir.appendingPathComponent("payload", isDirectory: true) + do { + try FileManager.default.createDirectory(at: payloadDir, withIntermediateDirectories: false) + return payloadDir + } catch { + cleanupAndExit("Could not create payload directory at \(payloadDir.path)!", code: 1) + } + + }() + + lazy var scriptsDir: URL = { + tempDir.appendingPathComponent("scripts", isDirectory: true) + }() + + lazy var itemURL: URL = URL(filePath: itemPath) + + // MARK: functions + + mutating func cleanupAndExit(_ text: String = "", code: Int32 = 0) -> Never { + let message = text.isEmpty ? "Exit Code \(code)" : text + log(message, level: 0) + + // delete tmp files, respecting options + if clean { + try? FileManager.default.removeItem(at: tempDir) + } + + if code != 0 { + Self.exit(withError: ExitCode(code)) + } + Self.exit() + } + + func log(_ message: String, level: Int = 1) { + if level <= verbosity { + print(message) + } + } + + mutating func createComponentPlist(app: AppMetadata) async -> URL { + let plist = tempDir.appending(component: "\(app.identifier).plist") + + let arguments: [String] = [ "--analyze", + "--root", payloadDir.path, + "--identifier", app.identifier, + "--version", app.version, + "--install-location", installLocation, + plist.path] + + log("Analyzing \(app.name)") + log("pkgbuild \(arguments.joined(separator: " "))", level: 2) + let result = await Process.launch(path: Constants.pkgbuild, arguments: arguments) + switch result { + case .success(let data): + if data.exitCode != 0 { + cleanupAndExit("An error occured while analyzing app.name: \(data.exitCode)", code: 6) + } + + if !relocatable { + do { + let components = try NSMutableArray(contentsOf: plist, error: ()) + for anyComponent in components { + if let component = anyComponent as? NSMutableDictionary { + component.setValue(false, forKey: "BundleIsRelocatable") + } + } + try components.write(to: plist) + } catch { + cleanupAndExit("Error updating component plist!", code: 6) + } + } + + return plist + case .failure: + cleanupAndExit("couldn't launch pkgbuild!", code: 5) + } + } + + func outputURL(pkgName: String) -> URL { + // default behavior, create file relative to CWD + var outputURL: URL = URL(filePath: pkgName, relativeTo: FileManager.default.currentDirectoryURL) + // if output variable is set, generate based on that + if let output { + let expandedOutput = output.expandingTildeInPath + outputURL = URL(filePath: expandedOutput, relativeTo: FileManager.default.currentDirectoryURL) + + if FileManager.default.fileExistsAndIsDirectory(atPath: outputURL.path) { + outputURL.append(component: pkgName) + } + + if outputURL.pathExtension != "pkg" { + outputURL.appendPathExtension("pkg") + } + } + return outputURL + } + + mutating func buildPKG(app: AppMetadata) async -> URL { + // create the component plist + let componentPlist = await createComponentPlist(app: app) + + // prepare pkgbuild command + let pkgName = "\(app.name)-\(app.version).pkg".replacingOccurrences(of: " ", with: "") + // TODO: re-implement substitution logic + + let outputURL = outputURL(pkgName: pkgName) + + var arguments = ["--root", payloadDir.path, + "--component-plist", componentPlist.path, + "--identifier", app.identifier, + "--version", app.version, + "--install-location", installLocation, + outputURL.path] + // prepare scripts folder + // TODO: parse scripts arguments and create folder + + // add signing options + if let sign = signOptions.sign { + arguments.append(contentsOf: ["--sign", sign]) + } + + if let keychain = signOptions.keychain { + arguments.append(contentsOf: ["--keychain", keychain]) + } + + if let cert = signOptions.cert { + arguments.append(contentsOf: ["--cert", cert]) + } + + // run pkgbuild command + log("Building \(pkgName)") + log("pkgbuild \(arguments.joined(separator: " "))", level: 2) + let result = await Process.launch(path: Constants.pkgbuild, arguments: arguments) + switch result { + case .success(let data): + if verbosity > 0 && !data.standardOutString.isNilOrEmpty { + print(data.standardOutString ?? "") + } + if data.exitCode != 0 { + cleanupAndExit("Error building pkg!", code: 7) + } + return outputURL + case .failure: + cleanupAndExit("could not launch pkgbuild", code: 8) + } + } + + // MARK: main + mutating func run() async { + // remove trailing '/' + if itemPath.hasSuffix("/") { + itemPath = String(itemPath.dropLast()) + } + + // expand homedir tilde + itemPath = itemPath.expandingTildeInPath + + if !Constants.supportedExtensions.contains(itemURL.pathExtension) { + cleanupAndExit("\(itemURL.pathExtension) is not a supported file type!", code: 1) + } + + if !FileManager.default.fileExists(atPath: itemPath) { + cleanupAndExit("Nothing found at \(itemPath)!", code: 41) + } + + // extract app path from itemPath + switch itemURL.pathExtension { + case "app": + sourceAppURL = itemURL + case "dmg": + break + default: + cleanupAndExit("Re-packaging '\(itemURL.pathExtension)' is not implemented yet!", code: 99) + } + + guard let sourceAppURL else { + cleanupAndExit("Could not determine app.", code: 4) + } + + log("found app \(sourceAppURL.path)") + + // get metadata from app + guard let appData = AppMetadata(url: sourceAppURL) else { + cleanupAndExit("Couldn't get App Metadata", code: 5) + } + + log("Name: \(appData.name), id: \(appData.identifier), version: \(appData.version), minOS: \(appData.minOSVersion)") + + // copy or move app + let destAppURL = payloadDir.appendingPathComponent(itemURL.lastPathComponent) + do { + log("copying to \(destAppURL.path)") + try FileManager.default.copyItem(at: sourceAppURL, to: destAppURL) + } catch { + cleanupAndExit("could not create a copy of /(sourceAppURL)", code: 5) + } + + // build pkg + let outputURL = await buildPKG(app: appData) + print("Wrote package to \(outputURL.path)") + + // cleanup + cleanupAndExit("Done!") + } +} diff --git a/testpost.sh b/test/testpost.sh similarity index 100% rename from testpost.sh rename to test/testpost.sh diff --git a/testpre.sh b/test/testpre.sh similarity index 100% rename from testpre.sh rename to test/testpre.sh diff --git a/testscripts/postinstall b/test/testscripts/postinstall similarity index 100% rename from testscripts/postinstall rename to test/testscripts/postinstall diff --git a/testscripts/preinstall b/test/testscripts/preinstall similarity index 100% rename from testscripts/preinstall rename to test/testscripts/preinstall diff --git a/todo.txt b/todo.txt index c495142..d377545 100644 --- a/todo.txt +++ b/todo.txt @@ -1,11 +1,12 @@ -- move to python3 -- grab minimum OS version from app bundle and use when building pkg -- identify shell scripts and build a payload free package +- [ ] move to swift +- [ ] grab minimum OS version from app bundle and use when building pkg +- [ ] create dist pkg by default, component as option +- [ ] signing and notarization as options +- [ ] modern compression as option +- [ ] identify shell scripts and build a payload free package - problems with this: - how to determine if the given file is a script? executable bit? parse the #! ? - how to choose id and version? (for true payload free packages, this may not matter since they don't leave a receipt anyway?) -- use some preference setting to determine default package name syntax -- support for tar, gzip and bzip -? other possible file formats: prefpanes, Safari extensions? -? identify app just by name or id (could use: mdfind "kMDItemKind == 'Application' && kMDItemDisplayName == 'iTunes'") -? identify mobileconfigs and build a package installer if make-profile-pkg is present +- [ ] use some preference setting to determine default package name syntax +- [ ] support for tar, gzip and bzip +- [ ] ? identify app just by name or id (could use: mdfind "kMDItemKind == 'Application' && kMDItemDisplayName == 'iTunes'")