├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── build_outset_prerelease_manual.yml
│ └── build_outset_release_manual.yml
├── .gitignore
├── .swiftlint.yml
├── CHANGELOG.md
├── LICENSE.md
├── MDM
└── Jamf Pro
│ └── service-status-ea.sh
├── Outset.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcshareddata
│ └── xcschemes
│ │ ├── Outset App Bundle.xcscheme
│ │ └── Outset Installer Package.xcscheme
└── xcuserdata
│ └── rea094.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ └── xcschememanagement.plist
├── Outset
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Outset.png
│ │ ├── Outset.png_128x128.png
│ │ ├── Outset.png_128x128@2x.png
│ │ ├── Outset.png_16x16.png
│ │ ├── Outset.png_16x16@2x.png
│ │ ├── Outset.png_256x256.png
│ │ ├── Outset.png_256x256@2x.png
│ │ ├── Outset.png_32x32.png
│ │ ├── Outset.png_32x32@2x.png
│ │ └── Outset.png_512x512.png
│ └── Contents.json
├── Extensions
│ ├── Data+additions.swift
│ ├── String+additions.swift
│ └── URL+additions.swift
├── Info.plist
├── Outset.entitlements
├── Outset.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
└── Utils
│ ├── Checksum.swift
│ ├── FileUtils.swift
│ ├── ItemProcessing.swift
│ ├── Logging.swift
│ ├── Network.swift
│ ├── Preferences.swift
│ ├── Services.swift
│ ├── ShellUtils.swift
│ ├── SystemInfo.swift
│ └── SystemUtils.swift
├── OutsetInstaller
└── OutsetInstaller.entitlements
├── Package
├── Library
│ ├── LaunchAgents
│ │ ├── io.macadmins.Outset.login-window.plist
│ │ ├── io.macadmins.Outset.login.plist
│ │ └── io.macadmins.Outset.on-demand.plist
│ └── LaunchDaemons
│ │ ├── io.macadmins.Outset.boot.plist
│ │ ├── io.macadmins.Outset.cleanup.plist
│ │ └── io.macadmins.Outset.login-privileged.plist
├── Scripts
│ ├── postinstall
│ └── preinstall
├── generatePackage.zsh
├── outset
└── outset-pkg
├── README.md
├── build_outset.zsh
└── legacy
├── .flake8
├── Makefile
├── README.md
├── Requirements.plist
├── custom-ondemand
├── Makefile
├── pkgroot
│ └── usr
│ │ └── local
│ │ └── outset
│ │ ├── login-every
│ │ └── login-every_example.py
│ │ ├── login-once
│ │ └── login-once_example.py
│ │ └── on-demand
│ │ └── on-demand_example.sh
└── scripts
│ └── postinstall
├── custom-outset
├── Makefile
└── pkgroot
│ └── usr
│ └── local
│ └── outset
│ ├── boot-every
│ └── boot-every_example.py
│ ├── boot-once
│ └── boot-once_example.py
│ ├── login-every
│ └── login-every_example.py
│ ├── login-once
│ └── login-once_example.py
│ ├── login-privileged-every
│ └── login-privileged-every_example.py
│ └── login-privileged-once
│ └── login-privileged-once_example.py
├── pkgroot
└── usr
│ └── local
│ └── outset
│ └── outset.py
├── scripts
└── postinstall
└── support-files
├── Outset.png
└── com.chilcote.outset.plist
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | _A clear and concise description of what the bug is._
12 |
13 | **To Reproduce**
14 | _Steps to reproduce the behavior:_
15 |
16 | **Expected behavior**
17 | _A clear and concise description of what you expected to happen._
18 |
19 | **Screenshots**
20 | _If applicable, add screenshots to help explain your problem._
21 |
22 | **System Details:**
23 | - OS:
24 | - Architecture
25 | - Version [e.g. 4.0]
26 |
27 | **Additional context**
28 | _Add any other context about the problem here._
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | _A clear and concise description of what the problem is._
12 |
13 | **Describe the solution you'd like**
14 | _A clear and concise description of what you want to happen._
15 |
16 | **Describe alternatives you've considered**
17 | _A clear and concise description of any alternative solutions or features you've considered._
18 |
19 | **Additional context**
20 | _Add any other context or screenshots about the feature request here._
21 |
--------------------------------------------------------------------------------
/.github/workflows/build_outset_prerelease_manual.yml:
--------------------------------------------------------------------------------
1 | name: Build Outset pre-release (Manual)
2 |
3 | env:
4 | NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD_MAOS }}
5 |
6 | on: [workflow_dispatch]
7 |
8 | jobs:
9 | build:
10 | runs-on: macos-14
11 |
12 | steps:
13 | - name: Checkout outset repo
14 | uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Install Apple Xcode certificates
19 | uses: apple-actions/import-codesign-certs@v3
20 | with:
21 | keychain-password: ${{ github.run_id }}
22 | p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }}
23 | p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }}
24 |
25 | - name: Install Apple Installer certificates
26 | uses: apple-actions/import-codesign-certs@v3
27 | with:
28 | create-keychain: false # do not create a new keychain for this value
29 | keychain-password: ${{ github.run_id }}
30 | p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }}
31 | p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }}
32 |
33 | - name: Run build package script
34 | run: ./build_outset.zsh "CREATE_PKG" "$NOTARY_APP_PASSWORD"
35 |
36 | - name: get environment variables
37 | id: get_env_var
38 | run: |
39 | echo "OUTSET_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV
40 | echo "OUTSET_MAIN_VERSION=$(/bin/cat ./build_info_main.txt)" >> $GITHUB_ENV
41 |
42 | - name: Generate changelog
43 | id: changelog
44 | uses: metcalfc/changelog-generator@afdcb9470aebdb2252c0c95a1c130723c9e21f3a # v4.1
45 | with:
46 | myToken: ${{ secrets.GITHUB_TOKEN }}
47 | reverse: 'true'
48 |
49 | - name: Create Release
50 | id: create_release
51 | uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
52 | with:
53 | name: Outset ${{env.OUTSET_VERSION}}
54 | tag_name: v${{env.OUTSET_VERSION}}
55 | draft: true
56 | prerelease: true
57 | token: ${{ secrets.GITHUB_TOKEN }}
58 | body: |
59 | # Notes
60 | This is a version of Outset created by GitHub Actions.
61 | Outset.app has been signed and notarized. The package has been signed, notarized and stapled.
62 |
63 | # Changelog
64 | ${{ steps.changelog_reader.outputs.changes }}
65 |
66 | # Changes
67 | ${{ steps.changelog.outputs.changelog }}
68 | files: ${{github.workspace}}/outputs/*.pkg
69 |
70 | - name: Upload packages
71 | uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
72 | with:
73 | name: packages
74 | path: outputs/
75 |
--------------------------------------------------------------------------------
/.github/workflows/build_outset_release_manual.yml:
--------------------------------------------------------------------------------
1 | name: Build Outset Release (Manual)
2 |
3 | env:
4 | NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD_MAOS }}
5 |
6 | on: [workflow_dispatch]
7 |
8 | jobs:
9 | build:
10 | runs-on: macos-12
11 |
12 | steps:
13 | - name: Checkout outset repo
14 | uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Install Apple Xcode certificates
19 | uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
20 | with:
21 | keychain-password: ${{ github.run_id }}
22 | p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }}
23 | p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }}
24 |
25 | - name: Install Apple Installer certificates
26 | uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
27 | with:
28 | create-keychain: false # do not create a new keychain for this value
29 | keychain-password: ${{ github.run_id }}
30 | p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }}
31 | p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }}
32 |
33 | - name: Run build package script
34 | run: ./build_outset.zsh "CREATE_PKG" "$NOTARY_APP_PASSWORD"
35 |
36 | - name: get environment variables
37 | id: get_env_var
38 | run: |
39 | echo "OUTSET_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV
40 | echo "OUTSET_MAIN_VERSION=$(/bin/cat ./build_info_main.txt)" >> $GITHUB_ENV
41 |
42 | - name: Generate changelog
43 | id: changelog
44 | uses: metcalfc/changelog-generator@afdcb9470aebdb2252c0c95a1c130723c9e21f3a # v4.1
45 | with:
46 | myToken: ${{ secrets.GITHUB_TOKEN }}
47 | reverse: 'true'
48 |
49 | - name: Create Release
50 | id: create_release
51 | uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
52 | with:
53 | name: Outset ${{env.OUTSET_VERSION}}
54 | tag_name: v${{env.OUTSET_VERSION}}
55 | draft: true
56 | prerelease: false
57 | token: ${{ secrets.GITHUB_TOKEN }}
58 | body: |
59 | # Notes
60 | This is a version of Outset created by GitHub Actions.
61 | Outset.app has been signed and notarized. The package has been signed, notarized and stapled.
62 |
63 | # Changelog
64 | ${{ steps.changelog_reader.outputs.changes }}
65 |
66 | # Changes
67 | ${{ steps.changelog.outputs.changelog }}
68 | files: ${{github.workspace}}/outputs/*.pkg
69 |
70 | - name: Upload packages
71 | uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
72 | with:
73 | name: packages
74 | path: outputs/
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | sample*
3 | *.pkg
4 | *.dmg
5 | *.pyc
6 | outset.wiki
7 | project.xcworkspace/
8 | Preview Content/
9 | xcuserdata/
10 | xcsharedata/
11 | build/
12 | outputs/
13 | OutsetPkg/
14 | build_info.txt
15 | build_info_main.txt
16 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules: # rule identifiers turned on by default to exclude from running
2 | - line_length
3 | - file_length
4 | - function_body_length
5 | - cyclomatic_complexity
6 | - large_tuple
7 | - force_cast
8 |
9 |
10 | excluded: # paths to ignore during linting. Takes precedence over `included`.
11 | - ./build/SourcePackages/checkouts/swift-argument-parser/*
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [4.0] - 2023-03-23
8 | ### Added
9 | - Initial automated build of Outset
10 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/MDM/Jamf Pro/service-status-ea.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Indicate if all the Outset daemons are enabled or not.
4 |
5 | outsetStatusRaw=$(/usr/local/outset/outset --service-status)
6 | enabledDaemons=$(echo $outsetStatusRaw | grep -c 'Enabled$')
7 |
8 | # If there is no user logged in, none of the 3 agents will be active, only the daemons.
9 |
10 | expectedServices=6
11 |
12 | currentUser=$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }' )
13 |
14 | if [ -z "$currentUser" -o "$currentUser" = "loginwindow" ]; then
15 | expectedServices=3
16 | fi
17 |
18 | healthyStatus="Not Healthy"
19 |
20 | if [ $enabledDaemons -eq $expectedServices ]; then
21 | healthyStatus="Healthy"
22 | fi
23 |
24 | echo "$healthyStatus"
--------------------------------------------------------------------------------
/Outset.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 4124EFB429414A5E003B00F4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4124EFB329414A5E003B00F4 /* Assets.xcassets */; };
11 | 4124EFB729414A5E003B00F4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4124EFB629414A5E003B00F4 /* Preview Assets.xcassets */; };
12 | 413833A429CC6A1B0093319F /* outset-pkg in CopyFiles */ = {isa = PBXBuildFile; fileRef = 413833A229CC69800093319F /* outset-pkg */; };
13 | 41765AC829C8450800D616BF /* Library in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4124EFCD29446FDE003B00F4 /* Library */; };
14 | 41765ACA29C97B6400D616BF /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41765AC929C97B6400D616BF /* Services.swift */; };
15 | 41E28EA229ACDCD6002ADBE5 /* Outset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4124EF90293822F4003B00F4 /* Outset.swift */; };
16 | 41E28EA329ACDCE3002ADBE5 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4124EFA2293B2F9B003B00F4 /* FileUtils.swift */; };
17 | 41E28EA429ACDCE3002ADBE5 /* ItemProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4124EFA4293B304B003B00F4 /* ItemProcessing.swift */; };
18 | 41E28EA529ACDCE3002ADBE5 /* SystemUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4124EF99293824C8003B00F4 /* SystemUtils.swift */; };
19 | 41E28EA729ACDD1F002ADBE5 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 41E28EA629ACDD1F002ADBE5 /* ArgumentParser */; };
20 | CC3DC8222AA70B2D0050EE16 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3DC8212AA70B2D0050EE16 /* Logging.swift */; };
21 | CC3DC8242AA70BD40050EE16 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3DC8232AA70BD40050EE16 /* Preferences.swift */; };
22 | CC3DC8262AA70D2D0050EE16 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3DC8252AA70D2D0050EE16 /* Network.swift */; };
23 | CC3DC8282AA70D930050EE16 /* SystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3DC8272AA70D930050EE16 /* SystemInfo.swift */; };
24 | CC3DC82A2AA70E380050EE16 /* Checksum.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3DC8292AA70E380050EE16 /* Checksum.swift */; };
25 | CC3DC82D2AA70EE60050EE16 /* Data+additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3DC82C2AA70EE60050EE16 /* Data+additions.swift */; };
26 | CC3DC82F2AA70F230050EE16 /* URL+additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3DC82E2AA70F230050EE16 /* URL+additions.swift */; };
27 | CC3DC8312AA70F790050EE16 /* String+additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3DC8302AA70F790050EE16 /* String+additions.swift */; };
28 | CC3DC8332AA7100C0050EE16 /* ShellUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3DC8322AA7100C0050EE16 /* ShellUtils.swift */; };
29 | /* End PBXBuildFile section */
30 |
31 | /* Begin PBXContainerItemProxy section */
32 | 41A67A0D296BAE0F000BFFCE /* PBXContainerItemProxy */ = {
33 | isa = PBXContainerItemProxy;
34 | containerPortal = 4124EF85293822F3003B00F4 /* Project object */;
35 | proxyType = 1;
36 | remoteGlobalIDString = 4124EFAC29414A5D003B00F4;
37 | remoteInfo = Outset;
38 | };
39 | /* End PBXContainerItemProxy section */
40 |
41 | /* Begin PBXCopyFilesBuildPhase section */
42 | 413833A329CC6A0C0093319F /* CopyFiles */ = {
43 | isa = PBXCopyFilesBuildPhase;
44 | buildActionMask = 2147483647;
45 | dstPath = "";
46 | dstSubfolderSpec = 7;
47 | files = (
48 | 413833A429CC6A1B0093319F /* outset-pkg in CopyFiles */,
49 | );
50 | runOnlyForDeploymentPostprocessing = 0;
51 | };
52 | 41765AC729C844F900D616BF /* CopyFiles */ = {
53 | isa = PBXCopyFilesBuildPhase;
54 | buildActionMask = 2147483647;
55 | dstPath = "${PROJECT_NAME}.app/Contents";
56 | dstSubfolderSpec = 16;
57 | files = (
58 | 41765AC829C8450800D616BF /* Library in CopyFiles */,
59 | );
60 | runOnlyForDeploymentPostprocessing = 0;
61 | };
62 | /* End PBXCopyFilesBuildPhase section */
63 |
64 | /* Begin PBXFileReference section */
65 | 4124EF90293822F4003B00F4 /* Outset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Outset.swift; sourceTree = ""; };
66 | 4124EF99293824C8003B00F4 /* SystemUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemUtils.swift; sourceTree = ""; };
67 | 4124EFA2293B2F9B003B00F4 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; };
68 | 4124EFA4293B304B003B00F4 /* ItemProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemProcessing.swift; sourceTree = ""; };
69 | 4124EFAD29414A5D003B00F4 /* Outset.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Outset.app; sourceTree = BUILT_PRODUCTS_DIR; };
70 | 4124EFB329414A5E003B00F4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
71 | 4124EFB629414A5E003B00F4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
72 | 4124EFB829414A5E003B00F4 /* Outset.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Outset.entitlements; sourceTree = ""; };
73 | 4124EFC329414DA4003B00F4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
74 | 4124EFCD29446FDE003B00F4 /* Library */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Library; sourceTree = ""; };
75 | 413833A229CC69800093319F /* outset-pkg */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "outset-pkg"; sourceTree = ""; };
76 | 41765AC929C97B6400D616BF /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = ""; };
77 | 4188FE4A29CE7196003E39BD /* preinstall */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = preinstall; sourceTree = ""; };
78 | 41A679FE2966479E000BFFCE /* generatePackage.zsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = generatePackage.zsh; sourceTree = ""; };
79 | 41A67A04296B9711000BFFCE /* postinstall */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = postinstall; sourceTree = ""; };
80 | 41A67A0C296BADFE000BFFCE /* Outset Install Package.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Outset Install Package.app"; sourceTree = BUILT_PRODUCTS_DIR; };
81 | 41A67A13296BF35F000BFFCE /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
82 | 41ADC47C29AECB8B00C5B94C /* outset */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = outset; sourceTree = ""; };
83 | 41ADC47E29AF649C00C5B94C /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; };
84 | CC3DC8212AA70B2D0050EE16 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; };
85 | CC3DC8232AA70BD40050EE16 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; };
86 | CC3DC8252AA70D2D0050EE16 /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; };
87 | CC3DC8272AA70D930050EE16 /* SystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemInfo.swift; sourceTree = ""; };
88 | CC3DC8292AA70E380050EE16 /* Checksum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checksum.swift; sourceTree = ""; };
89 | CC3DC82C2AA70EE60050EE16 /* Data+additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+additions.swift"; sourceTree = ""; };
90 | CC3DC82E2AA70F230050EE16 /* URL+additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+additions.swift"; sourceTree = ""; };
91 | CC3DC8302AA70F790050EE16 /* String+additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+additions.swift"; sourceTree = ""; };
92 | CC3DC8322AA7100C0050EE16 /* ShellUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellUtils.swift; sourceTree = ""; };
93 | /* End PBXFileReference section */
94 |
95 | /* Begin PBXFrameworksBuildPhase section */
96 | 4124EFAA29414A5D003B00F4 /* Frameworks */ = {
97 | isa = PBXFrameworksBuildPhase;
98 | buildActionMask = 2147483647;
99 | files = (
100 | 41E28EA729ACDD1F002ADBE5 /* ArgumentParser in Frameworks */,
101 | );
102 | runOnlyForDeploymentPostprocessing = 0;
103 | };
104 | /* End PBXFrameworksBuildPhase section */
105 |
106 | /* Begin PBXGroup section */
107 | 4124EF84293822F3003B00F4 = {
108 | isa = PBXGroup;
109 | children = (
110 | 41A67A13296BF35F000BFFCE /* README.md */,
111 | 41ADC47E29AF649C00C5B94C /* LICENSE.md */,
112 | 41A679EA29659F4E000BFFCE /* Package */,
113 | 4124EFAE29414A5D003B00F4 /* Outset */,
114 | 4124EF8E293822F4003B00F4 /* Products */,
115 | 4124EFC029414AD1003B00F4 /* Frameworks */,
116 | );
117 | sourceTree = "";
118 | };
119 | 4124EF8E293822F4003B00F4 /* Products */ = {
120 | isa = PBXGroup;
121 | children = (
122 | 4124EFAD29414A5D003B00F4 /* Outset.app */,
123 | 41A67A0C296BADFE000BFFCE /* Outset Install Package.app */,
124 | );
125 | name = Products;
126 | sourceTree = "";
127 | };
128 | 4124EFA6293B30D6003B00F4 /* Utils */ = {
129 | isa = PBXGroup;
130 | children = (
131 | 4124EFA2293B2F9B003B00F4 /* FileUtils.swift */,
132 | 4124EFA4293B304B003B00F4 /* ItemProcessing.swift */,
133 | 4124EF99293824C8003B00F4 /* SystemUtils.swift */,
134 | 41765AC929C97B6400D616BF /* Services.swift */,
135 | CC3DC8212AA70B2D0050EE16 /* Logging.swift */,
136 | CC3DC8232AA70BD40050EE16 /* Preferences.swift */,
137 | CC3DC8252AA70D2D0050EE16 /* Network.swift */,
138 | CC3DC8272AA70D930050EE16 /* SystemInfo.swift */,
139 | CC3DC8292AA70E380050EE16 /* Checksum.swift */,
140 | CC3DC8322AA7100C0050EE16 /* ShellUtils.swift */,
141 | );
142 | path = Utils;
143 | sourceTree = "";
144 | };
145 | 4124EFAE29414A5D003B00F4 /* Outset */ = {
146 | isa = PBXGroup;
147 | children = (
148 | 4124EF90293822F4003B00F4 /* Outset.swift */,
149 | CC3DC82B2AA70EC90050EE16 /* Extensions */,
150 | 4124EFA6293B30D6003B00F4 /* Utils */,
151 | 4124EFC329414DA4003B00F4 /* Info.plist */,
152 | 4124EFB329414A5E003B00F4 /* Assets.xcassets */,
153 | 4124EFB829414A5E003B00F4 /* Outset.entitlements */,
154 | 4124EFB529414A5E003B00F4 /* Preview Content */,
155 | );
156 | path = Outset;
157 | sourceTree = "";
158 | };
159 | 4124EFB529414A5E003B00F4 /* Preview Content */ = {
160 | isa = PBXGroup;
161 | children = (
162 | 4124EFB629414A5E003B00F4 /* Preview Assets.xcassets */,
163 | );
164 | path = "Preview Content";
165 | sourceTree = "";
166 | };
167 | 4124EFC029414AD1003B00F4 /* Frameworks */ = {
168 | isa = PBXGroup;
169 | children = (
170 | );
171 | name = Frameworks;
172 | sourceTree = "";
173 | };
174 | 41A679EA29659F4E000BFFCE /* Package */ = {
175 | isa = PBXGroup;
176 | children = (
177 | 4124EFCD29446FDE003B00F4 /* Library */,
178 | 41A67A03296B93D1000BFFCE /* Scripts */,
179 | 41A679FE2966479E000BFFCE /* generatePackage.zsh */,
180 | 41ADC47C29AECB8B00C5B94C /* outset */,
181 | 413833A229CC69800093319F /* outset-pkg */,
182 | );
183 | path = Package;
184 | sourceTree = "";
185 | };
186 | 41A67A03296B93D1000BFFCE /* Scripts */ = {
187 | isa = PBXGroup;
188 | children = (
189 | 41A67A04296B9711000BFFCE /* postinstall */,
190 | 4188FE4A29CE7196003E39BD /* preinstall */,
191 | );
192 | path = Scripts;
193 | sourceTree = "";
194 | };
195 | CC3DC82B2AA70EC90050EE16 /* Extensions */ = {
196 | isa = PBXGroup;
197 | children = (
198 | CC3DC82C2AA70EE60050EE16 /* Data+additions.swift */,
199 | CC3DC82E2AA70F230050EE16 /* URL+additions.swift */,
200 | CC3DC8302AA70F790050EE16 /* String+additions.swift */,
201 | );
202 | path = Extensions;
203 | sourceTree = "";
204 | };
205 | /* End PBXGroup section */
206 |
207 | /* Begin PBXNativeTarget section */
208 | 4124EFAC29414A5D003B00F4 /* Outset */ = {
209 | isa = PBXNativeTarget;
210 | buildConfigurationList = 4124EFB929414A5E003B00F4 /* Build configuration list for PBXNativeTarget "Outset" */;
211 | buildPhases = (
212 | 4124EFA929414A5D003B00F4 /* Sources */,
213 | 4124EFAA29414A5D003B00F4 /* Frameworks */,
214 | 4124EFAB29414A5D003B00F4 /* Resources */,
215 | 41765AC729C844F900D616BF /* CopyFiles */,
216 | 413833A329CC6A0C0093319F /* CopyFiles */,
217 | 410A30F329B606070093A1CB /* ShellScript */,
218 | );
219 | buildRules = (
220 | );
221 | dependencies = (
222 | );
223 | name = Outset;
224 | packageProductDependencies = (
225 | 41E28EA629ACDD1F002ADBE5 /* ArgumentParser */,
226 | );
227 | productName = Outset;
228 | productReference = 4124EFAD29414A5D003B00F4 /* Outset.app */;
229 | productType = "com.apple.product-type.application";
230 | };
231 | 41A67A05296BADFE000BFFCE /* Outset Install Package */ = {
232 | isa = PBXNativeTarget;
233 | buildConfigurationList = 41A67A09296BADFE000BFFCE /* Build configuration list for PBXNativeTarget "Outset Install Package" */;
234 | buildPhases = (
235 | 41A67A08296BADFE000BFFCE /* Generate Package */,
236 | );
237 | buildRules = (
238 | );
239 | dependencies = (
240 | 41A67A0E296BAE0F000BFFCE /* PBXTargetDependency */,
241 | );
242 | name = "Outset Install Package";
243 | productName = OutsetInstaller;
244 | productReference = 41A67A0C296BADFE000BFFCE /* Outset Install Package.app */;
245 | productType = "com.apple.product-type.application";
246 | };
247 | /* End PBXNativeTarget section */
248 |
249 | /* Begin PBXProject section */
250 | 4124EF85293822F3003B00F4 /* Project object */ = {
251 | isa = PBXProject;
252 | attributes = {
253 | BuildIndependentTargetsInParallel = 1;
254 | LastSwiftUpdateCheck = 1420;
255 | LastUpgradeCheck = 1420;
256 | TargetAttributes = {
257 | 4124EFAC29414A5D003B00F4 = {
258 | CreatedOnToolsVersion = 14.1;
259 | LastSwiftMigration = 1410;
260 | };
261 | };
262 | };
263 | buildConfigurationList = 4124EF88293822F3003B00F4 /* Build configuration list for PBXProject "Outset" */;
264 | compatibilityVersion = "Xcode 14.0";
265 | developmentRegion = en;
266 | hasScannedForEncodings = 0;
267 | knownRegions = (
268 | en,
269 | Base,
270 | );
271 | mainGroup = 4124EF84293822F3003B00F4;
272 | packageReferences = (
273 | 4124EF9D293828E0003B00F4 /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
274 | );
275 | productRefGroup = 4124EF8E293822F4003B00F4 /* Products */;
276 | projectDirPath = "";
277 | projectRoot = "";
278 | targets = (
279 | 4124EFAC29414A5D003B00F4 /* Outset */,
280 | 41A67A05296BADFE000BFFCE /* Outset Install Package */,
281 | );
282 | };
283 | /* End PBXProject section */
284 |
285 | /* Begin PBXResourcesBuildPhase section */
286 | 4124EFAB29414A5D003B00F4 /* Resources */ = {
287 | isa = PBXResourcesBuildPhase;
288 | buildActionMask = 2147483647;
289 | files = (
290 | 4124EFB729414A5E003B00F4 /* Preview Assets.xcassets in Resources */,
291 | 4124EFB429414A5E003B00F4 /* Assets.xcassets in Resources */,
292 | );
293 | runOnlyForDeploymentPostprocessing = 0;
294 | };
295 | /* End PBXResourcesBuildPhase section */
296 |
297 | /* Begin PBXShellScriptBuildPhase section */
298 | 410A30F329B606070093A1CB /* ShellScript */ = {
299 | isa = PBXShellScriptBuildPhase;
300 | alwaysOutOfDate = 1;
301 | buildActionMask = 2147483647;
302 | files = (
303 | );
304 | inputFileListPaths = (
305 | );
306 | inputPaths = (
307 | );
308 | outputFileListPaths = (
309 | );
310 | outputPaths = (
311 | );
312 | runOnlyForDeploymentPostprocessing = 0;
313 | shellPath = /bin/sh;
314 | shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
315 | };
316 | 41A67A08296BADFE000BFFCE /* Generate Package */ = {
317 | isa = PBXShellScriptBuildPhase;
318 | buildActionMask = 2147483647;
319 | files = (
320 | );
321 | inputFileListPaths = (
322 | );
323 | inputPaths = (
324 | "$(SRCROOT)/Package/generatePackage.zsh",
325 | "$(SRCROOT)/Package/Scripts",
326 | "$(SRCROOT)/Package/outset",
327 | );
328 | name = "Generate Package";
329 | outputFileListPaths = (
330 | );
331 | outputPaths = (
332 | "$(BUILT_PRODUCTS_DIR)/Outset.pkg",
333 | );
334 | runOnlyForDeploymentPostprocessing = 0;
335 | shellPath = /bin/sh;
336 | shellScript = ". \"${SCRIPT_INPUT_FILE_0}\"\n";
337 | };
338 | /* End PBXShellScriptBuildPhase section */
339 |
340 | /* Begin PBXSourcesBuildPhase section */
341 | 4124EFA929414A5D003B00F4 /* Sources */ = {
342 | isa = PBXSourcesBuildPhase;
343 | buildActionMask = 2147483647;
344 | files = (
345 | 41765ACA29C97B6400D616BF /* Services.swift in Sources */,
346 | CC3DC8282AA70D930050EE16 /* SystemInfo.swift in Sources */,
347 | CC3DC8242AA70BD40050EE16 /* Preferences.swift in Sources */,
348 | CC3DC82F2AA70F230050EE16 /* URL+additions.swift in Sources */,
349 | CC3DC8262AA70D2D0050EE16 /* Network.swift in Sources */,
350 | CC3DC8222AA70B2D0050EE16 /* Logging.swift in Sources */,
351 | 41E28EA329ACDCE3002ADBE5 /* FileUtils.swift in Sources */,
352 | CC3DC82D2AA70EE60050EE16 /* Data+additions.swift in Sources */,
353 | CC3DC8332AA7100C0050EE16 /* ShellUtils.swift in Sources */,
354 | CC3DC82A2AA70E380050EE16 /* Checksum.swift in Sources */,
355 | CC3DC8312AA70F790050EE16 /* String+additions.swift in Sources */,
356 | 41E28EA429ACDCE3002ADBE5 /* ItemProcessing.swift in Sources */,
357 | 41E28EA529ACDCE3002ADBE5 /* SystemUtils.swift in Sources */,
358 | 41E28EA229ACDCD6002ADBE5 /* Outset.swift in Sources */,
359 | );
360 | runOnlyForDeploymentPostprocessing = 0;
361 | };
362 | /* End PBXSourcesBuildPhase section */
363 |
364 | /* Begin PBXTargetDependency section */
365 | 41A67A0E296BAE0F000BFFCE /* PBXTargetDependency */ = {
366 | isa = PBXTargetDependency;
367 | target = 4124EFAC29414A5D003B00F4 /* Outset */;
368 | targetProxy = 41A67A0D296BAE0F000BFFCE /* PBXContainerItemProxy */;
369 | };
370 | /* End PBXTargetDependency section */
371 |
372 | /* Begin XCBuildConfiguration section */
373 | 4124EF92293822F4003B00F4 /* Debug */ = {
374 | isa = XCBuildConfiguration;
375 | buildSettings = {
376 | ALWAYS_SEARCH_USER_PATHS = NO;
377 | CLANG_ANALYZER_NONNULL = YES;
378 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
379 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
380 | CLANG_ENABLE_MODULES = YES;
381 | CLANG_ENABLE_OBJC_ARC = YES;
382 | CLANG_ENABLE_OBJC_WEAK = YES;
383 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
384 | CLANG_WARN_BOOL_CONVERSION = YES;
385 | CLANG_WARN_COMMA = YES;
386 | CLANG_WARN_CONSTANT_CONVERSION = YES;
387 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
388 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
389 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
390 | CLANG_WARN_EMPTY_BODY = YES;
391 | CLANG_WARN_ENUM_CONVERSION = YES;
392 | CLANG_WARN_INFINITE_RECURSION = YES;
393 | CLANG_WARN_INT_CONVERSION = YES;
394 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
395 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
396 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
397 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
398 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
399 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
400 | CLANG_WARN_STRICT_PROTOTYPES = YES;
401 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
402 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
403 | CLANG_WARN_UNREACHABLE_CODE = YES;
404 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
405 | COPY_PHASE_STRIP = NO;
406 | DEAD_CODE_STRIPPING = YES;
407 | DEBUG_INFORMATION_FORMAT = dwarf;
408 | ENABLE_STRICT_OBJC_MSGSEND = YES;
409 | ENABLE_TESTABILITY = YES;
410 | GCC_C_LANGUAGE_STANDARD = gnu11;
411 | GCC_DYNAMIC_NO_PIC = NO;
412 | GCC_NO_COMMON_BLOCKS = YES;
413 | GCC_OPTIMIZATION_LEVEL = 0;
414 | GCC_PREPROCESSOR_DEFINITIONS = (
415 | "DEBUG=1",
416 | "$(inherited)",
417 | );
418 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
419 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
420 | GCC_WARN_UNDECLARED_SELECTOR = YES;
421 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
422 | GCC_WARN_UNUSED_FUNCTION = YES;
423 | GCC_WARN_UNUSED_VARIABLE = YES;
424 | MACOSX_DEPLOYMENT_TARGET = 11.0;
425 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
426 | MTL_FAST_MATH = YES;
427 | ONLY_ACTIVE_ARCH = YES;
428 | SDKROOT = macosx;
429 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
430 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
431 | };
432 | name = Debug;
433 | };
434 | 4124EF93293822F4003B00F4 /* Release */ = {
435 | isa = XCBuildConfiguration;
436 | buildSettings = {
437 | ALWAYS_SEARCH_USER_PATHS = NO;
438 | CLANG_ANALYZER_NONNULL = YES;
439 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
440 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
441 | CLANG_ENABLE_MODULES = YES;
442 | CLANG_ENABLE_OBJC_ARC = YES;
443 | CLANG_ENABLE_OBJC_WEAK = YES;
444 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
445 | CLANG_WARN_BOOL_CONVERSION = YES;
446 | CLANG_WARN_COMMA = YES;
447 | CLANG_WARN_CONSTANT_CONVERSION = YES;
448 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
449 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
450 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
451 | CLANG_WARN_EMPTY_BODY = YES;
452 | CLANG_WARN_ENUM_CONVERSION = YES;
453 | CLANG_WARN_INFINITE_RECURSION = YES;
454 | CLANG_WARN_INT_CONVERSION = YES;
455 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
456 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
457 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
458 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
459 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
460 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
461 | CLANG_WARN_STRICT_PROTOTYPES = YES;
462 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
463 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
464 | CLANG_WARN_UNREACHABLE_CODE = YES;
465 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
466 | COPY_PHASE_STRIP = NO;
467 | DEAD_CODE_STRIPPING = YES;
468 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
469 | ENABLE_NS_ASSERTIONS = NO;
470 | ENABLE_STRICT_OBJC_MSGSEND = YES;
471 | GCC_C_LANGUAGE_STANDARD = gnu11;
472 | GCC_NO_COMMON_BLOCKS = YES;
473 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
474 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
475 | GCC_WARN_UNDECLARED_SELECTOR = YES;
476 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
477 | GCC_WARN_UNUSED_FUNCTION = YES;
478 | GCC_WARN_UNUSED_VARIABLE = YES;
479 | MACOSX_DEPLOYMENT_TARGET = 11.0;
480 | MTL_ENABLE_DEBUG_INFO = NO;
481 | MTL_FAST_MATH = YES;
482 | SDKROOT = macosx;
483 | SWIFT_COMPILATION_MODE = wholemodule;
484 | SWIFT_OPTIMIZATION_LEVEL = "-O";
485 | };
486 | name = Release;
487 | };
488 | 4124EFBA29414A5E003B00F4 /* Debug */ = {
489 | isa = XCBuildConfiguration;
490 | buildSettings = {
491 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
492 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
493 | CLANG_ENABLE_MODULES = YES;
494 | CODE_SIGN_ENTITLEMENTS = Outset/Outset.entitlements;
495 | CODE_SIGN_IDENTITY = "-";
496 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
497 | CODE_SIGN_STYLE = Manual;
498 | COMBINE_HIDPI_IMAGES = YES;
499 | CURRENT_PROJECT_VERSION = 4;
500 | DEAD_CODE_STRIPPING = YES;
501 | DEVELOPMENT_ASSET_PATHS = "\"Outset/Preview Content\"";
502 | DEVELOPMENT_TEAM = "";
503 | ENABLE_HARDENED_RUNTIME = YES;
504 | ENABLE_PREVIEWS = YES;
505 | GENERATE_INFOPLIST_FILE = YES;
506 | INFOPLIST_FILE = Outset/Info.plist;
507 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
508 | INFOPLIST_KEY_LSUIElement = YES;
509 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
510 | LD_RUNPATH_SEARCH_PATHS = (
511 | "$(inherited)",
512 | "@executable_path/../Frameworks",
513 | );
514 | MACOSX_DEPLOYMENT_TARGET = 10.15;
515 | MARKETING_VERSION = 4.1.2;
516 | PRODUCT_BUNDLE_IDENTIFIER = io.macadmins.Outset;
517 | PRODUCT_NAME = "$(TARGET_NAME)";
518 | PROVISIONING_PROFILE_SPECIFIER = "";
519 | SWIFT_EMIT_LOC_STRINGS = YES;
520 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
521 | SWIFT_VERSION = 5.0;
522 | };
523 | name = Debug;
524 | };
525 | 4124EFBB29414A5E003B00F4 /* Release */ = {
526 | isa = XCBuildConfiguration;
527 | buildSettings = {
528 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
529 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
530 | CLANG_ENABLE_MODULES = YES;
531 | CODE_SIGN_ENTITLEMENTS = Outset/Outset.entitlements;
532 | CODE_SIGN_IDENTITY = "-";
533 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
534 | CODE_SIGN_STYLE = Manual;
535 | COMBINE_HIDPI_IMAGES = YES;
536 | CURRENT_PROJECT_VERSION = 4;
537 | DEAD_CODE_STRIPPING = YES;
538 | DEVELOPMENT_ASSET_PATHS = "\"Outset/Preview Content\"";
539 | DEVELOPMENT_TEAM = T4SK8ZXCXG;
540 | ENABLE_HARDENED_RUNTIME = YES;
541 | ENABLE_PREVIEWS = YES;
542 | GENERATE_INFOPLIST_FILE = YES;
543 | INFOPLIST_FILE = Outset/Info.plist;
544 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
545 | INFOPLIST_KEY_LSUIElement = YES;
546 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
547 | LD_RUNPATH_SEARCH_PATHS = (
548 | "$(inherited)",
549 | "@executable_path/../Frameworks",
550 | );
551 | MACOSX_DEPLOYMENT_TARGET = 10.15;
552 | MARKETING_VERSION = 4.1.2;
553 | PRODUCT_BUNDLE_IDENTIFIER = io.macadmins.Outset;
554 | PRODUCT_NAME = "$(TARGET_NAME)";
555 | PROVISIONING_PROFILE_SPECIFIER = "";
556 | SWIFT_EMIT_LOC_STRINGS = YES;
557 | SWIFT_VERSION = 5.0;
558 | };
559 | name = Release;
560 | };
561 | 41A67A0A296BADFE000BFFCE /* Debug */ = {
562 | isa = XCBuildConfiguration;
563 | buildSettings = {
564 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
565 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
566 | CODE_SIGN_ENTITLEMENTS = OutsetInstaller/OutsetInstaller.entitlements;
567 | CODE_SIGN_IDENTITY = "Apple Development";
568 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
569 | CODE_SIGN_STYLE = Automatic;
570 | CURRENT_PROJECT_VERSION = 4;
571 | DEAD_CODE_STRIPPING = YES;
572 | DEVELOPMENT_ASSET_PATHS = "";
573 | DEVELOPMENT_TEAM = T4SK8ZXCXG;
574 | ENABLE_HARDENED_RUNTIME = YES;
575 | ENABLE_PREVIEWS = YES;
576 | GENERATE_INFOPLIST_FILE = YES;
577 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
578 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
579 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
580 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
581 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
582 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
583 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
584 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
585 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
586 | IPHONEOS_DEPLOYMENT_TARGET = 16.2;
587 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
588 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
589 | MACOSX_DEPLOYMENT_TARGET = 10.15;
590 | MARKETING_VERSION = 4.1.2;
591 | PRODUCT_BUNDLE_IDENTIFIER = io.macadmins.Outset;
592 | PRODUCT_NAME = "$(TARGET_NAME)";
593 | PROVISIONING_PROFILE_SPECIFIER = "";
594 | SDKROOT = auto;
595 | SUPPORTED_PLATFORMS = macosx;
596 | SUPPORTS_MACCATALYST = NO;
597 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
598 | SWIFT_EMIT_LOC_STRINGS = YES;
599 | SWIFT_VERSION = 5.0;
600 | };
601 | name = Debug;
602 | };
603 | 41A67A0B296BADFE000BFFCE /* Release */ = {
604 | isa = XCBuildConfiguration;
605 | buildSettings = {
606 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
607 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
608 | CODE_SIGN_ENTITLEMENTS = OutsetInstaller/OutsetInstaller.entitlements;
609 | CODE_SIGN_IDENTITY = "Apple Development";
610 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
611 | CODE_SIGN_STYLE = Automatic;
612 | CURRENT_PROJECT_VERSION = 4;
613 | DEAD_CODE_STRIPPING = YES;
614 | DEVELOPMENT_ASSET_PATHS = "";
615 | DEVELOPMENT_TEAM = T4SK8ZXCXG;
616 | ENABLE_HARDENED_RUNTIME = YES;
617 | ENABLE_PREVIEWS = YES;
618 | GENERATE_INFOPLIST_FILE = YES;
619 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
620 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
621 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
622 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
623 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
624 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
625 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
626 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
627 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
628 | IPHONEOS_DEPLOYMENT_TARGET = 16.2;
629 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
630 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
631 | MACOSX_DEPLOYMENT_TARGET = 10.15;
632 | MARKETING_VERSION = 4.1.2;
633 | PRODUCT_BUNDLE_IDENTIFIER = io.macadmins.Outset;
634 | PRODUCT_NAME = "$(TARGET_NAME)";
635 | PROVISIONING_PROFILE_SPECIFIER = "";
636 | SDKROOT = auto;
637 | SUPPORTED_PLATFORMS = macosx;
638 | SUPPORTS_MACCATALYST = NO;
639 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
640 | SWIFT_EMIT_LOC_STRINGS = YES;
641 | SWIFT_VERSION = 5.0;
642 | };
643 | name = Release;
644 | };
645 | /* End XCBuildConfiguration section */
646 |
647 | /* Begin XCConfigurationList section */
648 | 4124EF88293822F3003B00F4 /* Build configuration list for PBXProject "Outset" */ = {
649 | isa = XCConfigurationList;
650 | buildConfigurations = (
651 | 4124EF92293822F4003B00F4 /* Debug */,
652 | 4124EF93293822F4003B00F4 /* Release */,
653 | );
654 | defaultConfigurationIsVisible = 0;
655 | defaultConfigurationName = Release;
656 | };
657 | 4124EFB929414A5E003B00F4 /* Build configuration list for PBXNativeTarget "Outset" */ = {
658 | isa = XCConfigurationList;
659 | buildConfigurations = (
660 | 4124EFBA29414A5E003B00F4 /* Debug */,
661 | 4124EFBB29414A5E003B00F4 /* Release */,
662 | );
663 | defaultConfigurationIsVisible = 0;
664 | defaultConfigurationName = Release;
665 | };
666 | 41A67A09296BADFE000BFFCE /* Build configuration list for PBXNativeTarget "Outset Install Package" */ = {
667 | isa = XCConfigurationList;
668 | buildConfigurations = (
669 | 41A67A0A296BADFE000BFFCE /* Debug */,
670 | 41A67A0B296BADFE000BFFCE /* Release */,
671 | );
672 | defaultConfigurationIsVisible = 0;
673 | defaultConfigurationName = Release;
674 | };
675 | /* End XCConfigurationList section */
676 |
677 | /* Begin XCRemoteSwiftPackageReference section */
678 | 4124EF9D293828E0003B00F4 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = {
679 | isa = XCRemoteSwiftPackageReference;
680 | repositoryURL = "https://github.com/apple/swift-argument-parser.git";
681 | requirement = {
682 | kind = upToNextMajorVersion;
683 | minimumVersion = 1.0.0;
684 | };
685 | };
686 | /* End XCRemoteSwiftPackageReference section */
687 |
688 | /* Begin XCSwiftPackageProductDependency section */
689 | 41E28EA629ACDD1F002ADBE5 /* ArgumentParser */ = {
690 | isa = XCSwiftPackageProductDependency;
691 | package = 4124EF9D293828E0003B00F4 /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
692 | productName = ArgumentParser;
693 | };
694 | /* End XCSwiftPackageProductDependency section */
695 | };
696 | rootObject = 4124EF85293822F3003B00F4 /* Project object */;
697 | }
698 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-argument-parser",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-argument-parser.git",
7 | "state" : {
8 | "revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d",
9 | "version" : "1.2.0"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/xcshareddata/xcschemes/Outset App Bundle.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
66 |
68 |
74 |
75 |
76 |
77 |
79 |
80 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/xcshareddata/xcschemes/Outset Installer Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/xcuserdata/rea094.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/xcuserdata/rea094.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Outset App Bundle.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 3
11 |
12 | Outset Installer Package.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 2
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | 4124EF8C293822F4003B00F4
21 |
22 | primary
23 |
24 |
25 | 4124EFAC29414A5D003B00F4
26 |
27 | primary
28 |
29 |
30 | 41A679EE2965A03D000BFFCE
31 |
32 | primary
33 |
34 |
35 | 41A67A05296BADFE000BFFCE
36 |
37 | primary
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Outset.png_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "Outset.png_16x16@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "Outset.png_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "Outset.png_32x32@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "Outset.png_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "Outset.png_128x128@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "Outset.png_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "Outset.png_256x256@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "Outset.png_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "Outset.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_128x128.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_128x128@2x.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_16x16.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_16x16@2x.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_256x256.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_256x256@2x.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_32x32.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_32x32@2x.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/ef7b4ad4005c2176d61123fc393baaa747b73f12/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_512x512.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Outset/Extensions/Data+additions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Data+additions.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 | import CommonCrypto
10 |
11 | extension Data {
12 | // extension to the Data class that lets us compute sha256
13 | func sha256() -> Data {
14 | var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
15 | self.withUnsafeBytes {
16 | _ = CC_SHA256($0.baseAddress, CC_LONG(count), &hash)
17 | }
18 | return Data(hash)
19 | }
20 |
21 | func hexEncodedString() -> String {
22 | return map { String(format: "%02hhx", $0) }.joined()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Outset/Extensions/String+additions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+additions.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | func camelCaseToUnderscored() -> String {
12 | let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])", options: [])
13 | let range = NSRange(location: 0, length: utf16.count)
14 | return regex?.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "$1_$2").lowercased() ?? self
15 | }
16 | }
17 |
18 | func getValueForKey(_ key: String, inArray array: [String: String]) -> String? {
19 | // short function that treats a [String: String] as a key value pair.
20 | return array[key]
21 | }
22 |
--------------------------------------------------------------------------------
/Outset/Extensions/URL+additions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+additions.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | extension URL {
11 | var isDirectory: Bool {
12 | (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Outset/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 4.1.2
19 | CFBundleVersion
20 | 4.1.2
21 | LSApplicationCategoryType
22 | public.app-category.utilities
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Outset/Outset.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Outset/Outset.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // outset
4 | //
5 | // Created by Bart Reardon on 1/12/2022.
6 | //
7 | // swift implementation of outset by Joseph Chilcote https://github.com/chilcote/outset
8 | //
9 |
10 | import Foundation
11 | import ArgumentParser
12 | import OSLog
13 |
14 | let author = "Bart Reardon - Adapted from outset by Joseph Chilcote (chilcote@gmail.com) https://github.com/chilcote/outset"
15 | let outsetVersion: AnyObject = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as AnyObject
16 |
17 | // Outset specific directories
18 | let outsetDirectory = "/usr/local/outset/"
19 | let bootEveryDir = outsetDirectory+"boot-every"
20 | let bootOnceDir = outsetDirectory+"boot-once"
21 | let loginWindowDir = outsetDirectory+"login-window"
22 | let loginEveryDir = outsetDirectory+"login-every"
23 | let loginOnceDir = outsetDirectory+"login-once"
24 | let loginEveryPrivilegedDir = outsetDirectory+"login-privileged-every"
25 | let loginOncePrivilegedDir = outsetDirectory+"login-privileged-once"
26 | let onDemandDir = outsetDirectory+"on-demand"
27 | let shareDirectory = outsetDirectory+"share/"
28 |
29 | let onDemandTrigger = "/private/tmp/.io.macadmins.outset.ondemand.launchd"
30 | let loginPrivilegedTrigger = "/private/tmp/.io.macadmins.outset.login-privileged.launchd"
31 | let cleanupTrigger = "/private/tmp/.io.macadmins.outset.cleanup.launchd"
32 |
33 | // File permission defaults
34 | let requiredFilePermissions: NSNumber = 0o644
35 | let requiredExecutablePermissions: NSNumber = 0o755
36 |
37 | // Set some variables
38 | var debugMode: Bool = false
39 | var loginwindowState: Bool = true
40 | var consoleUser: String = getConsoleUserInfo().username
41 | var continueFirstBoot: Bool = true
42 | var prefs = loadOutsetPreferences()
43 |
44 | // Log Stuff
45 | let bundleID = Bundle.main.bundleIdentifier ?? "io.macadmins.Outset"
46 | let osLog = OSLog(subsystem: bundleID, category: "main")
47 | // We could make these availab as preferences perhaps
48 | let logFileName = "outset.log"
49 | let logFileMaxCount: Int = 30
50 | let logDirectory = outsetDirectory+"logs"
51 | let logFilePath = logDirectory+"/"+logFileName
52 |
53 | // Logic insertion point
54 | @main
55 | struct Outset: ParsableCommand {
56 | static let configuration = CommandConfiguration(
57 | commandName: "outset",
58 | abstract: "Outset is a utility that automatically processes scripts and/or packages at boot, on demand, or login.")
59 |
60 | @Flag(help: .hidden)
61 | var debug = false
62 |
63 | @Flag(help: "Used by launchd for scheduled runs at boot")
64 | var boot = false
65 |
66 | @Flag(help: "Used by launchd for scheduled runs at login")
67 | var login = false
68 |
69 | @Flag(help: "Used by launchd for scheduled runs at the login window")
70 | var loginWindow = false
71 |
72 | @Flag(help: "Used by launchd for scheduled privileged runs at login")
73 | var loginPrivileged = false
74 |
75 | @Flag(help: "Process scripts on demand")
76 | var onDemand = false
77 |
78 | @Flag(help: "Manually process scripts in login-every")
79 | var loginEvery = false
80 |
81 | @Flag(help: "Manually process scripts in login-once")
82 | var loginOnce = false
83 |
84 | @Flag(help: "Used by launchd to clean up on-demand dir")
85 | var cleanup = false
86 |
87 | @Option(help: ArgumentHelp("Add one or more users to ignored list", valueName: "username"))
88 | var addIgnoredUser: [String] = []
89 |
90 | @Option(help: ArgumentHelp("Remove one or more users from ignored list", valueName: "username"))
91 | var removeIgnoredUser: [String] = []
92 |
93 | @Option(help: ArgumentHelp("Add one or more scripts to override list", valueName: "script"), completion: .file())
94 | var addOverride: [String] = []
95 |
96 | // maintaining misspelt option as hidden
97 | @Option(help: .hidden, completion: .file())
98 | var addOveride: [String] = []
99 |
100 | @Option(help: ArgumentHelp("Remove one or more scripts from override list", valueName: "script"), completion: .file())
101 | var removeOverride: [String] = []
102 |
103 | // maintaining misspelt option as hidden
104 | @Option(help: .hidden, completion: .file())
105 | var removeOveride: [String] = []
106 |
107 | // removed from view in favour for checksum. retained to support backward compatability
108 | @Option(help: .hidden, completion: .file())
109 | var computeSHA: [String] = []
110 |
111 | @Option(help: ArgumentHelp("Compute the checksum (SHA256) hash of the given file. Use the keyword 'all' to compute all values and generate a formatted configuration plist", valueName: "file"), completion: .file())
112 | var checksum: [String] = []
113 |
114 | @Flag(help: .hidden)
115 | var shasumReport = false
116 |
117 | @Flag(help: .hidden)
118 | var checksumReport = false
119 |
120 | @Flag(help: .hidden)
121 | var enableServices = false
122 |
123 | @Flag(help: .hidden)
124 | var disableServices = false
125 |
126 | @Flag(help: .hidden)
127 | var serviceStatus = false
128 |
129 | @Flag(help: "Show version number")
130 | var version = false
131 |
132 | mutating func run() throws {
133 |
134 | if debug || UserDefaults.standard.bool(forKey: "verbose_logging") {
135 | debugMode = true
136 | }
137 |
138 | if version {
139 | printStdOut("\(outsetVersion)")
140 | if debugMode {
141 | writeSysReport()
142 | }
143 | }
144 |
145 | if enableServices, #available(macOS 13.0, *) {
146 | let manager = ServiceManager()
147 | manager.registerDaemons()
148 | }
149 |
150 | if disableServices, #available(macOS 13.0, *) {
151 | let manager = ServiceManager()
152 | manager.removeDaemons()
153 | }
154 |
155 | if serviceStatus, #available(macOS 13.0, *) {
156 | let manager = ServiceManager()
157 | manager.getStatus()
158 | }
159 |
160 | if boot {
161 | // perform log file rotation
162 | performLogRotation(logFolderPath: logDirectory, logFileBaseName: logFileName, maxLogFiles: logFileMaxCount)
163 |
164 | writeLog("Processing scheduled runs for boot", logLevel: .info)
165 | ensureWorkingFolders()
166 |
167 | writeOutsetPreferences(prefs: prefs)
168 |
169 | if !folderContents(path: bootOnceDir).isEmpty {
170 | if prefs.waitForNetwork {
171 | loginwindowState = false
172 | loginWindowUpdateState(.disable)
173 | continueFirstBoot = waitForNetworkUp(timeout: floor(Double(prefs.networkTimeout) / 10))
174 | }
175 | if continueFirstBoot {
176 | processItems(bootOnceDir, deleteItems: true)
177 | } else {
178 | writeLog("Unable to connect to network. Skipping boot-once scripts...", logLevel: .error)
179 | }
180 | if !loginwindowState {
181 | loginWindowUpdateState(.enable)
182 | }
183 | }
184 |
185 | if !folderContents(path: bootEveryDir).isEmpty {
186 | processItems(bootEveryDir)
187 | }
188 |
189 | writeLog("Boot processing complete")
190 | }
191 |
192 | if loginWindow {
193 | writeLog("Processing scheduled runs for login window", logLevel: .info)
194 |
195 | if !folderContents(path: loginWindowDir).isEmpty {
196 | processItems(loginWindowDir)
197 | }
198 | }
199 |
200 | if login {
201 | writeLog("Processing scheduled runs for login", logLevel: .info)
202 | if !prefs.ignoredUsers.contains(consoleUser) {
203 | if !folderContents(path: loginOnceDir).isEmpty {
204 | processItems(loginOnceDir, once: true, override: prefs.overrideLoginOnce)
205 | }
206 | if !folderContents(path: loginEveryDir).isEmpty {
207 | processItems(loginEveryDir)
208 | }
209 | if !folderContents(path: loginOncePrivilegedDir).isEmpty || !folderContents(path: loginEveryPrivilegedDir).isEmpty {
210 | createTrigger(loginPrivilegedTrigger)
211 | }
212 | }
213 |
214 | }
215 |
216 | if loginPrivileged {
217 | writeLog("Processing scheduled runs for privileged login", logLevel: .info)
218 | if checkFileExists(path: loginPrivilegedTrigger) {
219 | pathCleanup(pathname: loginPrivilegedTrigger)
220 | }
221 | if !prefs.ignoredUsers.contains(consoleUser) {
222 | if !folderContents(path: loginOncePrivilegedDir).isEmpty {
223 | processItems(loginOncePrivilegedDir, once: true, override: prefs.overrideLoginOnce)
224 | }
225 | if !folderContents(path: loginEveryPrivilegedDir).isEmpty {
226 | processItems(loginEveryPrivilegedDir)
227 | }
228 | } else {
229 | writeLog("Skipping login scripts for user \(consoleUser)")
230 | }
231 | }
232 |
233 | if onDemand {
234 | writeLog("Processing on-demand", logLevel: .info)
235 | if !folderContents(path: onDemandDir).isEmpty {
236 | if !["root", "loginwindow"].contains(consoleUser) {
237 | let currentUser = NSUserName()
238 | if consoleUser == currentUser {
239 | processItems(onDemandDir)
240 | createTrigger(cleanupTrigger)
241 | } else {
242 | writeLog("User \(currentUser) is not the current console user. Skipping on-demand run.")
243 | }
244 | } else {
245 | writeLog("No current user session. Skipping on-demand run.")
246 | }
247 | }
248 | }
249 |
250 | if loginEvery {
251 | writeLog("Processing scripts in login-every", logLevel: .info)
252 | if !prefs.ignoredUsers.contains(consoleUser) {
253 | if !folderContents(path: loginEveryDir).isEmpty {
254 | processItems(loginEveryDir)
255 | }
256 | }
257 | }
258 |
259 | if loginOnce {
260 | writeLog("Processing scripts in login-once", logLevel: .info)
261 | if !prefs.ignoredUsers.contains(consoleUser) {
262 | if !folderContents(path: loginOnceDir).isEmpty {
263 | processItems(loginOnceDir, once: true, override: prefs.overrideLoginOnce)
264 | }
265 | } else {
266 | writeLog("user \(consoleUser) is in the ignored list. skipping", logLevel: .debug)
267 | }
268 | }
269 |
270 | if cleanup {
271 | writeLog("Cleaning up on-demand directory.", logLevel: .info)
272 | if checkFileExists(path: onDemandTrigger) { pathCleanup(pathname: onDemandTrigger) }
273 | if checkFileExists(path: cleanupTrigger) { pathCleanup(pathname: cleanupTrigger) }
274 | if !folderContents(path: onDemandDir).isEmpty { pathCleanup(pathname: onDemandDir) }
275 | }
276 |
277 | if !addIgnoredUser.isEmpty {
278 | ensureRoot("add to ignored users")
279 | for username in addIgnoredUser {
280 | if prefs.ignoredUsers.contains(username) {
281 | writeLog("User \"\(username)\" is already in the ignored users list", logLevel: .info)
282 | } else {
283 | writeLog("Adding \(username) to ignored users list", logLevel: .info)
284 | prefs.ignoredUsers.append(username)
285 | }
286 | }
287 | writeOutsetPreferences(prefs: prefs)
288 | }
289 |
290 | if !removeIgnoredUser.isEmpty {
291 | ensureRoot("remove ignored users")
292 | for username in removeIgnoredUser {
293 | if let index = prefs.ignoredUsers.firstIndex(of: username) {
294 | prefs.ignoredUsers.remove(at: index)
295 | }
296 | }
297 | writeOutsetPreferences(prefs: prefs)
298 | }
299 |
300 | if !addOverride.isEmpty || !addOveride.isEmpty {
301 | if !addOveride.isEmpty {
302 | addOverride = addOveride
303 | }
304 | ensureRoot("add scripts to override list")
305 |
306 | for var override in addOverride {
307 | if !override.contains(loginOnceDir) && !override.contains(loginOncePrivilegedDir) {
308 | override = "\(loginOnceDir)/\(override)"
309 | }
310 | writeLog("Adding \(override) to override list", logLevel: .debug)
311 | prefs.overrideLoginOnce[override] = Date()
312 | }
313 | writeOutsetPreferences(prefs: prefs)
314 | }
315 |
316 | if !removeOverride.isEmpty || !removeOveride.isEmpty {
317 | if !removeOveride.isEmpty {
318 | removeOverride = removeOveride
319 | }
320 | ensureRoot("remove scripts to override list")
321 | for var override in removeOverride {
322 | if !override.contains(loginOnceDir) {
323 | override = "\(loginOnceDir)/\(override)"
324 | }
325 | writeLog("Removing \(override) from override list", logLevel: .debug)
326 | prefs.overrideLoginOnce.removeValue(forKey: override)
327 | }
328 | writeOutsetPreferences(prefs: prefs)
329 | }
330 |
331 | if !checksum.isEmpty || !computeSHA.isEmpty {
332 | if checksum.isEmpty {
333 | checksum = computeSHA
334 | }
335 | if checksum[0].lowercased() == "all" {
336 | checksumAllFiles()
337 | } else {
338 | for fileToHash in checksum {
339 | let url = URL(fileURLWithPath: fileToHash)
340 | if let hash = sha256(for: url) {
341 | printStdOut("Checksum for file \(fileToHash): \(hash)")
342 | }
343 | }
344 | }
345 | }
346 |
347 | if shasumReport || checksumReport {
348 | writeLog("Checksum report", logLevel: .info)
349 | for (filename, checksum) in checksumLoadApprovedFiles() {
350 | writeLog("\(filename) : \(checksum)", logLevel: .info)
351 | }
352 | }
353 | }
354 | }
355 |
--------------------------------------------------------------------------------
/Outset/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Outset/Utils/Checksum.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Checksum.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct FileHashes: Codable {
11 | var sha256sum: [String: String] = [String: String]()
12 | }
13 |
14 | func checksumLoadApprovedFiles() -> [String: String] {
15 | // imports the list of file hashes that are approved to run
16 | var outsetFileHashList = FileHashes()
17 |
18 | let defaults = UserDefaults.standard
19 | let hashes = defaults.object(forKey: "sha256sum")
20 |
21 | if let data = hashes as? [String: String] {
22 | for (key, value) in data {
23 | outsetFileHashList.sha256sum[key] = value
24 | }
25 | }
26 |
27 | return outsetFileHashList.sha256sum
28 | }
29 |
30 | func verifySHASUMForFile(filename: String, shasumArray: [String: String]) -> Bool {
31 | // Verify that the file
32 | var proceed = false
33 | let errorMessage = "no required hash or file hash mismatch for: \(filename). Skipping"
34 | writeLog("checking hash for \(filename)", logLevel: .debug)
35 | let url = URL(fileURLWithPath: filename)
36 | if let fileHash = sha256(for: url) {
37 | writeLog("file hash : \(fileHash)", logLevel: .debug)
38 | if let storedHash = getValueForKey(filename, inArray: shasumArray) {
39 | writeLog("required hash : \(storedHash)", logLevel: .debug)
40 | if storedHash == fileHash {
41 | proceed = true
42 | }
43 | }
44 | }
45 | if !proceed {
46 | writeLog(errorMessage, logLevel: .error)
47 | }
48 |
49 | return proceed
50 | }
51 |
52 | func sha256(for url: URL) -> String? {
53 | // computes a sha256sum for the specified file path and returns a string
54 | do {
55 | let fileData = try Data(contentsOf: url)
56 | let sha256 = fileData.sha256()
57 | return sha256.hexEncodedString()
58 | } catch {
59 | return nil
60 | }
61 | }
62 |
63 | func checksumAllFiles() {
64 | // compute checksum (SHA256) for all files in the outset directory
65 | // returns data in two formats to stdout:
66 | // plaintext
67 | // as plist format ready for import into an MDM or converting to a .mobileconfig
68 |
69 | let url = URL(fileURLWithPath: outsetDirectory)
70 | writeLog("CHECKSUM", logLevel: .info)
71 | var shasumPlist = FileHashes()
72 | if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
73 | for case let fileURL as URL in enumerator {
74 | do {
75 | let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
76 | if fileAttributes.isRegularFile! && fileURL.pathExtension != "plist" && fileURL.lastPathComponent != "outset" {
77 | if let shasum = sha256(for: fileURL) {
78 | printStdOut("\(fileURL.relativePath) : \(shasum)")
79 | shasumPlist.sha256sum[fileURL.relativePath] = shasum
80 | }
81 | }
82 | } catch {
83 | printStdErr(error.localizedDescription)
84 | printStdErr(fileURL.absoluteString)
85 | }
86 | }
87 |
88 | writeLog("PLIST", logLevel: .info)
89 | let encoder = PropertyListEncoder()
90 | encoder.outputFormat = .xml
91 | do {
92 | let data = try encoder.encode(shasumPlist)
93 | if let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] {
94 | let formatted = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
95 | if let string = String(data: formatted, encoding: .utf8) {
96 | printStdOut(string)
97 | }
98 | }
99 | } catch {
100 | writeLog("plist encoding failed", logLevel: .error)
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Outset/Utils/FileUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utils.swift
3 | // outset
4 | //
5 | // Created by Bart Reardon on 3/12/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | func installPackage(pkg: String) -> Bool {
11 | // Installs pkg onto boot drive
12 | if isRoot() {
13 | var pkgToInstall: String = ""
14 | var dmgMount: String = ""
15 |
16 | if pkg.lowercased().hasSuffix("dmg") {
17 | dmgMount = mountDmg(dmg: pkg)
18 | for files in folderContents(path: dmgMount) where ["pkg", "mpkg"].contains(files.lowercased().suffix(3)) {
19 | pkgToInstall = dmgMount
20 | }
21 | } else if ["pkg", "mpkg"].contains(pkg.lowercased().suffix(3)) {
22 | pkgToInstall = pkg
23 | }
24 | writeLog("Installing \(pkgToInstall)")
25 | let cmd = "/usr/sbin/installer -pkg \(pkgToInstall) -target /"
26 | let (output, error, status) = runShellCommand(cmd, verbose: true)
27 | if status != 0 {
28 | writeLog(error, logLevel: .error)
29 | } else {
30 | writeLog(output)
31 | }
32 |
33 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
34 | if !dmgMount.isEmpty {
35 | writeLog(detachDmg(dmgMount: dmgMount))
36 | }
37 | }
38 | return true
39 | } else {
40 | writeLog("Unable to process \(pkg)", logLevel: .error)
41 | writeLog("Must be root to install packages", logLevel: .error)
42 | }
43 | return false
44 | }
45 |
46 | func ensureWorkingFolders() {
47 | // Ensures working folders are all present and creates them if necessary
48 | let workingDirectories = [
49 | bootEveryDir,
50 | bootOnceDir,
51 | loginWindowDir,
52 | loginEveryDir,
53 | loginOnceDir,
54 | loginEveryPrivilegedDir,
55 | loginOncePrivilegedDir,
56 | onDemandDir,
57 | logDirectory
58 | ]
59 |
60 | for directory in workingDirectories where !checkDirectoryExists(path: directory) {
61 | writeLog("\(directory) does not exist, creating now.", logLevel: .debug)
62 | do {
63 | try FileManager.default.createDirectory(atPath: directory, withIntermediateDirectories: true)
64 | } catch {
65 | writeLog("could not create path at \(directory)", logLevel: .error)
66 | }
67 | }
68 | }
69 |
70 | func checkFileExists(path: String) -> Bool {
71 | return FileManager.default.fileExists(atPath: path)
72 | }
73 |
74 | func checkDirectoryExists(path: String) -> Bool {
75 | var isDirectory: ObjCBool = false
76 | _ = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
77 | return isDirectory.boolValue
78 | }
79 |
80 | func folderContents(path: String) -> [String] {
81 | // Returns a array of strings containing the folder contents
82 | // Does not perform a recursive list
83 | var filelist: [String] = []
84 | do {
85 | let files = try FileManager.default.contentsOfDirectory(atPath: path)
86 | let sortedFiles = files.sorted()
87 | for file in sortedFiles {
88 | filelist.append("\(path)/\(file)")
89 | }
90 | } catch {
91 | return []
92 | }
93 | return filelist
94 | }
95 |
96 | func verifyPermissions(pathname: String) -> Bool {
97 | // Files should be owned by root
98 | // Files that are not scripts should have permissions 644 (-rw-r--r--)
99 | // Files that are scripts should have permissions 755 (-rwxr-xr-x)
100 | // If the permission for the request file is not correct then return fals to indicate it should not be processed
101 |
102 | let (ownerID, mode) = getFileProperties(pathname: pathname)
103 | let posixPermissions = String(mode.intValue, radix: 8, uppercase: false)
104 | let errorMessage = "Permissions for \(pathname) are incorrect. Should be owned by root and with mode"
105 |
106 | writeLog("ownerID for \(pathname) : \(String(describing: ownerID))", logLevel: .debug)
107 | writeLog("posixPermissions for \(pathname) : \(String(describing: posixPermissions))", logLevel: .debug)
108 |
109 | if ["pkg", "mpkg", "dmg", "mobileconfig"].contains(pathname.lowercased().split(separator: ".").last) {
110 | if ownerID == 0 && mode == requiredFilePermissions {
111 | return true
112 | } else {
113 | writeLog("\(errorMessage) x644", logLevel: .error)
114 | }
115 | } else {
116 | if ownerID == 0 && mode == requiredExecutablePermissions {
117 | return true
118 | } else {
119 | writeLog("\(errorMessage) x755", logLevel: .error)
120 | }
121 | }
122 | return false
123 | }
124 |
125 | func getFileProperties(pathname: String) -> (ownerID: Int, permissions: NSNumber) {
126 | // returns the ID and permissions of the specified file
127 | var fileAttributes: [FileAttributeKey: Any]
128 | var ownerID: Int = 0
129 | var mode: NSNumber = 0
130 | do {
131 | fileAttributes = try FileManager.default.attributesOfItem(atPath: pathname)
132 | if let ownerProperty = fileAttributes[.ownerAccountID] as? Int {
133 | ownerID = ownerProperty
134 | }
135 | if let modeProperty = fileAttributes[.posixPermissions] as? NSNumber {
136 | mode = modeProperty
137 | }
138 | } catch {
139 | writeLog("Could not read file at path \(pathname)", logLevel: .error)
140 | }
141 | return (ownerID, mode)
142 | }
143 |
144 | func pathCleanup(pathname: String) {
145 | // check if folder and clean all files in that folder
146 | // Deletes given script or cleans folder
147 | writeLog("Cleaning up \(pathname)", logLevel: .debug)
148 | if checkDirectoryExists(path: pathname) {
149 | for fileItem in folderContents(path: pathname) {
150 | writeLog("Cleaning up \(fileItem)", logLevel: .debug)
151 | deletePath(fileItem)
152 | }
153 | } else if checkFileExists(path: pathname) {
154 | writeLog("\(pathname) exists", logLevel: .debug)
155 | deletePath(pathname)
156 | } else {
157 | writeLog("\(pathname) doesn't seem to exist", logLevel: .error)
158 | }
159 | }
160 |
161 | func deletePath(_ path: String) {
162 | // Deletes the specified file
163 | writeLog("Deleting \(path)", logLevel: .debug)
164 | do {
165 | try FileManager.default.removeItem(atPath: path)
166 | writeLog("\(path) deleted", logLevel: .debug)
167 | } catch {
168 | writeLog("\(path) could not be removed", logLevel: .error)
169 | }
170 | }
171 |
172 | func createTrigger(_ path: String) {
173 | FileManager.default.createFile(atPath: path, contents: nil)
174 | }
175 |
176 | func mountDmg(dmg: String) -> String {
177 | // Attaches dmg and returns the path
178 | let cmd = "/usr/bin/hdiutil attach -nobrowse -noverify -noautoopen \(dmg)"
179 | writeLog("Attaching \(dmg)", logLevel: .debug)
180 | let (output, error, status) = runShellCommand(cmd)
181 | if status != 0 {
182 | writeLog("Failed attaching \(dmg) with error \(error)", logLevel: .error)
183 | return error
184 | }
185 | return output.trimmingCharacters(in: .whitespacesAndNewlines)
186 | }
187 |
188 | func detachDmg(dmgMount: String) -> String {
189 | // Detaches dmg
190 | writeLog("Detaching \(dmgMount)", logLevel: .debug)
191 | let cmd = "/usr/bin/hdiutil detach -force \(dmgMount)"
192 | let (output, error, status) = runShellCommand(cmd)
193 | if status != 0 {
194 | writeLog("Failed detaching \(dmgMount) with error \(error)", logLevel: .error)
195 | return error
196 | }
197 | return output.trimmingCharacters(in: .whitespacesAndNewlines)
198 | }
199 |
200 | func performLogRotation(logFolderPath: String, logFileBaseName: String, maxLogFiles: Int = 30) {
201 | let fileManager = FileManager.default
202 | let currentDay = Calendar.current.component(.day, from: Date())
203 |
204 | // Check if the day has changed
205 | let newestLogFile = logFolderPath + "/" + logFileBaseName
206 | if fileManager.fileExists(atPath: newestLogFile) {
207 | let fileCreationDate = try? fileManager.attributesOfItem(atPath: newestLogFile)[.creationDate] as? Date
208 | if let creationDate = fileCreationDate {
209 | let dayOfCreation = Calendar.current.component(.day, from: creationDate)
210 | if dayOfCreation != currentDay {
211 | // rotate files
212 | for archivedLogFile in (1...maxLogFiles).reversed() {
213 | let sourcePath = logFolderPath + "/" + (archivedLogFile == 1 ? logFileBaseName : "\(logFileBaseName).\(archivedLogFile-1)")
214 | let destinationPath = logFolderPath + "/" + "\(logFileBaseName).\(archivedLogFile)"
215 |
216 | if fileManager.fileExists(atPath: sourcePath) {
217 | if archivedLogFile == maxLogFiles {
218 | // Delete the oldest log file if it exists
219 | try? fileManager.removeItem(atPath: sourcePath)
220 | } else {
221 | // Move the log file to the next number in the rotation
222 | try? fileManager.moveItem(atPath: sourcePath, toPath: destinationPath)
223 | }
224 | }
225 | }
226 | writeLog("Logrotate complete", logLevel: .debug)
227 | }
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/Outset/Utils/ItemProcessing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Processing.swift
3 | // outset
4 | //
5 | // Created by Bart Reardon on 3/12/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | func processItems(_ path: String, deleteItems: Bool=false, once: Bool=false, override: [String: Date] = [:]) {
11 | // Main processing logic
12 |
13 | if !checkFileExists(path: path) {
14 | writeLog("\(path) does not exist. Exiting")
15 | exit(1)
16 | }
17 |
18 | // Profile support has been removed in Outset v4
19 | var itemsToProcess: [String] = [] // raw list of files
20 | var packages: [String] = [] // array of packages once they have passed checks
21 | var scripts: [String] = [] // array of scripts once they have passed checks
22 | var runOnceDict: [String: Date] = [:]
23 |
24 | let checksumList = checksumLoadApprovedFiles()
25 | let checksumsAvailable = !checksumList.isEmpty
26 |
27 | // See if there's any old stuff to migrate
28 | // Perform this each processing run to pick up individual user preferences as well
29 | migrateLegacyPreferences()
30 |
31 | // Get a list of all the files to process
32 | itemsToProcess = folderContents(path: path)
33 |
34 | // iterate over the list and check the
35 | for pathname in itemsToProcess {
36 | if verifyPermissions(pathname: pathname) {
37 | switch pathname.split(separator: ".").last {
38 | case "pkg", "mpkg", "dmg":
39 | packages.append(pathname)
40 | default:
41 | scripts.append(pathname)
42 | }
43 | } else {
44 | writeLog("Bad permissions: \(pathname)", logLevel: .error)
45 | }
46 | }
47 |
48 | // load runonce data
49 | runOnceDict = loadRunOncePlist()
50 |
51 | // loop through the packages list and process installs.
52 | for package in packages {
53 | if checksumsAvailable && !verifySHASUMForFile(filename: package, shasumArray: checksumList) {
54 | continue
55 | }
56 |
57 | if once {
58 | if !runOnceDict.contains(where: {$0.key == package}) {
59 | if installPackage(pkg: package) {
60 | runOnceDict.updateValue(Date(), forKey: package)
61 | }
62 | } else {
63 | if override.contains(where: {$0.key == package}) {
64 | writeLog("override for \(package) dated \(override[package]!)", logLevel: .debug)
65 | if override[package]! > runOnceDict[package]! {
66 | writeLog("Actioning package override", logLevel: .debug)
67 | if installPackage(pkg: package) {
68 | runOnceDict.updateValue(Date(), forKey: package)
69 | }
70 | }
71 | }
72 | }
73 | } else {
74 | _ = installPackage(pkg: package)
75 | }
76 | if deleteItems {
77 | pathCleanup(pathname: package)
78 | }
79 | }
80 |
81 | // loop through the scripts list and process.
82 | for script in scripts {
83 | if checksumsAvailable && !verifySHASUMForFile(filename: script, shasumArray: checksumList) {
84 | continue
85 | }
86 |
87 | if once {
88 | writeLog("Processing run-once \(script)", logLevel: .info)
89 | // If this is supposed to be a runonce item then we want to check to see if has an existing runonce entry
90 | // looks for a key with the full script path. Writes the full path and run date when done
91 | if !runOnceDict.contains(where: {$0.key == script}) {
92 | writeLog("run-once not yet processed. proceeding", logLevel: .debug)
93 | let (output, error, status) = runShellCommand(script, args: [consoleUser], verbose: true)
94 | if status != 0 {
95 | writeLog(error, logLevel: .error)
96 | } else {
97 | runOnceDict.updateValue(Date(), forKey: script)
98 | writeLog(output)
99 | }
100 | } else {
101 | // there's a run-once plist entry for this script. Check to see if there's an override
102 | writeLog("checking for override", logLevel: .debug)
103 | if override.contains(where: {$0.key == script}) {
104 | writeLog("override for \(script) dated \(override[script]!)", logLevel: .debug)
105 | if override[script]! > runOnceDict[script]! {
106 | writeLog("Actioning script override", logLevel: .debug)
107 | let (output, error, status) = runShellCommand(script, args: [consoleUser], verbose: true)
108 | if status != 0 {
109 | writeLog(error, logLevel: .error)
110 | } else {
111 | runOnceDict.updateValue(Date(), forKey: script)
112 | if !output.isEmpty {
113 | writeLog(output, logLevel: .debug)
114 | }
115 | }
116 | }
117 | } else {
118 | writeLog("no override for \(script)", logLevel: .debug)
119 | }
120 | }
121 | } else {
122 | writeLog("Processing script \(script)", logLevel: .info)
123 | let (_, error, status) = runShellCommand(script, args: [consoleUser], verbose: true)
124 | if status != 0 {
125 | writeLog(error, logLevel: .error)
126 | }
127 | }
128 | if deleteItems {
129 | pathCleanup(pathname: script)
130 | }
131 | }
132 |
133 | if !runOnceDict.isEmpty {
134 | writeRunOncePlist(runOnceData: runOnceDict)
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/Outset/Utils/Logging.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logging.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | // swiftlint:disable force_try
12 | class StandardError: TextOutputStream {
13 | func write(_ string: String) {
14 | if #available(macOS 10.15.4, *) {
15 | try! FileHandle.standardError.write(contentsOf: Data(string.utf8))
16 | } else {
17 | // Fallback on earlier versions (should work on pre 10.15.4 but untested)
18 | if let data = string.data(using: .utf8) {
19 | FileHandle.standardError.write(data)
20 | }
21 | }
22 | }
23 | }
24 | // swiftlint:enable force_try
25 |
26 | func oslogTypeToString(_ type: OSLogType) -> String {
27 | switch type {
28 | case OSLogType.default: return "default"
29 | case OSLogType.info: return "info"
30 | case OSLogType.debug: return "debug"
31 | case OSLogType.error: return "error"
32 | case OSLogType.fault: return "fault"
33 | default: return "unknown"
34 | }
35 | }
36 |
37 | func printStdErr(_ errorMessage: String) {
38 | var standardError = StandardError()
39 | print(errorMessage, to: &standardError)
40 | }
41 |
42 | func printStdOut(_ message: String) {
43 | print(message)
44 | }
45 |
46 | func writeLog(_ message: String, logLevel: OSLogType = .info, log: OSLog = osLog) {
47 | // write to the system logs
48 |
49 | // let logger = Logger() // 'Logger' is only available in macOS 11.0 or newer so we use os_log
50 |
51 | os_log("%{public}@", log: log, type: logLevel, message)
52 | switch logLevel {
53 | case .error, .debug, .fault:
54 | printStdErr("\(oslogTypeToString(logLevel).uppercased()): \(message)")
55 | default:
56 | printStdOut("\(oslogTypeToString(logLevel).uppercased()): \(message)")
57 | }
58 |
59 | // also write to a log file
60 | writeFileLog(message: message, logLevel: logLevel)
61 | }
62 |
63 | func writeFileLog(message: String, logLevel: OSLogType) {
64 | // write to a log file for accessability of those that don't want to manage the system log
65 | if logLevel == .debug && !debugMode {
66 | return
67 | }
68 | let logFileURL = URL(fileURLWithPath: logFilePath)
69 | if !checkFileExists(path: logFilePath) {
70 | FileManager.default.createFile(atPath: logFileURL.path, contents: nil, attributes: nil)
71 | let attributes = [FileAttributeKey.posixPermissions: 0o666]
72 | do {
73 | try FileManager.default.setAttributes(attributes, ofItemAtPath: logFileURL.path)
74 | } catch {
75 | printStdErr("\(oslogTypeToString(.error).uppercased()): Unable to create log file at \(logFilePath)")
76 | printStdErr(error.localizedDescription)
77 | return
78 | }
79 | }
80 | do {
81 | let fileHandle = try FileHandle(forWritingTo: logFileURL)
82 | defer { fileHandle.closeFile() }
83 |
84 | let dateFormatter = DateFormatter()
85 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
86 |
87 | let date = dateFormatter.string(from: Date())
88 | let logEntry = "\(date) \(oslogTypeToString(logLevel).uppercased()): \(message)\n"
89 |
90 | fileHandle.seekToEndOfFile()
91 | fileHandle.write(logEntry.data(using: .utf8)!)
92 | } catch {
93 | printStdErr("\(oslogTypeToString(.error).uppercased()): Unable to read log file at \(logFilePath)")
94 | printStdErr(error.localizedDescription)
95 | return
96 | }
97 | }
98 |
99 | func writeSysReport() {
100 | // Logs system information to log file
101 | writeLog("User: \(getConsoleUserInfo())", logLevel: .debug)
102 | writeLog("Model: \(getDeviceHardwareModel())", logLevel: .debug)
103 | writeLog("Marketing Model: \(getMarketingModel())", logLevel: .debug)
104 | writeLog("Serial: \(getDeviceSerialNumber())", logLevel: .debug)
105 | writeLog("OS: \(getOSVersion())", logLevel: .debug)
106 | writeLog("Build: \(getOSBuildVersion())", logLevel: .debug)
107 | }
108 |
--------------------------------------------------------------------------------
/Outset/Utils/Network.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Network.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 | import SystemConfiguration
10 |
11 | func isNetworkUp() -> Bool {
12 | // https://stackoverflow.com/a/39782859/17584669
13 | // perform a check to see if the network is available.
14 |
15 | var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
16 | zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
17 | zeroAddress.sin_family = sa_family_t(AF_INET)
18 |
19 | let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
20 | $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
21 | SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
22 | }
23 | }
24 |
25 | var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
26 | if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
27 | return false
28 | }
29 |
30 | let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
31 | let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
32 | let ret = (isReachable && !needsConnection)
33 |
34 | return ret
35 | }
36 |
37 | func waitForNetworkUp(timeout: Double) -> Bool {
38 | // used during --boot if "wait_for_network" prefrence is true
39 | var networkUp = false
40 | let deadline = DispatchTime.now() + timeout
41 | while !networkUp && DispatchTime.now() < deadline {
42 | writeLog("Waiting for network: \(timeout) seconds", logLevel: .debug)
43 | networkUp = isNetworkUp()
44 | if !networkUp {
45 | writeLog("Waiting...", logLevel: .debug)
46 | Thread.sleep(forTimeInterval: 1)
47 | }
48 | }
49 | if !networkUp && DispatchTime.now() > deadline {
50 | writeLog("No network connectivity detected after \(timeout) seconds", logLevel: .error)
51 | }
52 | return networkUp
53 | }
54 |
--------------------------------------------------------------------------------
/Outset/Utils/Preferences.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Preferences.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct OutsetPreferences: Codable {
11 | var waitForNetwork: Bool = false
12 | var networkTimeout: Int = 180
13 | var ignoredUsers: [String] = []
14 | var overrideLoginOnce: [String: Date] = [String: Date]()
15 |
16 | enum CodingKeys: String, CodingKey {
17 | case waitForNetwork = "wait_for_network"
18 | case networkTimeout = "network_timeout"
19 | case ignoredUsers = "ignored_users"
20 | case overrideLoginOnce = "override_login_once"
21 | }
22 | }
23 |
24 | func writeOutsetPreferences(prefs: OutsetPreferences) {
25 |
26 | if debugMode {
27 | showPrefrencePath("Stor")
28 | }
29 |
30 | let defaults = UserDefaults.standard
31 |
32 | // Take the OutsetPreferences object and write it to UserDefaults
33 | let mirror = Mirror(reflecting: prefs)
34 | for child in mirror.children {
35 | // Use the name of each property as the key, and save its value to UserDefaults
36 | if let propertyName = child.label {
37 | let key = propertyName.camelCaseToUnderscored()
38 | if isRoot() {
39 | // write the preference to /Library/Preferences/
40 | CFPreferencesSetValue(key as CFString,
41 | child.value as CFPropertyList,
42 | Bundle.main.bundleIdentifier! as CFString,
43 | kCFPreferencesAnyUser,
44 | kCFPreferencesAnyHost)
45 | } else {
46 | // write the preference to ~/Library/Preferences/
47 | defaults.set(child.value, forKey: key)
48 | }
49 | }
50 | }
51 | }
52 |
53 | func loadOutsetPreferences() -> OutsetPreferences {
54 |
55 | if debugMode {
56 | showPrefrencePath("Load")
57 | }
58 |
59 | let defaults = UserDefaults.standard
60 | var outsetPrefs = OutsetPreferences()
61 |
62 | if isRoot() {
63 | // force preferences to be read from /Library/Preferences instead of root's preferences
64 | outsetPrefs.networkTimeout = CFPreferencesCopyValue("network_timeout" as CFString, Bundle.main.bundleIdentifier! as CFString, kCFPreferencesAnyUser, kCFPreferencesAnyHost) as? Int ?? 180
65 | outsetPrefs.ignoredUsers = CFPreferencesCopyValue("ignored_users" as CFString, Bundle.main.bundleIdentifier! as CFString, kCFPreferencesAnyUser, kCFPreferencesAnyHost) as? [String] ?? []
66 | outsetPrefs.overrideLoginOnce = CFPreferencesCopyValue("override_login_once" as CFString, Bundle.main.bundleIdentifier! as CFString, kCFPreferencesAnyUser, kCFPreferencesAnyHost) as? [String: Date] ?? [:]
67 | outsetPrefs.waitForNetwork = (CFPreferencesCopyValue("wait_for_network" as CFString, Bundle.main.bundleIdentifier! as CFString, kCFPreferencesAnyUser, kCFPreferencesAnyHost) != nil)
68 | } else {
69 | // load preferences for the current user, which includes /Library/Preferences
70 | outsetPrefs.networkTimeout = defaults.integer(forKey: "network_timeout")
71 | outsetPrefs.ignoredUsers = defaults.array(forKey: "ignored_users") as? [String] ?? []
72 | outsetPrefs.overrideLoginOnce = defaults.object(forKey: "override_login_once") as? [String: Date] ?? [:]
73 | outsetPrefs.waitForNetwork = defaults.bool(forKey: "wait_for_network")
74 | }
75 | return outsetPrefs
76 | }
77 |
78 | func loadRunOncePlist() -> [String: Date] {
79 |
80 | if debugMode {
81 | showPrefrencePath("Load")
82 | }
83 |
84 | let defaults = UserDefaults.standard
85 | var runOnceKey = "run_once"
86 |
87 | if isRoot() {
88 | runOnceKey += "-"+getConsoleUserInfo().username
89 | return CFPreferencesCopyValue(runOnceKey as CFString, Bundle.main.bundleIdentifier! as CFString, kCFPreferencesAnyUser, kCFPreferencesAnyHost) as? [String: Date] ?? [:]
90 | } else {
91 | return defaults.object(forKey: runOnceKey) as? [String: Date] ?? [:]
92 | }
93 | }
94 |
95 | func writeRunOncePlist(runOnceData: [String: Date]) {
96 |
97 | if debugMode {
98 | showPrefrencePath("Stor")
99 | }
100 |
101 | let defaults = UserDefaults.standard
102 | var runOnceKey = "run_once"
103 |
104 | if isRoot() {
105 | runOnceKey += "-"+getConsoleUserInfo().username
106 | CFPreferencesSetValue(runOnceKey as CFString,
107 | runOnceData as CFPropertyList,
108 | Bundle.main.bundleIdentifier! as CFString,
109 | kCFPreferencesAnyUser,
110 | kCFPreferencesAnyHost)
111 | } else {
112 | defaults.set(runOnceData, forKey: runOnceKey)
113 | }
114 | }
115 |
116 | func migrateLegacyPreferences() {
117 | let newoldRootUserDefaults = "/var/root/Library/Preferences/io.macadmins.Outset.plist"
118 | // shared folder should not contain any executable content, iterate and update as required
119 | if checkFileExists(path: shareDirectory) || checkFileExists(path: newoldRootUserDefaults) {
120 | writeLog("Legacy preferences exist. Migrating to user defaults", logLevel: .debug)
121 |
122 | let legacyOutsetPreferencesFile = "\(shareDirectory)com.chilcote.outset.plist"
123 | let legacyRootRunOncePlistFile = "com.github.outset.once.\(getConsoleUserInfo().userID).plist"
124 | let userHomeDirectory = FileManager.default.homeDirectoryForCurrentUser
125 | let userHomePath = userHomeDirectory.relativeString.replacingOccurrences(of: "file://", with: "")
126 | let legacyUserRunOncePlistFile = userHomePath+"Library/Preferences/com.github.outset.once.plist"
127 |
128 | var shareFiles: [String] = []
129 | shareFiles.append(legacyOutsetPreferencesFile)
130 | shareFiles.append(legacyRootRunOncePlistFile)
131 | shareFiles.append(legacyUserRunOncePlistFile)
132 | shareFiles.append(newoldRootUserDefaults)
133 |
134 | for filename in shareFiles where checkFileExists(path: filename) {
135 |
136 | let url = URL(fileURLWithPath: filename)
137 | do {
138 | let data = try Data(contentsOf: url)
139 | switch filename {
140 |
141 | case newoldRootUserDefaults:
142 | if isRoot() {
143 | writeLog("\(newoldRootUserDefaults) migration", logLevel: .debug)
144 | let legacyDefaultKeys = CFPreferencesCopyKeyList(Bundle.main.bundleIdentifier! as CFString, kCFPreferencesCurrentUser, kCFPreferencesAnyHost)
145 | for key in legacyDefaultKeys as! [CFString] {
146 | let keyValue = CFPreferencesCopyValue(key, Bundle.main.bundleIdentifier! as CFString, kCFPreferencesCurrentUser, kCFPreferencesAnyHost)
147 | CFPreferencesSetValue(key as CFString,
148 | keyValue as CFPropertyList,
149 | Bundle.main.bundleIdentifier! as CFString,
150 | kCFPreferencesAnyUser,
151 | kCFPreferencesAnyHost)
152 | }
153 | deletePath(newoldRootUserDefaults)
154 | }
155 | case legacyOutsetPreferencesFile:
156 | if isRoot() {
157 | writeLog("\(legacyOutsetPreferencesFile) migration", logLevel: .debug)
158 | do {
159 | let legacyPreferences = try PropertyListDecoder().decode(OutsetPreferences.self, from: data)
160 | writeOutsetPreferences(prefs: legacyPreferences)
161 | writeLog("Migrated Legacy Outset Preferences", logLevel: .debug)
162 | deletePath(legacyOutsetPreferencesFile)
163 | } catch {
164 | writeLog("legacy Preferences migration failed", logLevel: .error)
165 | }
166 | }
167 | case legacyRootRunOncePlistFile, legacyUserRunOncePlistFile:
168 | writeLog("\(legacyRootRunOncePlistFile) and \(legacyUserRunOncePlistFile) migration", logLevel: .debug)
169 | do {
170 | let legacyRunOncePlistData = try PropertyListDecoder().decode([String: Date].self, from: data)
171 | writeRunOncePlist(runOnceData: legacyRunOncePlistData)
172 | writeLog("Migrated Legacy Runonce Data", logLevel: .debug)
173 | if isRoot() {
174 | deletePath(legacyRootRunOncePlistFile)
175 | } else {
176 | deletePath(legacyUserRunOncePlistFile)
177 | }
178 | } catch {
179 | writeLog("legacy Run Once Plist migration failed", logLevel: .error)
180 | }
181 |
182 | default:
183 | continue
184 | }
185 | } catch {
186 | writeLog("could not load \(filename)", logLevel: .error)
187 | }
188 |
189 | }
190 |
191 | if checkFileExists(path: shareDirectory) && folderContents(path: shareDirectory).isEmpty {
192 | deletePath(shareDirectory)
193 | }
194 | }
195 |
196 | }
197 |
198 | func showPrefrencePath(_ action: String) {
199 | var prefsPath: String
200 | if isRoot() {
201 | prefsPath = "/Library/Preferences".appending("/\(Bundle.main.bundleIdentifier!).plist")
202 | } else {
203 | let path = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)
204 | prefsPath = path[0].appending("/Preferences").appending("/\(Bundle.main.bundleIdentifier!).plist")
205 | }
206 | writeLog("\(action)ing preference file: \(prefsPath)", logLevel: .debug)
207 | }
208 |
--------------------------------------------------------------------------------
/Outset/Utils/Services.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Services.swift
3 | // Outset
4 | //
5 | // Created by Bart Reardon on 21/3/2023.
6 | //
7 |
8 | import Foundation
9 | import ServiceManagement
10 |
11 | @available(macOS 13.0, *)
12 | class ServiceManager {
13 |
14 | // The identifier must match the CFBundleIdentifier string in Info.plist.
15 | // LaunchDaemon path: $APP.app/Contents/Library/LaunchDaemons/
16 | let bootDaemon = SMAppService.daemon(plistName: "io.macadmins.Outset.boot.plist")
17 | let loginPrivilegedDaemon = SMAppService.daemon(plistName: "io.macadmins.Outset.login-privileged.plist")
18 | let cleanupDaemon = SMAppService.daemon(plistName: "io.macadmins.Outset.cleanup.plist")
19 |
20 | // LaunchAgent path: $APP.app/Contents/Library/LaunchAgents/
21 | let loginAgent = SMAppService.agent(plistName: "io.macadmins.Outset.login.plist")
22 | let onDemandAgent = SMAppService.agent(plistName: "io.macadmins.Outset.on-demand.plist")
23 | let loginWindowAgent = SMAppService.agent(plistName: "io.macadmins.Outset.login-window.plist")
24 |
25 | func servicesEnabled() -> Bool {
26 | return SMAppService.mainApp.status == .enabled
27 | }
28 |
29 | func disableAppService() {
30 | try? SMAppService.mainApp.unregister()
31 | }
32 |
33 | func enableAppService() {
34 | try? SMAppService.mainApp.register()
35 | }
36 |
37 | private func register(_ service: SMAppService) {
38 | if !isRoot() {
39 | writeLog("Must be root to register \(service.description)",
40 | logLevel: .error)
41 | return
42 | }
43 | if service.status == .enabled {
44 | writeLog("\(service.description) status: \(statusToString(service))",
45 | logLevel: .info)
46 | } else {
47 | do {
48 | try service.register()
49 | } catch let error {
50 | if error.localizedDescription.contains("Operation not permitted") {
51 | writeLog("\(service.description): \(error.localizedDescription). Login item requires approval", logLevel: .error)
52 | } else if !error.localizedDescription.contains("Service cannot load in requested session") {
53 | writeLog("\(service.description): \(error.localizedDescription)", logLevel: .error)
54 | }
55 | }
56 | }
57 | }
58 |
59 | private func unregister(_ service: SMAppService) {
60 | if !isRoot() {
61 | writeLog("Must be root to unregister \(service.description)",
62 | logLevel: .error)
63 | return
64 | }
65 | if service.status == .enabled {
66 | do {
67 | try service.unregister()
68 | } catch let error {
69 | writeLog("\(service.description): \(error.localizedDescription)", logLevel: .error)
70 | }
71 | } else {
72 | writeLog("\(service.description) status: \(statusToString(service))",
73 | logLevel: .info)
74 | }
75 | }
76 |
77 | private func statusToString(_ service: SMAppService) -> String {
78 | switch service.status {
79 | case .notRegistered:
80 | return "Not Registered"
81 | case .enabled:
82 | return "Enabled"
83 | case .requiresApproval:
84 | return "Requires Approval"
85 | case .notFound:
86 | return "Not Found"
87 | default:
88 | return "Unknown status"
89 | }
90 | }
91 |
92 | private func status(_ service: SMAppService) {
93 | writeLog("\(service.description) status: \(statusToString(service))", logLevel: .info)
94 | }
95 |
96 | func registerDaemons() {
97 | writeLog("registering Services", logLevel: .debug)
98 | register(bootDaemon)
99 | register(loginPrivilegedDaemon)
100 | register(cleanupDaemon)
101 | register(loginAgent)
102 | register(onDemandAgent)
103 | // Disabled for the time being until ServiceManagement and loginwindow agent issues are resolved
104 | // register(loginWindowAgent)
105 | }
106 |
107 | func removeDaemons() {
108 | writeLog("de-registering Services", logLevel: .debug)
109 | unregister(bootDaemon)
110 | unregister(loginPrivilegedDaemon)
111 | unregister(cleanupDaemon)
112 | unregister(loginAgent)
113 | unregister(onDemandAgent)
114 | // Disabled for the time being until ServiceManagement and loginwindow agent issues are resolved
115 | // unregister(loginWindowAgent)
116 | }
117 |
118 | func getStatus() {
119 | writeLog("getting Services status", logLevel: .info)
120 | status(bootDaemon)
121 | status(loginPrivilegedDaemon)
122 | status(cleanupDaemon)
123 | status(loginAgent)
124 | status(onDemandAgent)
125 | status(loginWindowAgent)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Outset/Utils/ShellUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShellUtils.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | func runShellCommand(_ command: String, args: [String] = [], verbose: Bool = false) -> (output: String, error: String, exitCode: Int32) {
11 | // runs a shell command passed as an argument
12 | // If the verbose parameter is set to true, will log the command being run and its status when completed.
13 | // returns the output, error and exit code as a tuple.
14 |
15 | if verbose {
16 | writeLog("Running task \(command)", logLevel: .debug)
17 | }
18 | let task = Process()
19 | let pipe = Pipe()
20 | let errorpipe = Pipe()
21 |
22 | var cmd = command
23 | for arg in args {
24 | cmd += " '\(arg)'"
25 | }
26 | let arguments = ["-c", cmd]
27 |
28 | var output: String = ""
29 | var error: String = ""
30 |
31 | task.launchPath = "/bin/sh"
32 | task.arguments = arguments
33 | task.standardOutput = pipe
34 | task.standardError = errorpipe
35 | task.launch()
36 |
37 | let data = pipe.fileHandleForReading.readDataToEndOfFile()
38 | let errordata = errorpipe.fileHandleForReading.readDataToEndOfFile()
39 |
40 | output.append(String(data: data, encoding: .utf8)!)
41 | error.append(String(data: errordata, encoding: .utf8)!)
42 |
43 | task.waitUntilExit()
44 | let status = task.terminationStatus
45 | if verbose {
46 | writeLog("Completed task \(command) with status \(status)", logLevel: .debug)
47 | writeLog("Task output: \n\(output)", logLevel: .debug)
48 | }
49 | return (output, error, status)
50 | }
51 |
--------------------------------------------------------------------------------
/Outset/Utils/SystemInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemInfo.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | func getOSVersion() -> String {
11 | // Returns the OS version
12 | let osVersion = ProcessInfo().operatingSystemVersion
13 | let version = "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
14 | return version
15 | }
16 |
17 | func getOSBuildVersion() -> String {
18 | // Returns the current OS build from sysctl
19 | var size = 0
20 | sysctlbyname("kern.osversion", nil, &size, nil, 0)
21 | var osversion = [CChar](repeating: 0, count: size)
22 | sysctlbyname("kern.osversion", &osversion, &size, nil, 0)
23 | return String(cString: osversion)
24 |
25 | }
26 |
27 | func getDeviceSerialNumber() -> String {
28 | // Returns the current devices serial number
29 | let platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice") )
30 | guard platformExpert > 0 else {
31 | return "Serial Unknown"
32 | }
33 | guard let serialNumber = (IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0).takeUnretainedValue() as? String)?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) else {
34 | return "Serial Unknown"
35 | }
36 | IOObjectRelease(platformExpert)
37 | return serialNumber
38 | }
39 |
40 | func getMarketingModel() -> String {
41 | let appleSiliconProduct = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/AppleARMPE/product")
42 | let cfKeyValue = IORegistryEntryCreateCFProperty(appleSiliconProduct, "product-description" as CFString, kCFAllocatorDefault, 0)
43 | IOObjectRelease(appleSiliconProduct)
44 | let keyValue: AnyObject? = cfKeyValue?.takeUnretainedValue()
45 | if keyValue != nil, let data = keyValue as? Data {
46 | return String(data: data, encoding: String.Encoding.utf8)?.trimmingCharacters(in: CharacterSet(["\0"])) ?? ""
47 | }
48 | return ""
49 | }
50 |
51 | func getDeviceHardwareModel() -> String {
52 | // Returns the current devices hardware model from sysctl
53 | var size = 0
54 | sysctlbyname("hw.model", nil, &size, nil, 0)
55 | var model = [CChar](repeating: 0, count: size)
56 | sysctlbyname("hw.model", &model, &size, nil, 0)
57 | return String(cString: model)
58 | }
59 |
--------------------------------------------------------------------------------
/Outset/Utils/SystemUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Functions.swift
3 | // outset
4 | //
5 | // Created by Bart Reardon on 1/12/2022.
6 | //
7 |
8 | import Foundation
9 | import SystemConfiguration
10 | import IOKit
11 | import CoreFoundation
12 |
13 | enum Action {
14 | case enable
15 | case disable
16 | }
17 |
18 | func ensureRoot(_ reason: String) {
19 | if !isRoot() {
20 | writeLog("Must be root to \(reason)", logLevel: .error)
21 | exit(1)
22 | }
23 | }
24 |
25 | func isRoot() -> Bool {
26 | return NSUserName() == "root"
27 | }
28 |
29 | func getConsoleUserInfo() -> (username: String, userID: String) {
30 | // We need the console user, not the process owner so NSUserName() won't work for our needs when outset runs as root
31 | var uid: uid_t = 0
32 | if let consoleUser = SCDynamicStoreCopyConsoleUser(nil, &uid, nil) as? String {
33 | return (consoleUser, "\(uid)")
34 | } else {
35 | return ("", "")
36 | }
37 | }
38 |
39 | func loginWindowUpdateState(_ action: Action) {
40 | var cmd: String
41 | let loginWindowPlist: String = "/System/Library/LaunchDaemons/com.apple.loginwindow.plist"
42 | switch action {
43 | case .enable:
44 | writeLog("Enabling loginwindow process", logLevel: .debug)
45 | cmd = "/bin/launchctl load \(loginWindowPlist)"
46 | case .disable:
47 | writeLog("Disabling loginwindow process", logLevel: .debug)
48 | cmd = "/bin/launchctl unload \(loginWindowPlist)"
49 | }
50 | _ = runShellCommand(cmd)
51 | }
52 |
--------------------------------------------------------------------------------
/OutsetInstaller/OutsetInstaller.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Package/Library/LaunchAgents/io.macadmins.Outset.login-window.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | OnDemand
6 |
7 | LaunchOnlyOnce
8 |
9 | Program
10 |
11 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
12 |
13 | LimitLoadToSessionType
14 |
15 | LoginWindow
16 |
17 | AssociatedBundleIdentifiers
18 | io.macadmins.Outset
19 | Label
20 | io.macadmins.Outset.login-window
21 | ProgramArguments
22 |
23 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
24 | --login-window
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Package/Library/LaunchAgents/io.macadmins.Outset.login.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 |
7 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
8 |
9 | Label
10 | io.macadmins.Outset.login
11 | AssociatedBundleIdentifiers
12 | io.macadmins.Outset
13 | ProgramArguments
14 |
15 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
16 | --login
17 |
18 | RunAtLoad
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Package/Library/LaunchAgents/io.macadmins.Outset.on-demand.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 |
7 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
8 |
9 | AssociatedBundleIdentifiers
10 | io.macadmins.Outset
11 | KeepAlive
12 |
13 | PathState
14 |
15 | /private/tmp/.io.macadmins.outset.ondemand.launchd
16 |
17 |
18 |
19 | Label
20 | io.macadmins.Outset.on-demand
21 | OnDemand
22 |
23 | ProgramArguments
24 |
25 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
26 | --on-demand
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Package/Library/LaunchDaemons/io.macadmins.Outset.boot.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 |
7 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
8 |
9 | AssociatedBundleIdentifiers
10 | io.macadmins.Outset
11 | Label
12 | io.macadmins.Outset.boot
13 | ProgramArguments
14 |
15 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
16 | --boot
17 |
18 | RunAtLoad
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Package/Library/LaunchDaemons/io.macadmins.Outset.cleanup.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 |
7 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
8 |
9 | AssociatedBundleIdentifiers
10 | io.macadmins.Outset
11 | KeepAlive
12 |
13 | PathState
14 |
15 | /private/tmp/.io.macadmins.outset.cleanup.launchd
16 |
17 |
18 |
19 | Label
20 | io.macadmins.Outset.cleanup
21 | OnDemand
22 |
23 | ProgramArguments
24 |
25 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
26 | --cleanup
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Package/Library/LaunchDaemons/io.macadmins.Outset.login-privileged.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 |
7 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
8 |
9 | AssociatedBundleIdentifiers
10 | io.macadmins.Outset
11 | KeepAlive
12 |
13 | PathState
14 |
15 | /private/tmp/.io.macadmins.outset.login-privileged.launchd
16 |
17 |
18 |
19 | Label
20 | io.macadmins.Outset.login-privileged
21 | OnDemand
22 |
23 | ProgramArguments
24 |
25 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
26 | --login-privileged
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Package/Scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Postinstall script for outset CLI
4 | #
5 | # Created by Bart Reardon on 9/1/2023.
6 | #
7 |
8 | # Get the macOS version number
9 | macos_version="$(sw_vers -productVersion)"
10 | LD_ROOT="/Library/LaunchDaemons"
11 | LA_ROOT="/Library/LaunchAgents"
12 |
13 | APP_PATH="/usr/local/outset/Outset.app"
14 | APP_ROOT="${APP_PATH}/Contents"
15 |
16 | # register the app bundle
17 | /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister "${APP_PATH}"
18 |
19 | # run boot argument - creates necessary paths and folders
20 | "${APP_PATH}/Contents/MacOS/Outset" --boot
21 |
22 | ### Commented out for now until issues with SMAppService are sorted
23 | # Check if the macOS version is 13 or newer. If so, we don't need to load the launchd plists manually.
24 | #if [[ $(echo "${macos_version}" | cut -d'.' -f1) -ge 13 ]]; then
25 | # # register the agents
26 | # "${APP_PATH}/Contents/MacOS/Outset" --enable-services
27 | #
28 | # # issue with ServiceManagement in that login-window agents don't get loaded so we'll copy that one over manually
29 | # cp "${APP_ROOT}/${LA_ROOT}/io.macadmins.outset.login-window.plist" "${LA_ROOT}"
30 | # exit 0
31 | #fi
32 | ###
33 |
34 | ## LaunchDaemons
35 | DAEMONS=(
36 | "${LD_ROOT}/io.macadmins.outset.boot.plist"
37 | "${LD_ROOT}/io.macadmins.outset.cleanup.plist"
38 | "${LD_ROOT}/io.macadmins.outset.login-privileged.plist"
39 | )
40 |
41 | ## LaunchAgents
42 | AGENTS=(
43 | "${LA_ROOT}/io.macadmins.outset.login.plist"
44 | "${LA_ROOT}/io.macadmins.outset.on-demand.plist"
45 | "${LA_ROOT}/io.macadmins.outset.login-window.plist"
46 | )
47 |
48 | for daemon in ${DAEMONS}; do
49 | cp "${APP_ROOT}/${daemon}" "${LD_ROOT}"
50 | if [ -e "${daemon}" ]; then
51 | /bin/launchctl bootstrap system "${daemon}"
52 | fi
53 | done
54 |
55 | for agent in ${AGENTS}; do
56 | cp "${APP_ROOT}/${agent}" "${LA_ROOT}"
57 | echo ${agent}
58 | done
59 |
--------------------------------------------------------------------------------
/Package/Scripts/preinstall:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # preinstall.sh
4 | # Outset
5 | #
6 | # Created by Bart Reardon on 25/3/2023.
7 | #
8 |
9 | ## Process legacy launchd items if upgrading from outset 3.0.3 or earlier
10 |
11 | LD_ROOT="/Library/LaunchDaemons"
12 | LA_ROOT="/Library/LaunchAgents"
13 | OUTSET_ROOT="/usr/local/outset"
14 | OUTSET_BACKUP="${OUTSET_ROOT}/backup"
15 |
16 | USER_ID=$(id -u "$(/usr/bin/stat -f %Su /dev/console)")
17 |
18 | ## LaunchDaemons
19 | DAEMONS=(
20 | "${LD_ROOT}/com.github.outset.boot.plist"
21 | "${LD_ROOT}/com.github.outset.cleanup.plist"
22 | "${LD_ROOT}/com.github.outset.login-privileged.plist"
23 | )
24 |
25 | ## LaunchAgents
26 | AGENTS=(
27 | "${LA_ROOT}/com.github.outset.login.plist"
28 | "${LA_ROOT}/com.github.outset.on-demand.plist"
29 | )
30 |
31 | # Unload if present
32 | for daemon in $DAEMONS; do
33 | if [ -e "${daemon}" ]; then
34 | /bin/launchctl bootout system "${daemon}"
35 | rm -fv "${daemon}"
36 | fi
37 | done
38 |
39 | for agent in $AGENTS; do
40 | if [ -e "${agent}" ]; then
41 | if [ ${USER_ID} -ne 0 ]; then
42 | /bin/launchctl bootout gui/${USER_ID} "${agent}"
43 | fi
44 | rm -fv "${agent}"
45 | fi
46 | done
47 |
48 | # backup existing preference files
49 | mkdir -p "${OUTSET_BACKUP}"
50 |
51 | if [ -d "${OUTSET_ROOT}/share" ]; then
52 | cp ${OUTSET_ROOT}/share/* "${OUTSET_BACKUP}/"
53 | fi
54 |
55 | for user in /Users/*; do
56 | if [ -e "/Users/${user}/Library/Preferences/com.github.outset.once.plist" ]; then
57 | mkdir -p "${OUTSET_ROOT}/backup/${user}"
58 | cp "/Users/${user}/Library/Preferences/com.github.outset.once.plist" "${OUTSET_BACKUP}/${user}/"
59 | fi
60 | done
61 |
--------------------------------------------------------------------------------
/Package/generatePackage.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | set -x
4 |
5 | cd "${BUILT_PRODUCTS_DIR}"
6 |
7 | STAGING_DIRECTORY="${TMPDIR}/staging"
8 | INSTALL_LOCATION="/usr/local/outset/"
9 | INSTALL_SCRIPTS=${SCRIPT_INPUT_FILE_1}
10 | OUTSET_ALIAS=${SCRIPT_INPUT_FILE_2}
11 | APP_NAME=${PROJECT_NAME}
12 | IDENTIFIER="${PRODUCT_BUNDLE_IDENTIFIER}"
13 | VERSION="${MARKETING_VERSION}"
14 |
15 | # Clean staging dir
16 | rm -r "${STAGING_DIRECTORY}"
17 |
18 | # Set up a staging directory with the contents to install.
19 | mkdir -p "${STAGING_DIRECTORY}/${INSTALL_LOCATION}"
20 | chmod 755 "${OUTSET_ALIAS}"
21 | cp "${OUTSET_ALIAS}" "${STAGING_DIRECTORY}/${INSTALL_LOCATION}"
22 | cp -r "Outset.app" "${STAGING_DIRECTORY}/${INSTALL_LOCATION}"
23 |
24 | # Generate the component property list.
25 | pkgbuild --analyze --root "${STAGING_DIRECTORY}" component.plist
26 |
27 | # Force the installation package (.pkg) to not be relocatable.
28 | # This ensures the package components install in `INSTALL_LOCATION`.
29 | plutil -replace BundleIsRelocatable -bool no component.plist
30 |
31 | # Build a temporary package using the component property list.
32 | pkgbuild --root "${STAGING_DIRECTORY}" --component-plist component.plist --identifier "${IDENTIFIER}" --version "${VERSION}" --scripts "${INSTALL_SCRIPTS}" tmp-package.pkg
33 |
34 | # Synthesize the distribution for the temporary package.
35 | productbuild --synthesize --package tmp-package.pkg --identifier "${IDENTIFIER}" --version "${VERSION}" Distribution
36 |
37 | # Synthesize the final package from the distribution.
38 | productbuild --distribution Distribution --package-path "${BUILT_PRODUCTS_DIR}" "${SCRIPT_OUTPUT_FILE_0}"
39 |
40 | # Get the developer installer identity of possible for signing
41 | # IDENTITY=$(security find-certificate -p -c "Developer ID Installer" 2>/dev/null) && IDENTITY=$(echo "${IDENTITY}" | openssl x509 -noout -subject | sed -n 's/.*CN=\([^/]*\).*/\1/p')
42 | #
43 | # if [[ -z ${IDENTITY} ]]; then
44 | # productbuild --distribution Distribution --package-path "${BUILT_PRODUCTS_DIR}" "${SCRIPT_OUTPUT_FILE_0}"
45 | # else
46 | # productbuild --distribution Distribution --package-path "${BUILT_PRODUCTS_DIR}" --sign "${IDENTITY}" "${SCRIPT_OUTPUT_FILE_0}"
47 | # fi
48 |
49 |
--------------------------------------------------------------------------------
/Package/outset:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | /usr/local/outset/Outset.app/Contents/MacOS/Outset ${@}
4 |
--------------------------------------------------------------------------------
/Package/outset-pkg:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #==================================================================================
3 | #
4 | # FILE: outset-pkg
5 | #
6 | # USAGE: outset-pkg --all [] | --file []
7 | #
8 | # DESCRIPTION: Creates a PKG of /usr/local/outset processing directories
9 | # Either for one file or package the entire structure
10 | #
11 | # NOTES: ---
12 | # AUTHOR: Bart Reardon
13 | # ORGANIZATION: Macadmins Open Source
14 | # CREATED: 2023-03-23
15 | # REVISION: 1.0.2
16 | #
17 | # COPYRIGHT: (C) Macadmins Open Source 2023. All rights reserved.
18 | #
19 | #==================================================================================
20 |
21 | FILE_TO_PACKAGE=""
22 | FILE_TARGET="login-once"
23 | BUILD_ALL=false
24 | POSTINSTALL_SCRIPT=false
25 | MAKE_EXECUTABLE=false
26 | POSTINSTALL_SCRIPT_FILE=""
27 | BUILD_DIRECTORY="/var/tmp/outset"
28 |
29 | PKGTITLE="Outset Custom Content"
30 | PKG_DOMAIN="io.macadmins.Outset"
31 | PKGVERSION="1.0"
32 | PKGID="${PKG_DOMAIN}.custom-content"
33 |
34 | OUTSET_ROOT="/usr/local/outset"
35 | PGKROOT="${BUILD_DIRECTORY}/PGKROOT"
36 | SCRIPT_DIR="${PGKROOT}/scripts"
37 | PKGNAME="outset-custom-content"
38 | ONDEMAND_POSTINSTALL=$(cat <] | --file []"
51 | echo ""
52 | echo "OPTIONS:"
53 | echo " -a, --all Package all scripts from outset processing directories into one package"
54 | echo " -f, --file Package the selected file"
55 | echo " -x, --make-executable Ensure the selected file executable bit is set (only applies to script files)"
56 | echo " -t, --target Target processing directory (default 'login-once')"
57 | echo " -s, --postinstall-script []"
58 | echo " Include a postinstall script. If no argument is given, a standard postinstall"
59 | echo " script to trigger on-demand will be included."
60 | echo " -v, --version, Set package version to the selected number (default is '1.0')"
61 | echo " -p, --build-path, Path to use as the build location (default ${BUILD_DIRECTORY})"
62 | echo " -h, --help Print this message"
63 | echo ""
64 | }
65 |
66 | validateVersion() {
67 | # Regular expression to match a version number
68 | regex='^[0-9]+([.][0-9]+)*$'
69 |
70 | # Check if the string matches the regular expression
71 | if [[ $1 =~ $regex ]]; then
72 | echo $1
73 | else
74 | return 1
75 | fi
76 | }
77 |
78 | validateTarget() {
79 | targetList=($(ls -d ${OUTSET_ROOT}/login-* ${OUTSET_ROOT}/boot-* ${OUTSET_ROOT}/on-*))
80 | for target in "${targetList[@]}"; do
81 | if [[ "${target##*/}" == "$1" ]]; then
82 | echo "${target##*/}"
83 | return 0
84 | fi
85 | done
86 | return 1
87 | }
88 |
89 | printValidTargets() {
90 | targetList=($(ls -d ${OUTSET_ROOT}/login-* ${OUTSET_ROOT}/boot-* ${OUTSET_ROOT}/on-*))
91 | echo "Invalid target name '$1'"
92 | echo "Valid targets:"
93 | for target in ${targetList[@]}; do
94 | echo " [${target##*/}]"
95 | done
96 | exit 1
97 | }
98 |
99 | exitWithError() {
100 | echo "$1"
101 | exit 1
102 | }
103 |
104 | # if no arguments passed, print help and exit
105 | if [[ "$#" -eq 0 ]]; then
106 | printUsage
107 | exit 0
108 | fi
109 |
110 | # Loop through named arguments
111 | while [[ "$#" -gt 0 ]]; do
112 | case $1 in
113 | --file|-f) FILE_TO_PACKAGE="$2"; shift ;;
114 | --make-executable|-x) MAKE_EXECUTABLE=true ;;
115 | --target|-t) FILE_TARGET=$(validateTarget "$2") || printValidTargets "$2"; shift ;;
116 | --version|-v) PKGVERSION=$(validateVersion "$2") || exitWithError "invalid version number $2"; shift ;;
117 | --postinstall-script|-s) POSTINSTALL_SCRIPT_FILE="$2" && POSTINSTALL_SCRIPT=true
118 | # if the first character is a hyphen, then no argument was passed
119 | if [[ -z $POSTINSTALL_SCRIPT_FILE ]] || [[ "${POSTINSTALL_SCRIPT_FILE:0:1}" == "-" ]]; then
120 | POSTINSTALL_SCRIPT_FILE=""
121 | else
122 | shift
123 | fi
124 | ;;
125 | --build-path|-p)
126 | if [[ -e "$2" ]]; then
127 | BUILD_DIRECTORY="${2%/}"
128 | PGKROOT="${BUILD_DIRECTORY}/PGKROOT"
129 | else
130 | exitWithError "path '$2' not found"
131 | fi
132 | shift
133 | ;;
134 | --help|-h|help) printUsage; exit 0 ;;
135 | --all|-a) BUILD_ALL=true ;;
136 | *) echo "Unknown argument: $1"; printUsage; exit 1 ;;
137 | esac
138 | shift
139 | done
140 |
141 | # create PGKROOT structure
142 | mkdir -p "${PGKROOT}${OUTSET_ROOT}"
143 |
144 | if $POSTINSTALL_SCRIPT; then
145 | mkdir -p "${SCRIPT_DIR}"
146 | if [[ -n $POSTINSTALL_SCRIPT_FILE ]]; then
147 | if [[ -e "${POSTINSTALL_SCRIPT_FILE}" ]]; then
148 | cp "${POSTINSTALL_SCRIPT_FILE}" "${SCRIPT_DIR}/postinstall"
149 | else
150 | exitWithError "${POSTINSTALL_SCRIPT_FILE} doesn't exist"
151 | fi
152 | else
153 | echo "${ONDEMAND_POSTINSTALL}" > "${SCRIPT_DIR}/postinstall"
154 | fi
155 | chmod 755 "${SCRIPT_DIR}/postinstall"
156 | fi
157 |
158 | if [[ -n $FILE_TO_PACKAGE ]]; then
159 | PKGID="${PKG_DOMAIN}.${FILE_TARGET}-${FILE_TO_PACKAGE##*/}"
160 | PKGNAME="outset-${FILE_TARGET}-${FILE_TO_PACKAGE##*/}_v${PKGVERSION}"
161 | TARGET_DIR="${PGKROOT}${OUTSET_ROOT}/${FILE_TARGET}/"
162 |
163 | mkdir -p "${TARGET_DIR}"
164 |
165 | if [[ -e "${FILE_TO_PACKAGE}" ]]; then
166 | cp "${FILE_TO_PACKAGE}" "${TARGET_DIR}"
167 | # if the file is not a pkg or dmg, make it executable
168 | if $MAKE_EXECUTABLE && [[ "${FILE_TO_PACKAGE##*.}" != "pkg" ]] && [[ "${FILE_TO_PACKAGE##*.}" != "dmg" ]]; then
169 | chmod 755 "${TARGET_DIR}/${FILE_TO_PACKAGE##*/}"
170 | fi
171 | else
172 | exitWithError "${FILE_TO_PACKAGE} doesn't exist"
173 | fi
174 | elif $BUILD_ALL; then
175 | PKGNAME="outset-custom-content_v${PKGVERSION}"
176 | for folder in $(ls -d ${OUTSET_ROOT}/login-* ${OUTSET_ROOT}/boot-* ${OUTSET_ROOT}/on-*); do
177 | cp -r "${folder}" "${PGKROOT}${OUTSET_ROOT}"
178 | done
179 | fi
180 |
181 | # create package
182 | TMP_PKG="${BUILD_DIRECTORY}/${PKGNAME}.component.pkg"
183 | BUILD_PKG="${BUILD_DIRECTORY}/${PKGNAME}.pkg"
184 |
185 | if $POSTINSTALL_SCRIPT; then
186 | /usr/bin/pkgbuild --root "${PGKROOT}" --scripts "${SCRIPT_DIR}" --identifier ${PKGID} --version ${PKGVERSION} "${TMP_PKG}"
187 | else
188 | /usr/bin/pkgbuild --root "${PGKROOT}" --identifier ${PKGID} --version ${PKGVERSION} "${TMP_PKG}"
189 | fi
190 | /usr/bin/productbuild --identifier ${PKGID} --package "${TMP_PKG}" "${BUILD_PKG}"
191 |
192 | # clean up
193 | rm -r "${PGKROOT}"
194 | rm "${TMP_PKG}"
195 |
196 | # done
197 | echo "Package has been created at ${BUILD_PKG}"
198 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Outset
2 | ======
3 |
4 | 
5 |
6 | Outset is a utility application which automatically processes scripts and packages during the boot sequence, user logins, or on demand.
7 |
8 | [Check out the wiki](https://github.com/macadmins/outset/wiki) for more information on how to use Outset or find out [how it works](https://github.com/chilcote/outset/wiki/FAQ).
9 |
10 | ## Requirements
11 | + macOS 10.15+
12 |
13 | ## Usage
14 |
15 | OPTIONS:
16 | --boot Used by launchd for scheduled runs at boot
17 | --login Used by launchd for scheduled runs at login
18 | --login-window Used by launchd for scheduled runs at the login window
19 | --login-privileged Used by launchd for scheduled privileged runs at login
20 | --on-demand Process scripts on demand
21 | --login-every Manually process scripts in login-every
22 | --login-once Manually process scripts in login-once
23 | --cleanup Used by launchd to clean up on-demand dir
24 | --add-ignored-user
25 | Add one or more users to ignored list
26 | --remove-ignored-user
27 | Remove one or more users from ignored list
28 | --add-override