From 6ff485fcdddaf8dbbaac8af86f308fb1f80b6d48 Mon Sep 17 00:00:00 2001 From: Allen Houchins <32207388+allenhouchins@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:30:51 -0500 Subject: [PATCH] Support custom tap folder for apps without casks; add three apps (#43784) This pull request introduces support for ingesting Homebrew casks from third-party taps (not available in the official `Homebrew/homebrew-cask`) into the Fleet Maintained Apps (FMA) system. It does this by allowing cask metadata to be committed directly into the repository and referenced via a new `cask_path` field. The PR also updates CI workflows to better support Fleet Desktop validation and documents the new contributor flow. **Support for custom Homebrew casks:** * Added a new `cask_path` field to app manifests, allowing the FMA ingester to read cask metadata from a local JSON file instead of fetching from the Homebrew API. This enables ingestion of apps from third-party taps or custom casks not present in the official Homebrew repository. [[1]](diffhunk://#diff-be469dd148f0c50ad56489c48bdb514522e1a46d21336e8f747b5880d71a6d1bR49-R66) [[2]](diffhunk://#diff-abd7db4bef16a062c1bd81f54a7c846f1e91b913a9fe9f87976c8075f39b8cd2R270-R276) * Refactored the Homebrew ingester (`brewIngester`) to use a new `fetchCask` helper, which reads from the local file if `cask_path` is set, or falls back to the API otherwise. Includes robust error handling. [[1]](diffhunk://#diff-abd7db4bef16a062c1bd81f54a7c846f1e91b913a9fe9f87976c8075f39b8cd2L99-R101) [[2]](diffhunk://#diff-abd7db4bef16a062c1bd81f54a7c846f1e91b913a9fe9f87976c8075f39b8cd2R200-R251) * Added comprehensive documentation and examples for the custom tap workflow, including a new `custom-tap/` directory with cask DSL sources, generated JSON, and a regeneration script. [[1]](diffhunk://#diff-2dfa2fc79b9becad555db38289a16afe4ce651665a31868d386fed8b4e160740R1-R85) [[2]](diffhunk://#diff-be469dd148f0c50ad56489c48bdb514522e1a46d21336e8f747b5880d71a6d1bR49-R66) * Added new custom casks for `fleet-desktop`, `druva-insync`, and `zoom-rooms` under `inputs/homebrew/custom-tap/Casks/`. [[1]](diffhunk://#diff-2555a54830de2bfb0ffca8bc487aac67de84dee5d431fe5f42e90e1754f63bb6R1-R36) [[2]](diffhunk://#diff-db1fa8a43a27c5adf49a5ade04e61405ce1e9420f266e3160156cabf69ed4ea8R1-R40) [[3]](diffhunk://#diff-effd461583140683d41dc68d9a93692d039be5ad5e52b6b108ece79f17155107R1-R44) **Testing and validation:** * Added a new test (`TestIngestCaskPath`) to ensure the ingester correctly reads from `cask_path` and does not make unnecessary HTTP requests, with error handling for missing files. **CI workflow improvements:** * Updated GitHub Actions workflows to handle Fleet Desktop's installer requirements in CI by creating a managed preferences stub when validating Fleet Desktop, ensuring the installer succeeds even without MDM enrollment. [[1]](diffhunk://#diff-28b30c8601cb7662d59efbfbbcf800cae91455fd3d875627659dced8c1257a24R100) [[2]](diffhunk://#diff-28b30c8601cb7662d59efbfbbcf800cae91455fd3d875627659dced8c1257a24R116-R123) [[3]](diffhunk://#diff-28b30c8601cb7662d59efbfbbcf800cae91455fd3d875627659dced8c1257a24R148-R172) [[4]](diffhunk://#diff-c263ffc3062c3b5e4e4eb65976080c6cbddac478a5fed3392fe8b23c49bb2da8R69-R92) These changes make it possible to maintain and test apps from custom Homebrew taps within the Fleet repo, improving flexibility and reliability for Fleet-maintained apps. ## Summary by CodeRabbit * **New Features** * Added support for three new macOS apps: Fleet Desktop, Druva inSync, and Zoom Rooms * Added UI icons for Fleet Desktop and Zoom Rooms * **Enhancements** * Fleet Desktop includes an MDM enrollment caveat and improved installer validation for macOS installers * Support for overriding Homebrew cask input via a local cask JSON file * **Tests** * Added unit coverage for local cask JSON ingestion behavior * **Chores** * Added a deterministic script to regenerate Homebrew custom-tap manifests --- .github/workflows/test-fma-darwin-pr-only.yml | 34 ++++++ .github/workflows/test-fma-darwin.yml | 24 ++++ ee/maintained-apps/README.md | 18 +++ .../ingesters/homebrew/ingester.go | 101 +++++++++++------ .../ingesters/homebrew/ingester_test.go | 103 ++++++++++++++++++ .../homebrew/custom-tap/Casks/druva-insync.rb | 40 +++++++ .../custom-tap/Casks/fleet-desktop.rb | 36 ++++++ .../homebrew/custom-tap/Casks/zoom-rooms.rb | 44 ++++++++ .../inputs/homebrew/custom-tap/README.md | 85 +++++++++++++++ .../homebrew/custom-tap/api/druva-insync.json | 88 +++++++++++++++ .../custom-tap/api/fleet-desktop.json | 77 +++++++++++++ .../homebrew/custom-tap/api/zoom-rooms.json | 90 +++++++++++++++ .../inputs/homebrew/custom-tap/regenerate.sh | 89 +++++++++++++++ .../inputs/homebrew/druva-insync.json | 9 ++ .../inputs/homebrew/fleet-desktop.json | 9 ++ .../inputs/homebrew/schema/input-schema.json | 6 + .../inputs/homebrew/zoom-rooms.json | 9 ++ ee/maintained-apps/outputs/apps.json | 21 ++++ .../outputs/druva-insync/darwin.json | 22 ++++ .../outputs/fleet-desktop/darwin.json | 22 ++++ .../outputs/zoom-rooms/darwin.json | 22 ++++ .../components/icons/FleetDesktop.tsx | 14 +++ .../components/icons/ZoomRooms.tsx | 14 +++ .../SoftwarePage/components/icons/index.ts | 4 + .../app-icon-fleet-desktop-60x60@2x.png | Bin 0 -> 4310 bytes .../images/app-icon-zoom-rooms-60x60@2x.png | Bin 0 -> 13406 bytes 26 files changed, 949 insertions(+), 32 deletions(-) create mode 100644 ee/maintained-apps/inputs/homebrew/custom-tap/Casks/druva-insync.rb create mode 100644 ee/maintained-apps/inputs/homebrew/custom-tap/Casks/fleet-desktop.rb create mode 100644 ee/maintained-apps/inputs/homebrew/custom-tap/Casks/zoom-rooms.rb create mode 100644 ee/maintained-apps/inputs/homebrew/custom-tap/README.md create mode 100644 ee/maintained-apps/inputs/homebrew/custom-tap/api/druva-insync.json create mode 100644 ee/maintained-apps/inputs/homebrew/custom-tap/api/fleet-desktop.json create mode 100644 ee/maintained-apps/inputs/homebrew/custom-tap/api/zoom-rooms.json create mode 100755 ee/maintained-apps/inputs/homebrew/custom-tap/regenerate.sh create mode 100644 ee/maintained-apps/inputs/homebrew/druva-insync.json create mode 100644 ee/maintained-apps/inputs/homebrew/fleet-desktop.json create mode 100644 ee/maintained-apps/inputs/homebrew/zoom-rooms.json create mode 100644 ee/maintained-apps/outputs/druva-insync/darwin.json create mode 100644 ee/maintained-apps/outputs/fleet-desktop/darwin.json create mode 100644 ee/maintained-apps/outputs/zoom-rooms/darwin.json create mode 100644 frontend/pages/SoftwarePage/components/icons/FleetDesktop.tsx create mode 100644 frontend/pages/SoftwarePage/components/icons/ZoomRooms.tsx create mode 100644 website/assets/images/app-icon-fleet-desktop-60x60@2x.png create mode 100644 website/assets/images/app-icon-zoom-rooms-60x60@2x.png diff --git a/.github/workflows/test-fma-darwin-pr-only.yml b/.github/workflows/test-fma-darwin-pr-only.yml index 677a3ce3fb..fa7bf8eaa5 100644 --- a/.github/workflows/test-fma-darwin-pr-only.yml +++ b/.github/workflows/test-fma-darwin-pr-only.yml @@ -97,6 +97,7 @@ jobs: if [ -z "$DARWIN_SLUGS" ]; then echo "has_darwin_apps=false" >> $GITHUB_OUTPUT echo "has_google_chrome=false" >> $GITHUB_OUTPUT + echo "has_fleet_desktop=false" >> $GITHUB_OUTPUT echo "No darwin apps changed, skipping Darwin workflow" else echo "has_darwin_apps=true" >> $GITHUB_OUTPUT @@ -112,6 +113,14 @@ jobs: else echo "has_google_chrome=false" >> $GITHUB_OUTPUT fi + + # Check if fleet-desktop/darwin is in the changed apps + if echo "$DARWIN_SLUGS" | grep -q "^fleet-desktop/darwin$"; then + echo "has_fleet_desktop=true" >> $GITHUB_OUTPUT + echo "Fleet Desktop detected in changed apps" + else + echo "has_fleet_desktop=false" >> $GITHUB_OUTPUT + fi fi shell: bash @@ -136,6 +145,31 @@ jobs: sudo rm -rf "$app" done + # Fleet Desktop's installer refuses to run unless the + # com.fleetdm.fleetd.config managed preferences profile is present + # (it's normally delivered via MDM). CI runners aren't MDM-enrolled, + # so we drop a stub plist in place before the validate step so the + # install script succeeds. This only runs when fleet-desktop/darwin + # is actually being validated in this PR. + - name: Create Fleet Desktop MDM config stub (CI-only) + if: steps.check-darwin-apps.outputs.has_darwin_apps == 'true' && steps.check-darwin-apps.outputs.has_fleet_desktop == 'true' + run: | + sudo mkdir -p "/Library/Managed Preferences" + sudo tee "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist" > /dev/null <<'PLIST' + + + + + EnrollSecret + ci-test-placeholder + FleetURL + https://ci.test.example.com + + + PLIST + sudo chmod 644 "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist" + ls -l "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist" + - name: Filter apps.json and verify changed apps if: steps.check-darwin-apps.outputs.has_darwin_apps == 'true' run: | diff --git a/.github/workflows/test-fma-darwin.yml b/.github/workflows/test-fma-darwin.yml index c4ec4989a6..f7a690e1a1 100644 --- a/.github/workflows/test-fma-darwin.yml +++ b/.github/workflows/test-fma-darwin.yml @@ -66,6 +66,30 @@ jobs: sudo rm -rf "$app" done + # Fleet Desktop's installer refuses to run unless the + # com.fleetdm.fleetd.config managed preferences profile is present + # (it's normally delivered via MDM). CI runners aren't MDM-enrolled, + # so we drop a stub plist in place before the validate step so the + # install script succeeds. This workflow validates every FMA every + # run, so fleet-desktop is always exercised. + - name: Create Fleet Desktop MDM config stub (CI-only) + run: | + sudo mkdir -p "/Library/Managed Preferences" + sudo tee "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist" > /dev/null <<'PLIST' + + + + + EnrollSecret + ci-test-placeholder + FleetURL + https://ci.test.example.com + + + PLIST + sudo chmod 644 "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist" + ls -l "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist" + - name: Verify Fleet Maintained Apps mac run: | ls /Applications diff --git a/ee/maintained-apps/README.md b/ee/maintained-apps/README.md index 5cdae701e9..20accfc238 100644 --- a/ee/maintained-apps/README.md +++ b/ee/maintained-apps/README.md @@ -46,6 +46,24 @@ | `post_uninstall_scripts` | string | Command lines run **after** the generated uninstall script (e.g., for [Box](inputs/homebrew/box-drive.json)). | | `install_script_path` | string | Filepath to a custom install script (`.sh`). Overrides the generated install script. Script must be placed in `inputs/homebrew/scripts/`. | | `uninstall_script_path` | string | Filepath to a custom uninstall script (`.sh`). Overrides the generated uninstall script. Cannot be used together with `pre_uninstall_scripts` or `post_uninstall_scripts`. Script must be placed in `inputs/homebrew/scripts/`. | +| `cask_path` | string | Path (relative to the repo root) to a local file containing the cask JSON in the same schema as `https://formulae.brew.sh/api/cask/.json`. Used to commit cask metadata for third-party taps directly into this repo under [`inputs/homebrew/custom-tap/`](inputs/homebrew/custom-tap/). See [Ingesting apps from a custom tap](#ingesting-apps-from-a-custom-tap) below. | + +### Ingesting apps from a custom tap + +Apps that live in a third-party Homebrew tap (not `Homebrew/homebrew-cask`) are not proxied by `https://formulae.brew.sh/api/`. To ingest them, commit both the `.rb` source and the generated `.json` into [`inputs/homebrew/custom-tap/`](inputs/homebrew/custom-tap/), laid out like a Homebrew tap: + +``` +custom-tap/ +├── Casks/.rb # Cask DSL source +├── api/.json # Generated with regenerate.sh +└── regenerate.sh # Rebuild api/*.json from Casks/*.rb +``` + +1. Write the cask DSL in `inputs/homebrew/custom-tap/Casks/.rb`. +2. Run `./regenerate.sh` from inside `custom-tap/` to produce `api/.json`. Requires macOS with Homebrew and `jq`. +3. In the app's input manifest (`inputs/homebrew/.json`), set `cask_path` to `ee/maintained-apps/inputs/homebrew/custom-tap/api/.json`. See `inputs/homebrew/fleet-desktop.json` for an example. + +See [`inputs/homebrew/custom-tap/README.md`](inputs/homebrew/custom-tap/README.md) for the full contributor flow. Apps without `cask_path` continue to be fetched from `formulae.brew.sh`. ## Adding a new app (Windows) diff --git a/ee/maintained-apps/ingesters/homebrew/ingester.go b/ee/maintained-apps/ingesters/homebrew/ingester.go index 38719a55c8..c53b614bed 100644 --- a/ee/maintained-apps/ingesters/homebrew/ingester.go +++ b/ee/maintained-apps/ingesters/homebrew/ingester.go @@ -96,39 +96,9 @@ type brewIngester struct { } func (i *brewIngester) ingestOne(ctx context.Context, input inputApp) (*maintained_apps.FMAManifestApp, error) { - apiURL := fmt.Sprintf("%scask/%s.json", i.baseURL, input.Token) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) + cask, err := i.fetchCask(ctx, input) if err != nil { - return nil, ctxerr.Wrap(ctx, err, "create http request") - } - - res, err := i.client.Do(req) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "execute http request") - } - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "read http response body") - } - - switch res.StatusCode { - case http.StatusOK: - // success, go on - case http.StatusNotFound: - return nil, ctxerr.New(ctx, "app not found in brew API") - default: - if len(body) > 512 { - body = body[:512] - } - return nil, ctxerr.Errorf(ctx, "brew API returned status %d: %s", res.StatusCode, string(body)) - } - - var cask brewCask - if err := json.Unmarshal(body, &cask); err != nil { - return nil, ctxerr.Wrapf(ctx, err, "unmarshal brew cask for %s", input.Token) + return nil, err } out := &maintained_apps.FMAManifestApp{} @@ -227,6 +197,66 @@ func (i *brewIngester) ingestOne(ctx context.Context, input inputApp) (*maintain return out, nil } +// fetchCask resolves the brew cask JSON for the given input app from +// either a local file (cask_path) or the default brew API. +func (i *brewIngester) fetchCask(ctx context.Context, input inputApp) (brewCask, error) { + var cask brewCask + + if input.CaskPath != "" { + body, err := os.ReadFile(input.CaskPath) + if err != nil { + return cask, ctxerr.WrapWithData(ctx, err, "reading local cask JSON file", map[string]any{"cask_path": input.CaskPath}) + } + if err := json.Unmarshal(body, &cask); err != nil { + return cask, ctxerr.Wrapf(ctx, err, "unmarshal local cask JSON for %s", input.Token) + } + // Cross-check the cask file matches the configured input. This catches + // subtle misconfiguration like pointing cask_path at the wrong JSON file. + if cask.Token != input.Token { + return cask, ctxerr.Errorf(ctx, "local cask JSON token %q does not match input token %q (cask_path: %s)", cask.Token, input.Token, input.CaskPath) + } + if len(cask.Name) == 0 { + return cask, ctxerr.Errorf(ctx, "local cask JSON for %s has empty name (cask_path: %s)", input.Token, input.CaskPath) + } + return cask, nil + } + + apiURL := fmt.Sprintf("%scask/%s.json", i.baseURL, input.Token) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) + if err != nil { + return cask, ctxerr.Wrap(ctx, err, "create http request") + } + + res, err := i.client.Do(req) + if err != nil { + return cask, ctxerr.Wrap(ctx, err, "execute http request") + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return cask, ctxerr.Wrap(ctx, err, "read http response body") + } + + switch res.StatusCode { + case http.StatusOK: + // success, go on + case http.StatusNotFound: + return cask, ctxerr.New(ctx, "app not found in brew API") + default: + if len(body) > 512 { + body = body[:512] + } + return cask, ctxerr.Errorf(ctx, "brew API returned status %d: %s", res.StatusCode, string(body)) + } + + if err := json.Unmarshal(body, &cask); err != nil { + return cask, ctxerr.Wrapf(ctx, err, "unmarshal brew cask for %s", input.Token) + } + return cask, nil +} + type inputApp struct { // Name is the user-friendly name of the app. Name string `json:"name"` @@ -245,6 +275,13 @@ type inputApp struct { InstallScriptPath string `json:"install_script_path"` UninstallScriptPath string `json:"uninstall_script_path"` PatchPolicyPath string `json:"patch_policy_path"` + // CaskPath optionally points at a local file (relative to the repo + // root) containing the cask JSON in the same schema as + // https://formulae.brew.sh/api/cask/.json. Used to commit cask + // metadata for third-party taps directly into this repo (see + // inputs/homebrew/custom-tap/). When empty, the ingester fetches from + // formulae.brew.sh. + CaskPath string `json:"cask_path"` } type brewCask struct { diff --git a/ee/maintained-apps/ingesters/homebrew/ingester_test.go b/ee/maintained-apps/ingesters/homebrew/ingester_test.go index f588ce1156..0cc52b41f0 100644 --- a/ee/maintained-apps/ingesters/homebrew/ingester_test.go +++ b/ee/maintained-apps/ingesters/homebrew/ingester_test.go @@ -157,3 +157,106 @@ func TestIngestValidations(t *testing.T) { }) } } + +// TestIngestCaskPath verifies that when an input app sets cask_path, the +// ingester reads cask JSON from that local file and makes no HTTP call. +// This is the path used for casks committed into inputs/homebrew/custom-tap/. +func TestIngestCaskPath(t *testing.T) { + tempDir := t.TempDir() + + caskJSON, err := json.Marshal(brewCask{ + Token: "local-cask", + Name: []string{"Local Cask"}, + URL: "https://example.com/local/installer.pkg", + Version: "9.9.9", + SHA256: "deadbeef", + }) + require.NoError(t, err) + + caskPath := path.Join(tempDir, "local-cask.json") + require.NoError(t, os.WriteFile(caskPath, caskJSON, 0o644)) + + // Server that should never be called when cask_path is set; any hit is a + // bug because it means the ingester fell back to HTTP. + var httpHits int + srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + httpHits++ + })) + t.Cleanup(srv.Close) + + ctx := context.Background() + i := &brewIngester{ + logger: slog.New(slog.DiscardHandler), + client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)), + baseURL: srv.URL + "/", + } + + out, err := i.ingestOne(ctx, inputApp{ + Token: "local-cask", + UniqueIdentifier: "com.example.localcask", + InstallerFormat: "pkg", + Name: "Local Cask", + CaskPath: caskPath, + }) + require.NoError(t, err) + require.Equal(t, "https://example.com/local/installer.pkg", out.InstallerURL) + require.Equal(t, "9.9.9", out.Version) + require.Equal(t, "deadbeef", out.SHA256) + require.Equal(t, 0, httpHits, "cask_path path must not make an HTTP call") + + // Missing file yields an actionable error. + _, err = i.ingestOne(ctx, inputApp{ + Token: "missing", + UniqueIdentifier: "com.example.missing", + InstallerFormat: "pkg", + Name: "Missing", + CaskPath: path.Join(tempDir, "does-not-exist.json"), + }) + require.ErrorContains(t, err, "reading local cask JSON file") + require.Equal(t, 0, httpHits) + + // Token mismatch between input and cask file is rejected so a misconfigured + // cask_path can't silently ingest the wrong app. + mismatchPath := path.Join(tempDir, "mismatch.json") + mismatchJSON, err := json.Marshal(brewCask{ + Token: "some-other-cask", + Name: []string{"Some Other Cask"}, + URL: "https://example.com/other/installer.pkg", + Version: "1.0.0", + SHA256: "cafebabe", + }) + require.NoError(t, err) + require.NoError(t, os.WriteFile(mismatchPath, mismatchJSON, 0o644)) + + _, err = i.ingestOne(ctx, inputApp{ + Token: "local-cask", + UniqueIdentifier: "com.example.localcask", + InstallerFormat: "pkg", + Name: "Local Cask", + CaskPath: mismatchPath, + }) + require.ErrorContains(t, err, "does not match input token") + require.Equal(t, 0, httpHits) + + // Cask file with an empty name is rejected. + emptyNamePath := path.Join(tempDir, "empty-name.json") + emptyNameJSON, err := json.Marshal(brewCask{ + Token: "local-cask", + Name: []string{}, + URL: "https://example.com/local/installer.pkg", + Version: "9.9.9", + SHA256: "deadbeef", + }) + require.NoError(t, err) + require.NoError(t, os.WriteFile(emptyNamePath, emptyNameJSON, 0o644)) + + _, err = i.ingestOne(ctx, inputApp{ + Token: "local-cask", + UniqueIdentifier: "com.example.localcask", + InstallerFormat: "pkg", + Name: "Local Cask", + CaskPath: emptyNamePath, + }) + require.ErrorContains(t, err, "empty name") + require.Equal(t, 0, httpHits) +} diff --git a/ee/maintained-apps/inputs/homebrew/custom-tap/Casks/druva-insync.rb b/ee/maintained-apps/inputs/homebrew/custom-tap/Casks/druva-insync.rb new file mode 100644 index 0000000000..ebe6fac26f --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/custom-tap/Casks/druva-insync.rb @@ -0,0 +1,40 @@ +cask "druva-insync" do + version "7.6.1,110931" + sha256 "a67784b4d6789e9a671e2d77789c408116b10c89c6c8893c3f08ed6212684bf2" + + url "https://downloads.druva.com/downloads/inSync/MAC/#{version.csv.first}/inSync-#{version.csv.first}-r#{version.csv.second}.dmg" + name "Druva inSync" + desc "Endpoint data backup and recovery client" + homepage "https://www.druva.com/" + + livecheck do + skip "Druva does not expose a parseable version feed; bump manually" + end + + depends_on macos: ">= :big_sur" + + pkg "Install inSync.pkg" + + uninstall launchctl: [ + "com.druva.inSyncAgent", + "com.druva.inSyncDecom", + "com.druva.inSyncUpgrade", + "com.druva.inSyncUpgradeDaemon", + ], + quit: "com.druva.inSyncClient", + pkgutil: "com.druva.inSync.pkg", + delete: [ + "/Library/LaunchAgents/inSyncAgent.plist", + "/Library/LaunchAgents/inSyncUpgrade.plist", + "/Library/LaunchDaemons/inSyncDecommission.plist", + "/Library/LaunchDaemons/inSyncUpgradeDaemon.plist", + ] + + zap trash: [ + "~/Library/Application Support/Druva", + "~/Library/Caches/com.druva.inSyncClient", + "~/Library/Logs/Druva", + "~/Library/Preferences/com.druva.inSyncClient.plist", + "~/Library/Saved Application State/com.druva.inSyncClient.savedState", + ] +end diff --git a/ee/maintained-apps/inputs/homebrew/custom-tap/Casks/fleet-desktop.rb b/ee/maintained-apps/inputs/homebrew/custom-tap/Casks/fleet-desktop.rb new file mode 100644 index 0000000000..b9925c90bc --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/custom-tap/Casks/fleet-desktop.rb @@ -0,0 +1,36 @@ +cask "fleet-desktop" do + version "1.1.0" + sha256 "4f3537c37a094f333046072262b1f37729f73074bf935f09f05799bc341fef58" + + url "https://github.com/allenhouchins/fleet-desktop/releases/download/v#{version}/fleet_desktop-v#{version}.pkg" + name "Fleet Desktop" + desc "End-user client for Fleet device management" + homepage "https://github.com/allenhouchins/fleet-desktop" + + livecheck do + url :url + strategy :github_latest + end + + depends_on macos: ">= :ventura" + + pkg "fleet_desktop-v#{version}.pkg" + + uninstall quit: "com.fleetdm.fleet-desktop", + pkgutil: "com.fleetdm.fleet-desktop" + + zap trash: [ + "~/Library/Caches/com.fleetdm.fleet-desktop", + "~/Library/HTTPStorages/com.fleetdm.fleet-desktop", + "~/Library/HTTPStorages/com.fleetdm.fleet-desktop.binarycookies", + "~/Library/Preferences/com.fleetdm.fleet-desktop.plist", + "~/Library/Saved Application State/com.fleetdm.fleet-desktop.savedState", + "~/Library/WebKit/com.fleetdm.fleet-desktop", + ] + + caveats <<~EOS + Fleet Desktop requires the Mac to be enrolled in MDM with the + com.fleetdm.fleetd.config managed preferences profile. The installer + will fail with "Installation Failed" otherwise. + EOS +end diff --git a/ee/maintained-apps/inputs/homebrew/custom-tap/Casks/zoom-rooms.rb b/ee/maintained-apps/inputs/homebrew/custom-tap/Casks/zoom-rooms.rb new file mode 100644 index 0000000000..88fd3a0156 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/custom-tap/Casks/zoom-rooms.rb @@ -0,0 +1,44 @@ +cask "zoom-rooms" do + version "7.0.0.12322" + sha256 "c35229e6066732aec7f26b762d0f2d43ab7b8270e512fbabe5bbb77a9c714bbc" + + url "https://cdn.zoom.us/prod/#{version}/ZoomRooms.pkg" + name "Zoom Rooms" + desc "Conference room software for Zoom meetings" + homepage "https://www.zoom.com/en/products/zoom-rooms/" + + livecheck do + skip "Zoom does not expose a parseable Zoom Rooms version feed; bump manually" + end + + depends_on macos: ">= :catalina" + + pkg "ZoomRooms.pkg" + + # The product is branded "Zoom Rooms" but the installer drops the app at + # /Applications/ZoomPresence.app with the legacy bundle id us.zoom.ZoomPresence. + uninstall launchctl: [ + "us.zoom.rooms.daemon", + "us.zoom.rooms.tool", + ], + quit: "us.zoom.ZoomPresence", + pkgutil: "us.zoom.pkg.zp", + delete: [ + "/Applications/ZoomPresence.app", + "/Library/LaunchDaemons/us.zoom.rooms.daemon.plist", + "/Library/LaunchDaemons/us.zoom.rooms.tool.plist", + "/Library/PrivilegedHelperTools/us.zoom.ZoomRoomsDaemon", + "/Library/Logs/us.zoom.ZoomRoomUpdateRecord", + "/Library/Logs/zpinstall.log", + ] + + zap trash: [ + "~/Library/Application Support/ZoomPresence", + "~/Library/Caches/us.zoom.ZoomPresence", + "~/Library/HTTPStorages/us.zoom.ZoomPresence", + "~/Library/HTTPStorages/us.zoom.ZoomPresence.binarycookies", + "~/Library/Preferences/us.zoom.ZoomPresence.plist", + "~/Library/Saved Application State/us.zoom.ZoomPresence.savedState", + "~/Library/WebKit/us.zoom.ZoomPresence", + ] +end diff --git a/ee/maintained-apps/inputs/homebrew/custom-tap/README.md b/ee/maintained-apps/inputs/homebrew/custom-tap/README.md new file mode 100644 index 0000000000..64ffd4b666 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/custom-tap/README.md @@ -0,0 +1,85 @@ +# Fleet custom-tap casks + +This directory is a **self-contained source of truth** for Fleet-maintained apps that +don't exist in `Homebrew/homebrew-cask` and therefore can't be ingested from +`https://formulae.brew.sh/api/`. + +It is laid out like a Homebrew tap: + +``` +custom-tap/ +├── Casks/ # Cask DSL sources (.rb). Edit these. +│ ├── fleet-desktop.rb +│ └── druva-insync.rb +├── api/ # Generated cask metadata (.json). Do not hand-edit. +│ ├── fleet-desktop.json +│ └── druva-insync.json +├── regenerate.sh # Regenerates api/*.json from Casks/*.rb. +└── README.md # You are here. +``` + +It is **not** a real Homebrew tap — the Fleet repo isn't named +`homebrew-` and `Casks/` isn't at the repo root, so `brew tap` / +`brew install` against it won't work. It exists solely to feed Fleet's FMA +ingester, and keeping both the source (`.rb`) and the built artifact (`.json`) +in the same repo means the PR that changes a cask also tests the change in +CI. + +## How it hooks into the FMA ingester + +Each app here has an input manifest one directory up +(`../.json`) with a `cask_path` field pointing at the generated JSON. +Example — [`../fleet-desktop.json`](../fleet-desktop.json): + +```json +{ + "name": "Fleet Desktop", + "token": "fleet-desktop", + "cask_path": "ee/maintained-apps/inputs/homebrew/custom-tap/api/fleet-desktop.json", + ... +} +``` + +When `go run cmd/maintained-apps/main.go` runs, the ingester sees `cask_path`, +reads the local file, and skips the brew API entirely. Apps without +`cask_path` continue to use `https://formulae.brew.sh/api/` as before. + +## Adding a new cask + +1. Write the cask DSL in `Casks/.rb`. Use an existing file or + as a reference. +2. Run `./regenerate.sh` in this directory to produce `api/.json`. +3. Create an input manifest at + `ee/maintained-apps/inputs/homebrew/.json` that points + `cask_path` at the new JSON. Follow the template in + `../fleet-desktop.json`. +4. Generate the FMA output manifest: + `go run cmd/maintained-apps/main.go --slug="/darwin"` from the repo + root. +5. Follow the rest of the FMA contributor flow in + [`../../../README.md`](../../../README.md) (apps.json description, icon, + PR). + +## Updating an existing cask + +1. Edit the stanza you care about in `Casks/.rb`. +2. Run `./regenerate.sh` to refresh `api/.json`. +3. Regenerate the FMA output manifest: + `go run cmd/maintained-apps/main.go --slug="/darwin"`. +4. Commit all three changes together: the `.rb`, the `.json`, and the + `outputs//darwin.json`. + +## Why `regenerate.sh` strips fields + +`brew info --cask --json=v2` includes several fields that depend on the +developer's machine or the throwaway tap the script uses internally — +`installed`, `installed_time`, `outdated`, `full_token`, `tap`, +`tap_git_head`, `generated_date`. None of these are read by the FMA +ingester, so the script strips them to keep committed JSON stable across +machines. + +## Requirements + +- macOS (the `.rb` DSL is parsed by Homebrew). +- Homebrew installed (`brew` on PATH). +- `jq` (`brew install jq`). diff --git a/ee/maintained-apps/inputs/homebrew/custom-tap/api/druva-insync.json b/ee/maintained-apps/inputs/homebrew/custom-tap/api/druva-insync.json new file mode 100644 index 0000000000..299dda85e5 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/custom-tap/api/druva-insync.json @@ -0,0 +1,88 @@ +{ + "token": "druva-insync", + "old_tokens": [], + "name": [ + "Druva inSync" + ], + "desc": "Endpoint data backup and recovery client", + "homepage": "https://www.druva.com/", + "url": "https://downloads.druva.com/downloads/inSync/MAC/7.6.1/inSync-7.6.1-r110931.dmg", + "url_specs": {}, + "version": "7.6.1,110931", + "autobump": true, + "no_autobump_message": null, + "skip_livecheck": true, + "bundle_version": null, + "bundle_short_version": null, + "sha256": "a67784b4d6789e9a671e2d77789c408116b10c89c6c8893c3f08ed6212684bf2", + "artifacts": [ + { + "uninstall": [ + { + "launchctl": [ + "com.druva.inSyncAgent", + "com.druva.inSyncDecom", + "com.druva.inSyncUpgrade", + "com.druva.inSyncUpgradeDaemon" + ], + "quit": "com.druva.inSyncClient", + "pkgutil": "com.druva.inSync.pkg", + "delete": [ + "/Library/LaunchAgents/inSyncAgent.plist", + "/Library/LaunchAgents/inSyncUpgrade.plist", + "/Library/LaunchDaemons/inSyncDecommission.plist", + "/Library/LaunchDaemons/inSyncUpgradeDaemon.plist" + ] + } + ] + }, + { + "pkg": [ + "Install inSync.pkg" + ] + }, + { + "zap": [ + { + "trash": [ + "~/Library/Application Support/Druva", + "~/Library/Caches/com.druva.inSyncClient", + "~/Library/Logs/Druva", + "~/Library/Preferences/com.druva.inSyncClient.plist", + "~/Library/Saved Application State/com.druva.inSyncClient.savedState" + ] + } + ] + } + ], + "caveats": null, + "caveats_rosetta": null, + "depends_on": { + "macos": { + ">=": [ + "11" + ] + } + }, + "conflicts_with": null, + "container": null, + "rename": [], + "auto_updates": null, + "deprecated": false, + "deprecation_date": null, + "deprecation_reason": null, + "deprecation_replacement_formula": null, + "deprecation_replacement_cask": null, + "deprecate_args": null, + "disabled": false, + "disable_date": null, + "disable_reason": null, + "disable_replacement_formula": null, + "disable_replacement_cask": null, + "disable_args": null, + "languages": [], + "ruby_source_path": "Casks/druva-insync.rb", + "ruby_source_checksum": { + "sha256": "9443d1939f90512b0c4dc645ef19176e6c44c5bd0119043c6bc38657f9416791" + } +} diff --git a/ee/maintained-apps/inputs/homebrew/custom-tap/api/fleet-desktop.json b/ee/maintained-apps/inputs/homebrew/custom-tap/api/fleet-desktop.json new file mode 100644 index 0000000000..6ebeac51a8 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/custom-tap/api/fleet-desktop.json @@ -0,0 +1,77 @@ +{ + "token": "fleet-desktop", + "old_tokens": [], + "name": [ + "Fleet Desktop" + ], + "desc": "End-user client for Fleet device management", + "homepage": "https://github.com/allenhouchins/fleet-desktop", + "url": "https://github.com/allenhouchins/fleet-desktop/releases/download/v1.1.0/fleet_desktop-v1.1.0.pkg", + "url_specs": {}, + "version": "1.1.0", + "autobump": true, + "no_autobump_message": null, + "skip_livecheck": false, + "bundle_version": null, + "bundle_short_version": null, + "sha256": "4f3537c37a094f333046072262b1f37729f73074bf935f09f05799bc341fef58", + "artifacts": [ + { + "uninstall": [ + { + "quit": "com.fleetdm.fleet-desktop", + "pkgutil": "com.fleetdm.fleet-desktop" + } + ] + }, + { + "pkg": [ + "fleet_desktop-v1.1.0.pkg" + ] + }, + { + "zap": [ + { + "trash": [ + "~/Library/Caches/com.fleetdm.fleet-desktop", + "~/Library/HTTPStorages/com.fleetdm.fleet-desktop", + "~/Library/HTTPStorages/com.fleetdm.fleet-desktop.binarycookies", + "~/Library/Preferences/com.fleetdm.fleet-desktop.plist", + "~/Library/Saved Application State/com.fleetdm.fleet-desktop.savedState", + "~/Library/WebKit/com.fleetdm.fleet-desktop" + ] + } + ] + } + ], + "caveats": "Fleet Desktop requires the Mac to be enrolled in MDM with the\ncom.fleetdm.fleetd.config managed preferences profile. The installer\nwill fail with \"Installation Failed\" otherwise.\n", + "caveats_rosetta": null, + "depends_on": { + "macos": { + ">=": [ + "13" + ] + } + }, + "conflicts_with": null, + "container": null, + "rename": [], + "auto_updates": null, + "deprecated": false, + "deprecation_date": null, + "deprecation_reason": null, + "deprecation_replacement_formula": null, + "deprecation_replacement_cask": null, + "deprecate_args": null, + "disabled": false, + "disable_date": null, + "disable_reason": null, + "disable_replacement_formula": null, + "disable_replacement_cask": null, + "disable_args": null, + "languages": [], + "ruby_source_path": "Casks/fleet-desktop.rb", + "ruby_source_checksum": { + "sha256": "a348b5c812ce3ee1c1ee361c273ce39725feffb557d48bbda2f42648c1b2e6c5" + } +} diff --git a/ee/maintained-apps/inputs/homebrew/custom-tap/api/zoom-rooms.json b/ee/maintained-apps/inputs/homebrew/custom-tap/api/zoom-rooms.json new file mode 100644 index 0000000000..c04656a706 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/custom-tap/api/zoom-rooms.json @@ -0,0 +1,90 @@ +{ + "token": "zoom-rooms", + "old_tokens": [], + "name": [ + "Zoom Rooms" + ], + "desc": "Conference room software for Zoom meetings", + "homepage": "https://www.zoom.com/en/products/zoom-rooms/", + "url": "https://cdn.zoom.us/prod/7.0.0.12322/ZoomRooms.pkg", + "url_specs": {}, + "version": "7.0.0.12322", + "autobump": true, + "no_autobump_message": null, + "skip_livecheck": true, + "bundle_version": null, + "bundle_short_version": null, + "sha256": "c35229e6066732aec7f26b762d0f2d43ab7b8270e512fbabe5bbb77a9c714bbc", + "artifacts": [ + { + "uninstall": [ + { + "launchctl": [ + "us.zoom.rooms.daemon", + "us.zoom.rooms.tool" + ], + "quit": "us.zoom.ZoomPresence", + "pkgutil": "us.zoom.pkg.zp", + "delete": [ + "/Applications/ZoomPresence.app", + "/Library/LaunchDaemons/us.zoom.rooms.daemon.plist", + "/Library/LaunchDaemons/us.zoom.rooms.tool.plist", + "/Library/PrivilegedHelperTools/us.zoom.ZoomRoomsDaemon", + "/Library/Logs/us.zoom.ZoomRoomUpdateRecord", + "/Library/Logs/zpinstall.log" + ] + } + ] + }, + { + "pkg": [ + "ZoomRooms.pkg" + ] + }, + { + "zap": [ + { + "trash": [ + "~/Library/Application Support/ZoomPresence", + "~/Library/Caches/us.zoom.ZoomPresence", + "~/Library/HTTPStorages/us.zoom.ZoomPresence", + "~/Library/HTTPStorages/us.zoom.ZoomPresence.binarycookies", + "~/Library/Preferences/us.zoom.ZoomPresence.plist", + "~/Library/Saved Application State/us.zoom.ZoomPresence.savedState", + "~/Library/WebKit/us.zoom.ZoomPresence" + ] + } + ] + } + ], + "caveats": null, + "caveats_rosetta": null, + "depends_on": { + "macos": { + ">=": [ + "10.15" + ] + } + }, + "conflicts_with": null, + "container": null, + "rename": [], + "auto_updates": null, + "deprecated": false, + "deprecation_date": null, + "deprecation_reason": null, + "deprecation_replacement_formula": null, + "deprecation_replacement_cask": null, + "deprecate_args": null, + "disabled": false, + "disable_date": null, + "disable_reason": null, + "disable_replacement_formula": null, + "disable_replacement_cask": null, + "disable_args": null, + "languages": [], + "ruby_source_path": "Casks/zoom-rooms.rb", + "ruby_source_checksum": { + "sha256": "5a428ab919a9c0da0d54f4868ef4ce49693f83a778de7cdd508f063ef770514f" + } +} diff --git a/ee/maintained-apps/inputs/homebrew/custom-tap/regenerate.sh b/ee/maintained-apps/inputs/homebrew/custom-tap/regenerate.sh new file mode 100755 index 0000000000..a7915ee73d --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/custom-tap/regenerate.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# +# regenerate.sh +# +# Rebuild every api/.json from its Casks/.rb source using +# Homebrew. Run this whenever you edit or add a cask under Casks/, then +# commit both the updated .rb and the regenerated .json alongside the +# Fleet-maintained-app output manifest (produced separately by +# `go run cmd/maintained-apps/main.go --slug=`). +# +# Why a throwaway local tap? `brew info --cask --json=v2` can only parse +# casks reachable through a tap; it won't parse a loose .rb file path. +# So the script drops the Casks/*.rb into a private tap, runs brew info, +# extracts the single cask object, and tears the tap down on exit. +# +# Fields that vary between developer machines (install state, tap +# identity, build timestamps) are stripped so the committed JSON is +# deterministic. +# +# Requirements: macOS with Homebrew and jq installed. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +command -v brew >/dev/null 2>&1 || { + echo "error: brew is required; install from https://brew.sh" >&2 + exit 1 +} +command -v jq >/dev/null 2>&1 || { + echo "error: jq is required; install with 'brew install jq'" >&2 + exit 1 +} + +if [ ! -d Casks ]; then + echo "error: expected a Casks/ directory next to this script" >&2 + exit 1 +fi + +shopt -s nullglob +rb_files=(Casks/*.rb) +if [ ${#rb_files[@]} -eq 0 ]; then + echo "error: no .rb files found under Casks/" >&2 + exit 1 +fi + +TAP_USER="fleetdm" +TAP_NAME="fma-custom-tap" +TAP_DIR="$(brew --repository)/Library/Taps/${TAP_USER}/homebrew-${TAP_NAME}" + +cleanup() { + rm -rf "$TAP_DIR" +} +trap cleanup EXIT + +rm -rf "$TAP_DIR" +mkdir -p "$TAP_DIR/Casks" +cp Casks/*.rb "$TAP_DIR/Casks/" + +( + cd "$TAP_DIR" + git init -q + git add -A + git -c user.email=fma@local -c user.name=fma commit -q -m "local regenerate" +) >/dev/null + +mkdir -p api + +# Fields stripped from brew's output because they vary by developer +# machine and are not read by Fleet's FMA ingester: +# installed, installed_time, outdated — install state on this host +# tap, tap_git_head, full_token — tap identity (throwaway tap) +# generated_date — build timestamp +STRIP='del(.installed, .installed_time, .outdated, .tap, .tap_git_head, .full_token, .generated_date)' + +for rb in "${rb_files[@]}"; do + token="$(basename "$rb" .rb)" + out="api/${token}.json" + echo "Regenerating ${out} from ${rb}..." + brew info --cask --json=v2 "${TAP_USER}/${TAP_NAME}/${token}" \ + | jq ".casks[0] | ${STRIP}" \ + > "$out" +done + +echo +echo "Done. If api/*.json changed, also regenerate the FMA output manifests:" +echo " go run cmd/maintained-apps/main.go --slug=/darwin" +echo "and commit everything together." diff --git a/ee/maintained-apps/inputs/homebrew/druva-insync.json b/ee/maintained-apps/inputs/homebrew/druva-insync.json new file mode 100644 index 0000000000..492faa0c76 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/druva-insync.json @@ -0,0 +1,9 @@ +{ + "name": "Druva inSync", + "unique_identifier": "com.druva.inSyncClient", + "token": "druva-insync", + "installer_format": "dmg", + "slug": "druva-insync/darwin", + "default_categories": ["Productivity"], + "cask_path": "ee/maintained-apps/inputs/homebrew/custom-tap/api/druva-insync.json" +} diff --git a/ee/maintained-apps/inputs/homebrew/fleet-desktop.json b/ee/maintained-apps/inputs/homebrew/fleet-desktop.json new file mode 100644 index 0000000000..170998e8eb --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/fleet-desktop.json @@ -0,0 +1,9 @@ +{ + "name": "Fleet Desktop", + "unique_identifier": "com.fleetdm.fleet-desktop", + "token": "fleet-desktop", + "installer_format": "pkg", + "slug": "fleet-desktop/darwin", + "default_categories": ["Productivity"], + "cask_path": "ee/maintained-apps/inputs/homebrew/custom-tap/api/fleet-desktop.json" +} diff --git a/ee/maintained-apps/inputs/homebrew/schema/input-schema.json b/ee/maintained-apps/inputs/homebrew/schema/input-schema.json index 57e55988c8..f8f11f1fd4 100644 --- a/ee/maintained-apps/inputs/homebrew/schema/input-schema.json +++ b/ee/maintained-apps/inputs/homebrew/schema/input-schema.json @@ -79,6 +79,12 @@ "frozen": { "type": "boolean", "description": "If true, the app will not be processed during ingestion and no new output will be created." + }, + "cask_path": { + "type": "string", + "description": "Path (relative to the repo root) to a local file containing the cask JSON in the same schema as https://formulae.brew.sh/api/cask/.json. Used to commit cask metadata for third-party taps directly into this repo under inputs/homebrew/custom-tap/.", + "minLength": 1, + "pattern": "\\.json$" } }, "not": { diff --git a/ee/maintained-apps/inputs/homebrew/zoom-rooms.json b/ee/maintained-apps/inputs/homebrew/zoom-rooms.json new file mode 100644 index 0000000000..c801c9bbe7 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/zoom-rooms.json @@ -0,0 +1,9 @@ +{ + "name": "Zoom Rooms", + "unique_identifier": "us.zoom.ZoomPresence", + "token": "zoom-rooms", + "installer_format": "pkg", + "slug": "zoom-rooms/darwin", + "default_categories": ["Communication"], + "cask_path": "ee/maintained-apps/inputs/homebrew/custom-tap/api/zoom-rooms.json" +} diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index 656d81f639..2ef94923ac 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -631,6 +631,13 @@ "unique_identifier": "com.getdropbox.dropbox", "description": "Dropbox is a client for the Dropbox cloud storage service." }, + { + "name": "Druva inSync", + "slug": "druva-insync/darwin", + "platform": "darwin", + "unique_identifier": "com.druva.inSyncClient", + "description": "Druva inSync is a cloud-based product that protects endpoint data." + }, { "name": "Druva inSync", "slug": "druva-insync/windows", @@ -729,6 +736,13 @@ "unique_identifier": "Mozilla Firefox 140.7.1 ESR (x64 en-US)", "description": "Mozilla Firefox ESR is the Extended Support Release version of the popular web browser Firefox." }, + { + "name": "Fleet Desktop", + "slug": "fleet-desktop/darwin", + "platform": "darwin", + "unique_identifier": "com.fleetdm.fleet-desktop", + "description": "Fleet Desktop is a native macOS application that provides end users with a self-service portal for Fleet." + }, { "name": "Fork", "slug": "fork/darwin", @@ -1975,6 +1989,13 @@ "unique_identifier": "io.zeplin.osx", "description": "Zeplin is an app to share, organize, and collaborate on designs." }, + { + "name": "Zoom Rooms", + "slug": "zoom-rooms/darwin", + "platform": "darwin", + "unique_identifier": "us.zoom.ZoomPresence", + "description": "Zoom Rooms is conference room software for Zoom meetings." + }, { "name": "Zoom", "slug": "zoom/darwin", diff --git a/ee/maintained-apps/outputs/druva-insync/darwin.json b/ee/maintained-apps/outputs/druva-insync/darwin.json new file mode 100644 index 0000000000..403e969288 --- /dev/null +++ b/ee/maintained-apps/outputs/druva-insync/darwin.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "7.6.1", + "queries": { + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.druva.inSyncClient';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.druva.inSyncClient' AND version_compare(bundle_short_version, '7.6.1') < 0);" + }, + "installer_url": "https://downloads.druva.com/downloads/inSync/MAC/7.6.1/inSync-7.6.1-r110931.dmg", + "install_script_ref": "28323cfd", + "uninstall_script_ref": "f2a09666", + "sha256": "a67784b4d6789e9a671e2d77789c408116b10c89c6c8893c3f08ed6212684bf2", + "default_categories": [ + "Productivity" + ] + } + ], + "refs": { + "28323cfd": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath $INSTALLER_PATH)\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Try to launch the application\n if osascript -e \"tell application id \\\"$bundle_id\\\" to activate\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# install pkg files\nquit_and_track_application 'com.druva.inSyncClient'\nsudo installer -pkg \"$TMPDIR/Install inSync.pkg\" -target /\nrelaunch_application 'com.druva.inSyncClient'\n", + "f2a09666": "#!/bin/sh\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/volume<\\/key>/ {getline; gsub(/.*|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/install-location<\\/key>/ {getline; gsub(/.*|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.druva.inSyncAgent'\nremove_launchctl_service 'com.druva.inSyncDecom'\nremove_launchctl_service 'com.druva.inSyncUpgrade'\nremove_launchctl_service 'com.druva.inSyncUpgradeDaemon'\nquit_application 'com.druva.inSyncClient'\nremove_pkg_files 'com.druva.inSync.pkg'\nforget_pkg 'com.druva.inSync.pkg'\nsudo rm -rf '/Library/LaunchAgents/inSyncAgent.plist'\nsudo rm -rf '/Library/LaunchAgents/inSyncUpgrade.plist'\nsudo rm -rf '/Library/LaunchDaemons/inSyncDecommission.plist'\nsudo rm -rf '/Library/LaunchDaemons/inSyncUpgradeDaemon.plist'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Druva'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.druva.inSyncClient'\ntrash $LOGGED_IN_USER '~/Library/Logs/Druva'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.druva.inSyncClient.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.druva.inSyncClient.savedState'\n" + } +} diff --git a/ee/maintained-apps/outputs/fleet-desktop/darwin.json b/ee/maintained-apps/outputs/fleet-desktop/darwin.json new file mode 100644 index 0000000000..522133a9f7 --- /dev/null +++ b/ee/maintained-apps/outputs/fleet-desktop/darwin.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "1.1.0", + "queries": { + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.fleetdm.fleet-desktop';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.fleetdm.fleet-desktop' AND version_compare(bundle_short_version, '1.1.0') < 0);" + }, + "installer_url": "https://github.com/allenhouchins/fleet-desktop/releases/download/v1.1.0/fleet_desktop-v1.1.0.pkg", + "install_script_ref": "b2592bcc", + "uninstall_script_ref": "0c4d343a", + "sha256": "4f3537c37a094f333046072262b1f37729f73074bf935f09f05799bc341fef58", + "default_categories": [ + "Productivity" + ] + } + ], + "refs": { + "0c4d343a": "#!/bin/sh\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/volume<\\/key>/ {getline; gsub(/.*|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/install-location<\\/key>/ {getline; gsub(/.*|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nquit_application 'com.fleetdm.fleet-desktop'\nremove_pkg_files 'com.fleetdm.fleet-desktop'\nforget_pkg 'com.fleetdm.fleet-desktop'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.fleetdm.fleet-desktop'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.fleetdm.fleet-desktop'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.fleetdm.fleet-desktop.binarycookies'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.fleetdm.fleet-desktop.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.fleetdm.fleet-desktop.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.fleetdm.fleet-desktop'\n", + "b2592bcc": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath $INSTALLER_PATH)\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Try to launch the application\n if osascript -e \"tell application id \\\"$bundle_id\\\" to activate\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# install pkg files\nquit_and_track_application 'com.fleetdm.fleet-desktop'\nsudo installer -pkg \"$TMPDIR/fleet_desktop-v1.1.0.pkg\" -target /\nrelaunch_application 'com.fleetdm.fleet-desktop'\n" + } +} diff --git a/ee/maintained-apps/outputs/zoom-rooms/darwin.json b/ee/maintained-apps/outputs/zoom-rooms/darwin.json new file mode 100644 index 0000000000..e3ba41b68e --- /dev/null +++ b/ee/maintained-apps/outputs/zoom-rooms/darwin.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "7.0.0.12322", + "queries": { + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'us.zoom.ZoomPresence';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'us.zoom.ZoomPresence' AND version_compare(bundle_short_version, '7.0.0.12322') < 0);" + }, + "installer_url": "https://cdn.zoom.us/prod/7.0.0.12322/ZoomRooms.pkg", + "install_script_ref": "dc069c1a", + "uninstall_script_ref": "e2401991", + "sha256": "c35229e6066732aec7f26b762d0f2d43ab7b8270e512fbabe5bbb77a9c714bbc", + "default_categories": [ + "Communication" + ] + } + ], + "refs": { + "dc069c1a": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath $INSTALLER_PATH)\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# install pkg files\nquit_and_track_application 'us.zoom.ZoomPresence'\nsudo installer -pkg \"$TMPDIR/ZoomRooms.pkg\" -target /\nrelaunch_application 'us.zoom.ZoomPresence'\n", + "e2401991": "#!/bin/sh\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/volume<\\/key>/ {getline; gsub(/.*|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/install-location<\\/key>/ {getline; gsub(/.*|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'us.zoom.rooms.daemon'\nremove_launchctl_service 'us.zoom.rooms.tool'\nquit_application 'us.zoom.ZoomPresence'\nremove_pkg_files 'us.zoom.pkg.zp'\nforget_pkg 'us.zoom.pkg.zp'\nsudo rm -rf '/Applications/ZoomPresence.app'\nsudo rm -rf '/Library/LaunchDaemons/us.zoom.rooms.daemon.plist'\nsudo rm -rf '/Library/LaunchDaemons/us.zoom.rooms.tool.plist'\nsudo rm -rf '/Library/PrivilegedHelperTools/us.zoom.ZoomRoomsDaemon'\nsudo rm -rf '/Library/Logs/us.zoom.ZoomRoomUpdateRecord'\nsudo rm -rf '/Library/Logs/zpinstall.log'\ntrash $LOGGED_IN_USER '~/Library/Application Support/ZoomPresence'\ntrash $LOGGED_IN_USER '~/Library/Caches/us.zoom.ZoomPresence'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/us.zoom.ZoomPresence'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/us.zoom.ZoomPresence.binarycookies'\ntrash $LOGGED_IN_USER '~/Library/Preferences/us.zoom.ZoomPresence.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/us.zoom.ZoomPresence.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/us.zoom.ZoomPresence'\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/FleetDesktop.tsx b/frontend/pages/SoftwarePage/components/icons/FleetDesktop.tsx new file mode 100644 index 0000000000..564e94a447 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/FleetDesktop.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const FleetDesktop = (props: SVGProps) => ( + + + +); +export default FleetDesktop; diff --git a/frontend/pages/SoftwarePage/components/icons/ZoomRooms.tsx b/frontend/pages/SoftwarePage/components/icons/ZoomRooms.tsx new file mode 100644 index 0000000000..5f3468a779 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/ZoomRooms.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const ZoomRooms = (props: SVGProps) => ( + + + +); +export default ZoomRooms; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index 7ce0c6a4e0..e04f11a6b1 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -11,6 +11,7 @@ import Charles from "./Charles"; import ConnectFonts from "./ConnectFonts"; import CrashPlan from "./CrashPlan"; import DruvaInSync from "./DruvaInSync"; +import FleetDesktop from "./FleetDesktop"; import Gemini from "./Gemini"; import GoogleCredentialProviderForWindows from "./GoogleCredentialProviderForWindows"; import Iina from "./Iina"; @@ -251,6 +252,7 @@ import Zen from "./Zen"; import Zeplin from "./Zeplin"; import ZeroOneZeroEditor from "./010Editor"; import Zoom from "./Zoom"; +import ZoomRooms from "./ZoomRooms"; import Zotero from "./Zotero"; // SOFTWARE_NAME_TO_ICON_MAP list "special" applications that have a defined @@ -317,6 +319,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { "company portal": IntuneCompanyPortal, "connect fonts": ConnectFonts, crashplan: CrashPlan, + "fleet desktop": FleetDesktop, gemini: Gemini, "google credential provider for windows": GoogleCredentialProviderForWindows, iina: Iina, @@ -515,6 +518,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { zed: Zed, zen: Zen, zeplin: Zeplin, + "zoom rooms": ZoomRooms, zotero: Zotero, } as const; diff --git a/website/assets/images/app-icon-fleet-desktop-60x60@2x.png b/website/assets/images/app-icon-fleet-desktop-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4707e5bab98cd9d2eec3ad92fcb1dc318183adf GIT binary patch literal 4310 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 literal 0 HcmV?d00001 diff --git a/website/assets/images/app-icon-zoom-rooms-60x60@2x.png b/website/assets/images/app-icon-zoom-rooms-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..28b40a3e8772e4f04dfa124c25f7677c61601b20 GIT binary patch literal 13406 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