├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ └── docs.yml ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── config.toml ├── content │ ├── _index.md │ ├── getting-started │ │ ├── _index.md │ │ ├── fileStructure.md │ │ ├── overview.md │ │ └── userchrome.md │ ├── guides │ │ ├── _index.md │ │ ├── autoUpdates.md │ │ ├── branding.md │ │ ├── includingAddons.md │ │ ├── removingPocket.md │ │ └── windows.md │ └── reference │ │ ├── _index.md │ │ └── config.md ├── public │ └── CNAME ├── sass │ ├── _search.scss │ ├── _variables.scss │ ├── fabric-icons-inline.scss │ └── main.scss ├── static │ ├── CNAME │ └── images │ │ └── userchrome │ │ ├── 001_Vanilla_firefox.png │ │ └── 002_Browser_Toolbox.png └── templates │ ├── anchor-link.html │ ├── index.html │ └── page.html ├── jest.config.js ├── package.json ├── patches.sh ├── src ├── cmds.ts ├── commands │ ├── bootstrap.ts │ ├── build.ts │ ├── ci.ts │ ├── discard.ts │ ├── download.ts │ ├── download │ │ ├── addon.ts │ │ └── firefox.ts │ ├── execute.ts │ ├── export-file.test.ts │ ├── export-file.ts │ ├── ff-version.test.ts │ ├── ff-version.ts │ ├── init.ts │ ├── license-check.test.ts │ ├── license-check.ts │ ├── license-check.txt │ ├── package.ts │ ├── patches │ │ ├── branding-patch.ts │ │ ├── command.ts │ │ ├── copy-patches.ts │ │ ├── git-patch.ts │ │ └── index.ts │ ├── reset.ts │ ├── run.ts │ ├── set.ts │ ├── setup-project.test.ts │ ├── setup-project.ts │ ├── status.ts │ ├── update.ts │ └── updates │ │ ├── addons.ts │ │ └── browser.ts ├── constants │ ├── index.ts │ └── mozconfig.ts ├── index.ts ├── log.ts ├── middleware │ ├── patch-check.ts │ ├── register-command.ts │ └── update-check.ts ├── types.d.ts └── utils │ ├── change-tracking.ts │ ├── command-exists.ts │ ├── config.test.ts │ ├── config.ts │ ├── delay.ts │ ├── dispatch.test.ts │ ├── dispatch.ts │ ├── download.ts │ ├── dynamic-config.test.ts │ ├── dynamic-config.ts │ ├── error-handler.ts │ ├── fs.ts │ ├── index.ts │ ├── store.ts │ ├── string-template.ts │ ├── task-list.ts │ ├── version-formatter.ts │ ├── version.test.ts │ └── version.ts ├── template ├── .vscode │ └── settings.json ├── branding.optional │ ├── configure.sh │ └── locales │ │ └── en-US │ │ ├── brand.dtd │ │ ├── brand.ftl │ │ └── brand.properties ├── configs │ ├── common │ │ └── mozconfig │ ├── linux │ │ └── mozconfig │ ├── macos │ │ └── mozconfig │ └── windows │ │ └── mozconfig ├── patches.optional │ └── .gitkeep └── src │ └── browser │ └── themes.optional │ ├── custom │ ├── linux │ │ └── linux.inc.css │ ├── macos │ │ └── macos.inc.css │ ├── shared │ │ └── shared.inc.css │ └── windows │ │ └── windows.inc.css │ ├── linux │ ├── browser-css.patch │ └── jar-mn.patch │ ├── osx │ ├── browser-css.patch │ └── jar-mn.patch │ ├── shared │ ├── browser-shared-css.patch │ └── jar-inc-mn.patch │ └── windows │ ├── browser-css.patch │ └── jar-mn.patch ├── tests └── assets │ └── invalid-license.txt ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | template/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:unicorn/recommended'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | sourceType: 'module', 11 | }, 12 | plugins: ['@typescript-eslint', 'unicorn'], 13 | rules: { 14 | 'unicorn/no-process-exit': 0, 15 | // We are currently using commonjs. If / when it becomes viable for us to 16 | // switch to ESModules, we should consider enabling this rule 17 | 'unicorn/prefer-module': 0, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: trickypr 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | general: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 🛎️ 13 | uses: actions/checkout@v2.3.1 14 | 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 'lts/*' 18 | cache: 'yarn' 19 | 20 | - name: Install dependencies 21 | run: yarn --frozen-lockfile 22 | 23 | - name: Build 24 | run: yarn build 25 | 26 | - name: Test 27 | run: yarn test 28 | 29 | - name: Lint 30 | run: yarn lint 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '16 12 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'docs/**' 9 | - '.github/**' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | docs: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 🛎️ 17 | uses: actions/checkout@v2.3.1 18 | 19 | - name: Deploy docs 20 | uses: shalzz/zola-deploy-action@v0.13.0 21 | env: 22 | # Target branch 23 | PAGES_BRANCH: gh-pages 24 | BUILD_DIR: docs/ 25 | # Provide personal access token 26 | TOKEN: ${{ secrets.ROBOT_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | testing/ 107 | 108 | site/ 109 | 110 | docs/public/ 111 | .gluon 112 | 113 | # Patch generation 114 | .moz-central 115 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | testing/ 2 | node_modules/ 3 | src/ 4 | !template/src/ 5 | !dist/ 6 | docs/ 7 | site/ 8 | tests/ 9 | 10 | .prettierrc.json 11 | gluon.json 12 | tsconfig.json 13 | yarn.lock 14 | .eslintignore 15 | .eslintrc.json 16 | .gluon 17 | .vscode/ 18 | .github/ 19 | .eslintrc.js 20 | jest.config.js 21 | 22 | # Patch generation 23 | .moz-central 24 | patches.sh -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitmoji.additionalEmojis": [ 3 | { 4 | "emoji": "🔄", 5 | "code": ":arrows_counterclockwise:", 6 | "description": "Update changelog" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.0.0-rc.5] 11 | 12 | ### Fixed 13 | 14 | - `update`: Update will abort if already up to date 15 | - `download`: Will send the correct UA to github 16 | 17 | ### Removed 18 | 19 | - `import`: Gluon will no longer change the update url 20 | 21 | ## [1.0.0-rc.3] 22 | 23 | ### Fixed 24 | 25 | - Template is now valid for firefox 107 26 | 27 | ### Changed 28 | 29 | - Improved error handling and logging for the download command 30 | - `download`: The `engine/` directory will only be deleted if it is empty. Otherwise it will skip. Justification can be found in [#27](https://github.com/pulse-browser/gluon/issues/27) 31 | 32 | ## [1.0.0-rc.2] 33 | 34 | ### Fixed 35 | 36 | - `license-check`: Correct definition of `--fix` 37 | 38 | ## [1.0.0-rc.1] 39 | 40 | ### Added 41 | 42 | - CLI Related: 43 | - `config`: Allow you to specify environment-specific config 44 | - `package`: Generate update manifests 45 | - `config`: Different build modes using `buildMode` key 46 | - `ci`: A command for configuring everything on CI with one command 47 | 48 | ### Changed 49 | 50 | - `Docs`: Vastly improved docs 51 | - `mozconfig`: Update the update-signing to match mozilla's new system 52 | - `perf`: Reduced dependencies somewhat 53 | - CLI: 54 | - `setup-project`: Asks for binary name 55 | - Config 56 | - `firefox version`: Remove `ESR_NEXT`. Mozilla no longer provides this information via their API 57 | - `addons`: Addons now can be specified by provider. This allows for [bot update notifications](https://github.com/pulse-browser/update-bot). For migration example, see [this commit](https://github.com/pulse-browser/browser/commit/2ca3b2606299ef03e2adbcf43974bbe6ec8c2eea) 58 | 59 | ### Fixed 60 | 61 | - `.gluon` is included in generated gitignore 62 | 63 | ### Removed 64 | 65 | - `setup-project`: HTML template has been removed. Use [motherhen](https://github.com/ajvincent/motherhen) or [Quark Runtime (wip)](https://github.com/quark-platform/runtime) 66 | 67 | ## [1.0.0-a.2] 68 | 69 | ### Added 70 | 71 | - Initial beta release 72 | 73 | [1.0.0-rc.5]: https://github.com/pulse-browser/gluon/compare/v1.0.0-rc.3...v1.0.0-rc.4 74 | [1.0.0-rc.3]: https://github.com/pulse-browser/gluon/compare/v1.0.0-rc.2...v1.0.0-rc.3 75 | [1.0.0-rc.2]: https://github.com/pulse-browser/gluon/compare/v1.0.0-rc.1...v1.0.0-rc.2 76 | [1.0.0-rc.1]: https://github.com/pulse-browser/gluon/compare/v1.0.0-a.2...v1.0.0-rc.1 77 | [1.0.0-a.2]: https://github.com/pulse-browser/gluon/compare/v1.0.0-a.1...v1.0.0-a.2 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 |

6 | 7 | # Gluon 8 | 9 | Build Firefox-based browsers with ease 10 | 11 | **This is still in a prerelease / prototype phase. Changes will be made, things will be broken** 12 | 13 |
14 | 15 | ## Installation 16 | 17 | Per project (recommended): 18 | 19 | ```sh 20 | npm install gluon-build 21 | # or 22 | yarn add gluon-build 23 | ``` 24 | 25 | Globally: 26 | 27 | ```sh 28 | npm install -g gluon-build 29 | # or 30 | yarn global add gluon-build 31 | 32 | # Note: Linux and mac users may have to run the above command with sudo 33 | ``` 34 | 35 | ## Documentation 36 | 37 | Documentation is available on [docs.gluon.dev](https://docs.gluon.dev) or in the docs folder of this repository. 38 | 39 | ## Licencing notes 40 | 41 | The following is included in good faith. The writer is not a lawyer, and this is not legal advice. 42 | 43 | ### Melon 44 | 45 | Gluon has been extracted from melon, the build tool for the [desktop version of Dot Browser](https://github.com/dothq/browser-desktop) under MPL v2.0. 46 | 47 | This Source Code Form is subject to the terms of the Mozilla Public 48 | License, v. 2.0. If a copy of the MPL was not distributed with this 49 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 50 | 51 | ### Firefox 52 | 53 | This program downloads and modifies Firefox. [Follow their license](https://hg.mozilla.org/mozilla-central/file/tip/LICENSE) when distributing your program. 54 | 55 | ### Logo 56 | 57 | The package icon is from [Microsoft's Fluent Emoji](https://github.com/microsoft/fluentui-emoji). There is an [ongoing conversation](https://github.com/microsoft/fluentui-emoji/issues/18) regarding the license. 58 | 59 | ``` 60 | MIT License 61 | 62 | Copyright (c) Microsoft Corporation. 63 | 64 | Permission is hereby granted, free of charge, to any person obtaining a copy 65 | of this software and associated documentation files (the "Software"), to deal 66 | in the Software without restriction, including without limitation the rights 67 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 68 | copies of the Software, and to permit persons to whom the Software is 69 | furnished to do so, subject to the following conditions: 70 | 71 | The above copyright notice and this permission notice shall be included in all 72 | copies or substantial portions of the Software. 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/config.toml: -------------------------------------------------------------------------------- 1 | name = "Gluon" 2 | description = "Build Firefox-based browsers with ease" 3 | license = "MPL 2.0" 4 | homepage = "https://github.com/pulse-browser/gluon" 5 | min_version = "1.14.0" 6 | 7 | # The URL the site will be built for 8 | base_url = "https://docs.gluon.dev" 9 | 10 | # Whether to automatically compile all Sass files in the sass directory 11 | compile_sass = true 12 | 13 | # Whether to build a search index to be used later on by a JavaScript library 14 | build_search_index = true 15 | 16 | [markdown] 17 | # Whether to do syntax highlighting 18 | # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola 19 | highlight_code = true 20 | 21 | [extra] 22 | logo = "https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Package/3D/package_3d.png" 23 | release = "https://api.github.com/repos/pulse-browser/gluon/releases/latest" 24 | 25 | [author] 26 | name = "TrickyPR" 27 | -------------------------------------------------------------------------------- /docs/content/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "index" 3 | insert_anchor_links = "right" 4 | +++ 5 | 6 | ## Welcome to the Gluon docs 7 | 8 | If you are new here, you should read the [Getting Started](./getting-started/overview/) guide. 9 | -------------------------------------------------------------------------------- /docs/content/getting-started/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Getting Started" 3 | weight = 1 4 | sort_by = "weight" 5 | +++ 6 | -------------------------------------------------------------------------------- /docs/content/getting-started/fileStructure.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Understanding the file structure" 3 | weight = 10 4 | +++ 5 | 6 | # Understanding the file structure 7 | 8 | Lets take a look at the file structure of your project. It should look something like this: 9 | 10 | ```filesystem 11 | ├─ .gitignore 12 | ├─ gluon.json 13 | ├─ configs 14 | │ ├─ common 15 | │ │ └─ mozconfig 16 | │ ├─ linux 17 | │ │ └─ mozconfig 18 | │ ├─ macos 19 | │ │ └─ mozconfig 20 | │ └─ windows 21 | │ └─ mozconfig 22 | ├─ src 23 | │ ├─ README.md 24 | │ └─ browser 25 | │ └─ themes 26 | │ ├─ custom 27 | │ │ ├─ linux 28 | │ │ │ └─ linux.inc.css 29 | │ │ ├─ macos 30 | │ │ │ └─ macos.inc.css 31 | │ │ ├─ shared 32 | │ │ │ └─ shared.inc.css 33 | │ │ └─ windows 34 | │ │ └─ windows.inc.css 35 | │ ├─ linux 36 | │ │ ├─ browser-css.patch 37 | │ │ └─ jar-mn.patch 38 | │ ├─ osx 39 | │ │ ├─ browser-css.patch 40 | │ │ └─ jar-mn.patch 41 | │ ├─ shared 42 | │ │ ├─ browser-shared-css.patch 43 | │ │ └─ jar-inc-mn.patch 44 | │ └─ windows 45 | │ ├─ browser-css.patch 46 | │ └─ jar-mn.patch 47 | ├─ .gluon 48 | │ └─ ... 49 | └─ engine 50 | └─ ... 51 | ``` 52 | 53 | Whilst this may seem large (especially if you look inside of the `engine`) directory, it is all fairly manageable. 54 | 55 | ## gluon.json 56 | 57 | The primary configuration file for your project is the `gluon.json` file. This is where you will put information about your browser so Gluon can build it correctly. It should look something like this: 58 | 59 | ```json 60 | { 61 | "name": "Gluon Example Browser", 62 | "vendor": "Fushra", 63 | "appId": "dev.gluon.example", 64 | "binaryName": "gluon-example-browser", 65 | 66 | "version": { 67 | "product": "firefox", 68 | "version": "102.0.1" 69 | }, 70 | 71 | "buildOptions": { 72 | "windowsUseSymbolicLinks": false 73 | } 74 | } 75 | ``` 76 | 77 | Up the top of the config file, we have general information about the browser you are building. This includes its name, the vendor creating it, its appId, and the name of the binary that will be output. 78 | 79 | The `version` key is used to specify information about the product you are building. `product` is the Firefox branch you are building against. `version` is the version of Firefox you are building against, which will vary with the branch. Here `firefox` refers to the stable branch of Firefox. 80 | 81 | `buildOptions` provides a number of internal toggles for how Gluon builds your project. 82 | 83 | ## Configs 84 | 85 | The configs folder stores a combination of config files that are required by Firefox and assets required by Gluon. By default there are only [`mozconfig` files](https://firefox-source-docs.mozilla.org/build/buildsystem/mozconfigs.html), Gluon should generate most parts of this config for you. The only part that you will need to change is the source control repo: 86 | 87 | ```bash 88 | ac_add_options --with-app-name=${binName} 89 | export MOZ_USER_DIR="${name}" 90 | export MOZ_APP_VENDOR="${vendor}" 91 | export MOZ_APP_BASENAME=${binName} 92 | export MOZ_APP_PROFILE=${binName} 93 | export MOZ_APP_DISPLAYNAME="${name}" 94 | export MOZ_MACBUNDLE_ID=${appId} 95 | export MOZ_DISTRIBUTION_ID=${appId} 96 | 97 | # Uncomment if builds are too resource hungry 98 | # mk_add_options MOZ_MAKE_FLAGS="-j4" 99 | # ac_add_options --enable-linker=gold 100 | 101 | # Misc 102 | export MOZ_STUB_INSTALLER=1 103 | export MOZ_INCLUDE_SOURCE_INFO=1 104 | export MOZ_SOURCE_REPO=https://github.com/dothq/browser-desktop # <-- Change this! 105 | export MOZ_SOURCE_CHANGESET=${changeset} 106 | ``` 107 | 108 | This directory is also where you would put [branding assets for your browser](/guides/branding) 109 | 110 | ## src/ 111 | 112 | The source folder contains all of the modifications that you have made to Firefox. These come in two types, inserted files (and folders) and patches. Both of these are applied using the `gluon import` command. 113 | 114 | Inserted files are just files (and folders) that you have inserted into the Firefox source code. These will overwrite existing files if they already exist. On Linux and MacOS, these are symlinked so when you change a file in `src/`, the change will be mirrored in Firefox's source code instantly. On Windows, you will need to run `gluon import` for these changes to apply. 115 | 116 | Patches are changes to Firefox's files. As a rule of thumb, you should prefer splitting new content into a new file rather than using patches, but there are times when you must modify Firefox's source code. Each of these patch files are just git patch files: 117 | 118 | ```patch 119 | diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn 120 | index 404a88b218c652afac0cb2004676d22da53d48f3..5a4668ef2970dd773536907f51f3e7e7e3e023cb 100644 121 | --- a/browser/themes/linux/jar.mn 122 | +++ b/browser/themes/linux/jar.mn 123 | @@ -6,7 +6,7 @@ browser.jar: 124 | % skin browser classic/1.0 %skin/classic/browser/ 125 | #include ../shared/jar.inc.mn 126 | skin/classic/browser/sanitizeDialog.css 127 | - skin/classic/browser/browser.css 128 | +* skin/classic/browser/browser.css 129 | skin/classic/browser/contextmenu.css (../shared/contextmenu.css) 130 | skin/classic/browser/monitor-base.png 131 | skin/classic/browser/monitor-border.png 132 | ``` 133 | 134 | In this patch, you can see that I am adding a `*` to the start of a line. You generate these patches by modifying the file in the `engine/` directory and running `gluon export` to export your changes to the src directory. Be careful, if you do not export your changes, they will not be saved and will not work on other developers' computers or yours after an update! 135 | 136 | ```sh 137 | gluon export browser/themes/linux/jar.mn 138 | ``` 139 | 140 | ## engine/ 141 | 142 | The engine directory contains all of Firefox's source code. It is massive - around 15GB in size (around 11GB of that are build assets from when you run `gluon build`). I am not able to provide a full explanation of the contents of the directory. 143 | 144 | However, most of the changes you will want to make will be in `engine/browser/`, which contains the source code for the browser's UI. Here are some of the important directories inside of the `engine/browser/` directory: 145 | 146 | - [`engine/browser/base/content`](https://searchfox.org/mozilla-central/source/browser/base/content): These contain the xhtml files that make up a majority of the browser's ui 147 | - [`engine/browser/components`](https://searchfox.org/mozilla-central/source/browser/components): This contains some self-contained UI features, like screenshots, uitours, downloads, etc. 148 | - [`engine/browser/themes`](https://searchfox.org/mozilla-central/source/browser/themes): Here lies most of the browsers CSS. See [Customizing Your Browser's UI](/getting-started/userchrome) for more information. 149 | 150 | One of the best ways to find what you are looking for and get to know the code base is by searching it. However, if you try and search through it on your computer you are in for a world of pain. Instead, I recommend you use [SearchFox](https://searchfox.org), which is significantly faster. 151 | -------------------------------------------------------------------------------- /docs/content/getting-started/overview.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Setting up your project" 3 | weight = 5 4 | +++ 5 | 6 | ## Getting started with gluon 7 | 8 | ### What is gluon 9 | 10 | Gluon is a build tool and documentation for creating firefox-based browsers. Its goal is to simplify the process of creating web browsers to encourage competition and development within the space. 11 | 12 | ### Getting help 13 | 14 | If you are having problems with following these instructions, or with gluon in general, please contact us. You can [create a discussion on github](https://github.com/pulse-browser/gluon/discussions/new), ping @trickypr on the [Fushra Discord](https://discord.gg/xNkretH7sD). 15 | 16 | ### System requirements 17 | 18 | - **OS**: Linux and MacOS (If you are using windows, take a look at the [Windows Guide](../windows/)) 19 | - **Gluon dependencies**: NodeJS and npm 20 | - **Browser dependencies**: Will be handled by bootstrapping 21 | 22 | ### Getting started 23 | 24 | The first thing you are going to need to do is to install Gluon. As it is a nodejs program it can be installed through npm or yarn. 25 | 26 | ```sh 27 | npm install -g gluon-build@next 28 | # or 29 | yarn global add gluon-build@next 30 | 31 | # Note: Linux and mac users may have to run the above command with sudo 32 | ``` 33 | 34 | Now create a git repo and clone it to your local machine. Then run the following: 35 | 36 | ```sh 37 | gluon setup-project 38 | ``` 39 | 40 | This will ask you a variety of questions in relation to your project setup. Firstly, the release of the browser you want to bind to. 41 | 42 | ``` 43 | ? Select a product to fork › - Use arrow-keys. Return to submit. 44 | ❯ Firefox stable 45 | Firefox extended support (older) 46 | Firefox extended support (newer) 47 | Firefox developer edition (Not recommended) 48 | Firefox beta (Not recommended) 49 | ``` 50 | 51 | You can change what version you are bound to at any time. Pulse Browser currently uses the stable releases, but if you want a lower workload, the newer Extended Support releases might be good for you. 52 | 53 | Then next is the version of the browser you want to use. By default gluon will populate this with the latest version available, which we recommend using. Simply click enter to accept. 54 | 55 | ``` 56 | ? Enter the version of this product › 102.0.1 57 | ``` 58 | 59 | Next it will ask for the name of your browser. Avoid references to Firefox or other Mozilla brands, as this is likely to lead to trademark and copyright issues down the road. 60 | 61 | ``` 62 | ? Enter a product name › Gluon Example Browser 63 | ``` 64 | 65 | The binary name is the name that your program will be run from. We recommend that you add `-browser` to the end to [avoid conflicts with common utilities](https://github.com/dothq/browser/issues/604). 66 | 67 | ``` 68 | ? Enter the name of the binary › gluon-example-browser 69 | ``` 70 | 71 | Vendor is the company (or solo developer) who is creating the browser. 72 | 73 | ``` 74 | ? Enter a vendor › Fushra 75 | ``` 76 | 77 | The appid follows reverse dns naming conventions. For example, Fushra owns the domain `fushra.com`, so our browser is `com.fushra.browser.desktop`. If you do not have a domain, you can use your username / pseudonym as the appid, e.g. `trickypr.watermelon`. 78 | 79 | ``` 80 | ? Enter an appid › dev.gluon.example 81 | ``` 82 | 83 | Next you need to chose a starting template for your browser. If you know what you are doing, you can go with `None` and configure it how you like. Otherwise, we recommend you stick with `UserChrome`. 84 | 85 | ``` 86 | ? Select a ui mode template › - Use arrow-keys. Return to submit. 87 | None 88 | ❯ User Chrome (custom browser css, simplest) 89 | ``` 90 | 91 | Now you have created the directory structure for your project, you can build it for the first time. First, ask gluon to download the firefox source. 92 | 93 | ```sh 94 | gluon download 95 | ``` 96 | 97 | If you are running this for the first time, you will need to install the firefox dependencies. You can do this via boostrapping: 98 | 99 | ```sh 100 | gluon bootstrap 101 | ``` 102 | 103 | After the source code has been downloaded, the changes to firefox described in the source code must be applied. 104 | 105 | ```sh 106 | gluon import 107 | ``` 108 | 109 | Finally, you can start building the firefox source code. This takes around an hour and a half on my computer, but the binary output will be cached, making later builds faster 110 | 111 | ```sh 112 | gluon build 113 | ``` 114 | 115 | Now you can finally start the browser! 116 | 117 | ```sh 118 | gluon run 119 | ``` 120 | 121 | ## Common errors 122 | 123 | Here are some common errors that you might run into whilst running `gluon build` and some potential fixes. 124 | 125 | ### Anything to do with `wasm-ld` 126 | 127 | On Arch linux, there were two errors that were thrown: 128 | 129 | ``` 130 | Executable "wasm-ld" doesn't exist! 131 | wasm-ld: error: cannot open /usr/lib/clang/{CLANG_VERSION}/lib/wasi/libclang_rt.builtins-wasm32.a: No such file or directory 132 | ``` 133 | 134 | On Linux, I fixed the first error by installing `ldd`: 135 | 136 | ```sh 137 | apt-get install lld-7 # Debian 138 | apt-get install lld-8 # Ubuntu 139 | apk add lld # Alpine 140 | pacman -S lld # Arch 141 | dnf install lld # Fedora 142 | ``` 143 | 144 | The second error was fixed by installing the associated wasm libraries: 145 | 146 | ```sh 147 | sudo pacman -Syu wasi-libc wasi-libc++ wasi-compiler-rt 148 | ``` 149 | 150 | You will need to port the above command to your distrobution. If you do not care about the improved security of sandboxed libraries, you can simply disable them by adding the following to `` 151 | -------------------------------------------------------------------------------- /docs/content/getting-started/userchrome.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Customizing Your Browser's UI" 3 | weight = 15 4 | +++ 5 | 6 | # Customizing Your Browser's UI 7 | 8 | If you have already setup your project, you should have a working version of firefox that you built yourself: 9 | 10 | ![A regular firefox compile](/images/userchrome/001_Vanilla_firefox.png) 11 | 12 | Just like Electron apps, the entire Firefox ui is an (x)html page that is styled with css. This makes the process of giving our browser an identity very easy. 13 | 14 | Because it is just web technologies, we can use debugging tools (like inspect element) to understand what changes we want to make to the browser. You can open these debugging tools by pressing the keyboard shortcut `CTRL + ALT + SHIFT + I` (`CMD + OPTION + SHIFT + I` on MacOS). 15 | 16 | ![Inspect element but for a web browser](/images/userchrome/002_Browser_Toolbox.png) 17 | 18 | Because this is a more visual process, the rest of this tutorial is going to be in this video: 19 | 20 | 21 | 22 | This is the end of the guided section of the documentation. You should now have enough knowledge to modify Firefox as you want. There are more guides that you might find useful on the left sidebar, in no particular order. 23 | -------------------------------------------------------------------------------- /docs/content/guides/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Guides" 3 | weight = 2 4 | sort_by = "weight" 5 | +++ 6 | -------------------------------------------------------------------------------- /docs/content/guides/autoUpdates.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Automatic updates" 3 | weight = 20 4 | +++ 5 | 6 | # Automatic updates 7 | 8 | > **Note** 9 | > Automatic updates only works for browsers using GitHub to host their binaries. 10 | 11 | Mozilla provides an automatic update service that can be used by anyone building a Firefox fork or any other application that depends on Mozilla toolkit. 12 | 13 | Attached to your "brand", you will need to include information regarding that brand's release. For example, Pulse has the following information attached to its alpha brand. 14 | 15 | ```json 16 | { 17 | ..., 18 | "brands": { 19 | ..., 20 | "alpha": { 21 | ..., 22 | 23 | "release": { 24 | "displayVersion": "1.0.0-a.17", 25 | "github": { 26 | "repo": "pulse-browser/browser" 27 | }, 28 | "x86": { 29 | "windowsMar": "windows.mar", 30 | "macosMar": "macosIntel.mar", 31 | "linuxMar": "linux.mar" 32 | } 33 | } 34 | } 35 | }, 36 | ... 37 | } 38 | ``` 39 | 40 | The release key includes both the latest version (`displayVersion`) and release info. In this case, binaries are released to github. For `x86`, we provide a number of `.mar` files for each platform. `.mar` files are "Mozilla Archives" and are used to distribute updates. 41 | 42 | When creating update manifests, they will point to [the release tagged `displayVersion`](https://github.com/pulse-browser/browser/releases/tag/1.0.0-a.17). The update manifests tell Mozilla's updater to download the update manifest with the version that corresponds with the file name above. So an x86 linux computer will download the `linux.mar` file and use that to update. 43 | 44 | ## Creating MAR files and update manifests 45 | 46 | Both `.mar` files and the update manifests are automatically created by running `gluon package`. This will generate a number of files in the `dist` directory. 47 | 48 | An `output.mar` will be included in the root, which you should rename to the appropriate platform. For example, `linux.mar` for linux. This should be included with your GitHub release. 49 | 50 | Update manifests are stored in the `dist/update` directory. The contents of this directory should be uploaded to a webserver (e.g. GitHub Pages or S3) such that their root is at `/updates/browser/`. 51 | 52 | You will then need to set the `updateHostname` in `gluon.json` to the url of your update server. For Pulse, this is `updates.pulsebrowser.app`. You may also need to change the update server specified [here](https://searchfox.org/mozilla-central/rev/560b7b1b174ed36912b969eee0c1920f3c59bc56/build/moz.build#94). 53 | -------------------------------------------------------------------------------- /docs/content/guides/branding.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Branding your browser" 3 | weight = 15 4 | +++ 5 | 6 | # Branding your browser 7 | 8 | Before you ship your browser, you will want to include your own branding, rather than just using Mozilla's template branding. Gluon will automatically generate branding once you configure it correctly. 9 | 10 | ## Creating a brand 11 | 12 | You will first need to add a `brands` key within your `gluon.json`. For example: 13 | 14 | ```json 15 | { 16 | ... 17 | "brands": { 18 | "stable": { 19 | "backgroundColor": "#2B2A33", 20 | "brandShorterName": "Pulse", 21 | "brandShortName": "Pulse Browser", 22 | "brandFullName": "Pulse Browser" 23 | } 24 | }, 25 | ... 26 | } 27 | ``` 28 | 29 | More information regarding the available keys for this config object can be found in the [reference section](/reference/config/#brands). 30 | 31 | You will then need to create the folder `config/branding/`. In here, you will need to add a high-resolution `logo.png` (which will then be downscaled on import) and a `MacOSInstaller.svg` file, which will be used as the background for the macOS dmg file. 32 | 33 | When you add or change a brand, you will need to reimport your changes and specify the brand to target using `gluon set brand`. 34 | 35 | ## Specifying which brand to target 36 | 37 | You can specify the brand that you want to build for using the `gluon set brand ` command. For example: 38 | 39 | ```sh 40 | gluon set brand stable 41 | ``` 42 | 43 | Note that once you have set a new brand, you will need to rebuild your browser for changes to take effect: 44 | 45 | ```sh 46 | gluon build 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/content/guides/includingAddons.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Including addons" 3 | weight = 10 4 | +++ 5 | 6 | # Including addons 7 | 8 | Gluon provides an automated system for including extensions in your project. The addons are downloaded and included during the `download` build step. Addons can be included in the project config (`gluon.json`). 9 | 10 | ```json 11 | { 12 | // Your options here 13 | "addons": { 14 | "ublock": { 15 | "id": "uBlock0@raymondhill.net", 16 | "url": "https://github.com/gorhill/uBlock/releases/download/1.39.0/uBlock0_1.39.0.firefox.xpi" 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | Note that the `id` is the gecko application id specified in the `manifest.json`. 23 | 24 | ```json 25 | { 26 | // ... 27 | 28 | "browser_specific_settings": { 29 | "gecko": { 30 | "id": "uBlock0@raymondhill.net", 31 | "strict_min_version": "60.0" 32 | } 33 | } 34 | 35 | // ... 36 | } 37 | ``` 38 | 39 | ## Specifying location in customizable ui 40 | 41 | By default, when adding an addon with a toolbar button, it will be placed next to the hamburger menu. However, you may want to place it somewhere else. To do this, you must change the customizable ui in a similar way to how you would when removing pocket. 42 | 43 | You are going to want to open `engine/browser/components/customizableui/CustomizableUI.jsm`. At the top, you want to import the `ExtensionCommon` module. 44 | 45 | ```js 46 | const { makeWidgetId } = ChromeUtils.import( 47 | 'resource://gre/modules/ExtensionCommon.jsm' 48 | ).ExtensionCommon 49 | ``` 50 | 51 | Then, add a constant with the id of the addon at the top of the file, for example: 52 | 53 | ```js 54 | const kUBlockOriginID = 'uBlock0@raymondhill.net' 55 | ``` 56 | 57 | Now, you can go down to the `navbarPlacements` array (around line 240) and add 58 | 59 | ```js 60 | `${makeWidgetId(kUBlockOriginID)}-browser-action`, 61 | ``` 62 | 63 | To the array where you want the icon to appear, for example: 64 | 65 | ```js 66 | let navbarPlacements = [ 67 | 'back-button', 68 | 'forward-button', 69 | 'stop-reload-button', 70 | Services.policies.isAllowed('removeHomeButtonByDefault') 71 | ? null 72 | : 'home-button', 73 | 'spring', 74 | `${makeWidgetId(kUBlockOriginID)}-browser-action`, 75 | 'urlbar-container', 76 | 'spring', 77 | 'save-to-pocket-button', 78 | 'downloads-button', 79 | AppConstants.MOZ_DEV_EDITION ? 'developer-button' : null, 80 | 'fxa-toolbar-menu-button', 81 | ].filter((name) => name) 82 | ``` 83 | 84 | Finally, export the changes you have made: 85 | 86 | ```sh 87 | gluon export-file browser/components/customizableui/CustomizableUI.jsm 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/content/guides/removingPocket.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Removing pocket" 3 | weight = 5 4 | +++ 5 | 6 | # Removing pocket 7 | 8 | **Note:** This expects you have gluon setup. 9 | 10 | ## Disabling in firefox.js 11 | 12 | The goal of this guide is to disable pocket and remove its icon from the toolbar. The first changes we will need to make is to the firefox.js file located in `engine/browser/app/profile/firefox.js`. Scroll to the lines that include the following settings (around line 1980 in firefox 94): 13 | 14 | ```js 15 | pref('extensions.pocket.api', 'api.getpocket.com') 16 | pref('extensions.pocket.enabled', true) 17 | pref('extensions.pocket.oAuthConsumerKey', '40249-e88c401e1b1f2242d9e441c4') 18 | pref('extensions.pocket.site', 'getpocket.com') 19 | pref('extensions.pocket.onSaveRecs', true) 20 | pref('extensions.pocket.onSaveRecs.locales', 'en-US,en-GB,en-CA') 21 | ``` 22 | 23 | Delete these lines and replace them with the following: 24 | 25 | ```js 26 | // Taken from BetterFox user.js 27 | user_pref('extensions.pocket.enabled', false) 28 | user_pref('extensions.pocket.api', ' ') 29 | user_pref('extensions.pocket.oAuthConsumerKey', ' ') 30 | user_pref('extensions.pocket.site', ' ') 31 | ``` 32 | 33 | Next, you will need to remove pocket from the new tab page. You can do this by simply adding the following line to the bottom of `firefox.js`: 34 | 35 | ```js 36 | user_pref( 37 | 'browser.newtabpage.activity-stream.section.highlights.includePocket', 38 | false 39 | ) 40 | ``` 41 | 42 | Now you simply need to export the changes made to `firefox.js`: 43 | 44 | ```sh 45 | gluon export-file browser/app/profile/firefox.js 46 | ``` 47 | 48 | ## Removing pocket icon from toolbar 49 | 50 | Whilst the steps above will have disabled pocket. The pocket icon will still be visible in the toolbar. Instead you must remove it from the CustomizableUI layout. Open `engine/browser/components/customizableui/CustomizableUI.jsm` and find the array that looks like this (around line 240): 51 | 52 | ```js 53 | let navbarPlacements = [ 54 | 'back-button', 55 | 'forward-button', 56 | 'stop-reload-button', 57 | Services.policies.isAllowed('removeHomeButtonByDefault') 58 | ? null 59 | : 'home-button', 60 | 'spring', 61 | 'urlbar-container', 62 | 'spring', 63 | 'save-to-pocket-button', 64 | 'downloads-button', 65 | AppConstants.MOZ_DEV_EDITION ? 'developer-button' : null, 66 | 'fxa-toolbar-menu-button', 67 | ].filter((name) => name) 68 | ``` 69 | 70 | Remove the `save-to-pocket-button` item from the array and export the changes: 71 | 72 | ```sh 73 | gluon export-file browser/components/customizableui/CustomizableUI.jsm 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/content/guides/windows.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Preparing Windows" 3 | weight = 0 4 | +++ 5 | 6 | # Preparing Windows 7 | 8 | Building on windows is significantly more complex than building on macos or linux. This guide will walk you through preparing your Windows machine for building a Firefox fork. Before building, you should be aware that [only Windows 10 or 11 are officially supported](https://firefox-source-docs.mozilla.org/build/buildsystem/supported-configurations.html#build-hosts), but you might be able to get other versions to work. 9 | 10 | ## Installing Dependencies 11 | 12 | The first thing you will need to do is install Microsoft's c++ build tools. You will need to download [Build Tools for Visual Studio 2022](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022). The following will need to be installed: 13 | 14 | - **In the _Workloads_ tab** 15 | - Desktop development with C++ 16 | - **In the _Individual components_ tab** 17 | - MSVC v143 - VS 2022 C++ x64/x86 build tools. 18 | - Windows 10 SDK (at least 10.0.19041.0). 19 | - C++ ATL for v143 build tools (x86 and x64). 20 | 21 | > **Note:** 22 | > If this guide ever gets out of date, you can get the latest build requirements from [Mozilla's docs](https://firefox-source-docs.mozilla.org/setup/windows_build.html#system-preparation) 23 | 24 | You will need to install [MozillaBuild](https://ftp.mozilla.org/pub/mozilla/libraries/win32/MozillaBuildSetup-Latest.exe). Next, [install Git](https://git-scm.com/download/win). You will need to set the following specific options on install to ensure high performance: 25 | 26 | - Configuring the line ending conversions must be: Checkout as-is, commit as-is 27 | - Enable experimental built-in file system monitor 28 | 29 | Install [NodeJS](https://nodejs.org/en/download/current/) on your system. This should also install chocolatey. If it does not, [install it manually](https://docs.chocolatey.org/en-us/choco/setup). To install the final two dependencies, run: 30 | 31 | ```sh 32 | npm install --global yarn 33 | choco install make 34 | ``` 35 | 36 | You should be good to return back to the main [Getting Started docs](/getting-started/overview) 37 | 38 | ## Additional packages required for releasing 39 | 40 | If you are creating binaries to target windows, you will need nsis (which mach calls `makensisu` for some reason, even though the binary is `makensis`): 41 | 42 | ```powershell 43 | choco install nsis 44 | ``` 45 | 46 | Note that you will also have to provide a path to nsis on your system. For mine it is: 47 | 48 | ```sh 49 | export MAKENSISU="C:\\Program Files (x86)\\NSIS\\Bin\\makensis.exe" 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/content/reference/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Reference" 3 | weight = 4 4 | sort_by = "weight" 5 | +++ 6 | -------------------------------------------------------------------------------- /docs/content/reference/config.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "`gluon.json` Reference" 3 | weight = 0 4 | +++ 5 | 6 | # gluon.json Reference 7 | 8 | This reference guide may get outdated. If you need to check something, you can read [the config interface type](https://github.com/pulse-browser/gluon/blob/main/src/utils/config.ts#L96). 9 | 10 | ## name 11 | 12 | This is the name of the product that is to be built. 13 | 14 | ```json 15 | { 16 | "name": "Pulse Browser" 17 | } 18 | ``` 19 | 20 | ## vendor 21 | 22 | The name of the company that is building the browser 23 | 24 | ```json 25 | { 26 | "vendor": "Fushra" 27 | } 28 | ``` 29 | 30 | ## appId 31 | 32 | A reverse DNS identifier for the browser 33 | 34 | ```json 35 | { 36 | "appId": "com.fushra.browser" 37 | } 38 | ``` 39 | 40 | ## binaryName 41 | 42 | The name of the output binary. 43 | 44 | ```json 45 | { 46 | "binaryName": "pulse-browser" 47 | } 48 | ``` 49 | 50 | ## updateHostname 51 | 52 | The host of the update server for updating. This is configured as part of the build command 53 | 54 | ```json 55 | { 56 | "updateHostname": "updates.pulsebrowser.app" 57 | } 58 | ``` 59 | 60 | ## license 61 | 62 | Information about the license the browser will be under. This is used by the Gluon license checker to ensure files have the MPL header if specified. 63 | 64 | Specification: 65 | 66 | ```ts 67 | interface LicenseConfig { 68 | /** 69 | * What license you intend to put your project under. Currently MPL is the 70 | * only one supported by the license checker, but if you want implement more 71 | * please feel free to open a pull request. 72 | * 73 | * To disable the license checker, set this type to `unknown` 74 | */ 75 | licenseType: 'MPL-2.0' | 'unknown' 76 | /** 77 | * Files to be ignored by the license checker. For default values see the 78 | * `defaultConfig` variable in the config.ts file 79 | * 80 | * These should be rejex tests because compiled regex tests are **really** 81 | * fast which will stop the license checker from becoming absurdly slow with 82 | * larger projects 83 | */ 84 | ignoredFiles: string[] 85 | } 86 | ``` 87 | 88 | Example: 89 | 90 | ```json 91 | { 92 | "licenseType": "MPL-2.0", 93 | "ignoredFiles": [".*\\.json"] 94 | } 95 | ``` 96 | 97 | Commands that maybe used: 98 | 99 | ```sh 100 | gluon license-check 101 | gluon lc # Alias 102 | ``` 103 | 104 | ## version 105 | 106 | Provides information to gluon about the product and version that Gluon is responsible for managing. 107 | 108 | Specification: 109 | 110 | ```typescript 111 | enum SupportedProducts { 112 | Firefox = 'firefox', 113 | FirefoxESR = 'firefox-esr', 114 | FirefoxESRNext = 'firefox-esr-next', 115 | FirefoxDev = 'firefox-dev', 116 | FirefoxBeta = 'firefox-beta', 117 | FirefoxNightly = 'firefox-nightly', 118 | } 119 | 120 | interface VersionConfig { 121 | /** 122 | * What branch of firefox you are forking. e.g. stable ('firefox'), dev ('firefox-dev') 123 | * , esr ('firefox-esr') etc. 124 | * 125 | * For use in code, use {@link SupportedProducts} 126 | */ 127 | product: SupportedProducts 128 | /** 129 | * The version of the selected product you are forking 130 | */ 131 | version?: string 132 | } 133 | ``` 134 | 135 | Example 136 | 137 | ```json 138 | { 139 | "version": { 140 | "product": "firefox", 141 | "version": "102.0.1" 142 | } 143 | } 144 | ``` 145 | 146 | ## buildOptions 147 | 148 | These are flags that change how parts of Gluon operate. 149 | 150 | ### windowsUseSymbolicLinks 151 | 152 | When set to `true`, symbolic links will be enabled on Windows. From internal testing, this appears to fail on a majority of computers 153 | 154 | ## addons 155 | 156 | An index for each addon. These will be downloaded and configured as part of the `download` step that gluon performs. You can download extensions from AMO, Github or any URL. Note that the furha-robot will only be able to provide update checking to AMO and Github Extensions. 157 | 158 | Specification: 159 | 160 | ```typescript 161 | export interface GithubAddonInfo { 162 | platform: 'github' 163 | id: string 164 | repo: string 165 | version: string 166 | fileGlob: string 167 | } 168 | 169 | export interface AMOAddonInfo { 170 | platform: 'amo' 171 | id: string 172 | amoId: string 173 | version: string 174 | } 175 | 176 | export interface UrlAddonInfo { 177 | platform: 'url' 178 | version: string 179 | id: string 180 | url: string 181 | } 182 | 183 | export type AddonInfo = GithubAddonInfo | AMOAddonInfo | UrlAddonInfo 184 | 185 | type addons = Record 186 | ``` 187 | 188 | Example: 189 | 190 | ```json 191 | { 192 | "addons": { 193 | "ublock": { 194 | "platform": "github", 195 | "id": "uBlock0@raymondhill.net", 196 | "repo": "gorhill/uBlock", 197 | "version": "1.43.0", 198 | "fileGlob": "uBlock0_*.firefox.signed.xpi" 199 | }, 200 | "tabliss": { 201 | "platform": "amo", 202 | "id": "extension@tabliss.io", 203 | "amoId": "850407", 204 | "version": "2.6.0" 205 | } 206 | } 207 | } 208 | ``` 209 | 210 | Commands that use this: 211 | 212 | ```sh 213 | gluon download 214 | gluon updates-addons # Generates update manifests for addons 215 | ``` 216 | 217 | ## brands 218 | 219 | These are different distrobutions, for example, beta and stable. 220 | 221 | Specification: 222 | 223 | ```typescript 224 | export interface ReleaseInfo { 225 | /** 226 | * The version of your output product. E.g. 1.3.5 227 | */ 228 | displayVersion: string 229 | github?: { 230 | repo: string 231 | } 232 | 233 | x86?: { 234 | windowsMar?: string 235 | macosMar?: string 236 | linuxMar?: string 237 | } 238 | } 239 | 240 | export interface BrandInfo { 241 | backgroundColor: string 242 | brandShorterName: string 243 | brandShortName: string 244 | brandFullName: string 245 | release: ReleaseInfo 246 | } 247 | 248 | type brands = Record 249 | ``` 250 | 251 | Example: 252 | 253 | ```json 254 | { 255 | "brands": { 256 | "stable": { 257 | "backgroundColor": "#2B2A33", 258 | "brandShorterName": "Pulse", 259 | "brandShortName": "Pulse Browser", 260 | "brandFullName": "Pulse Browser", 261 | "release": { 262 | "displayVersion": "1.0.0", 263 | "github": { 264 | "repo": "pulse-browser/browser" 265 | }, 266 | "x86": { 267 | "windowsMar": "windows.mar", 268 | "macosMar": "macosIntel.mar", 269 | "linuxMar": "linux.mar" 270 | } 271 | } 272 | }, 273 | "beta": { 274 | "backgroundColor": "#2B2A33", 275 | "brandShorterName": "Pulse", 276 | "brandShortName": "Pulse Browser", 277 | "brandFullName": "Pulse Browser Beta", 278 | "release": { 279 | "displayVersion": "1.0.0-b.0", 280 | "github": { 281 | "repo": "pulse-browser/browser" 282 | }, 283 | "x86": { 284 | "windowsMar": "windows.mar", 285 | "macosMar": "macosIntel.mar", 286 | "linuxMar": "linux.mar" 287 | } 288 | } 289 | }, 290 | "alpha": { 291 | "backgroundColor": "#2B2A33", 292 | "brandShorterName": "Pulse", 293 | "brandShortName": "Pulse Browser", 294 | "brandFullName": "Pulse Browser Alpha", 295 | "release": { 296 | "displayVersion": "1.0.0-a.16", 297 | "github": { 298 | "repo": "pulse-browser/browser" 299 | }, 300 | "x86": { 301 | "windowsMar": "windows.mar", 302 | "macosMar": "macosIntel.mar", 303 | "linuxMar": "linux.mar" 304 | } 305 | } 306 | } 307 | } 308 | } 309 | ``` 310 | 311 | Commands: 312 | 313 | ```sh 314 | gluon build 315 | gluon package 316 | gluon updates-browser 317 | gluon set brand 318 | ``` 319 | -------------------------------------------------------------------------------- /docs/public/CNAME: -------------------------------------------------------------------------------- 1 | docs.gluon.dev 2 | -------------------------------------------------------------------------------- /docs/sass/_search.scss: -------------------------------------------------------------------------------- 1 | .search-container { 2 | display: none; 3 | 4 | &--is-visible { 5 | display: block; 6 | width: 100%; 7 | } 8 | 9 | #search { 10 | width: 100%; 11 | display: block; 12 | border:none; 13 | border-left: 1px solid $color; 14 | padding:1px 0; 15 | text-align: left; 16 | line-height: $baseline; 17 | font-size: $font-size; 18 | font-family:$font-family; 19 | color:$color; 20 | background:transparent; 21 | } 22 | 23 | #search:focus { 24 | outline:none; 25 | border:none; 26 | } 27 | 28 | .search-results { 29 | &__header { 30 | font-weight: bold; 31 | padding: 1rem 0rem; 32 | } 33 | 34 | &__items { 35 | margin: 0 2vw; 36 | padding: 0; 37 | list-style: circle; 38 | } 39 | 40 | &__item { 41 | margin-bottom: 1rem; 42 | } 43 | 44 | &__teaser { 45 | 46 | } 47 | } 48 | } 49 | 50 | #on_right { 51 | display: block; 52 | text-align: right; 53 | margin-bottom: $baseline; 54 | } 55 | 56 | #search-ico { 57 | font-family: 'FabricMDL2Icons'; 58 | cursor: pointer; 59 | font-size: $baseline; 60 | line-height: 1; 61 | } -------------------------------------------------------------------------------- /docs/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg: #f9f9f9; 3 | --fg: #222; 4 | 5 | --links: #00f; 6 | --hover-links: #c00; 7 | --visited-links: #009; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --bg: #333; 13 | --fg: #f9f9f9; 14 | 15 | --links: rgb(142, 142, 255); 16 | --hover-links: rgb(204, 101, 101); 17 | --visited-links: rgb(86, 86, 151); 18 | } 19 | } 20 | 21 | $baseline: 1.5rem; 22 | 23 | $background: var(--bg); 24 | $color: var(--fg); 25 | 26 | $links: var(--links); 27 | $hover-links: var(--hover-links); 28 | $visited-links: var(--visited-links); 29 | 30 | $font-size: 1.125rem; 31 | $font-family: Segoe UI, system-ui, -apple-system, sans-serif; 32 | $line-height: 1.75; 33 | $code_font: 400 1.125rem/1.75 SFMono-Regular, Consolas, Liberation Mono, Menlo, 34 | monospace; 35 | -------------------------------------------------------------------------------- /docs/sass/fabric-icons-inline.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Your use of the content in the files referenced here is subject to the terms of the license at https://aka.ms/fabric-assets-license 3 | */ 4 | @font-face { 5 | font-family: 'FabricMDL2Icons'; 6 | src: url('data:application/octet-stream;base64,d09GRgABAAAAAAusAA4AAAAAFLgABDXDAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABRAAAAEgAAABgMUZ1H2NtYXAAAAGMAAAAWgAAAYKg2Y81Y3Z0IAAAAegAAAAgAAAAKgnZCa9mcGdtAAACCAAAAPAAAAFZ/J7mjmdhc3AAAAL4AAAADAAAAAwACAAbZ2x5ZgAAAwQAAANyAAAEuLnx29VoZWFkAAAGeAAAADIAAAA2A3zu4GhoZWEAAAasAAAAFQAAACQQAQgDaG10eAAABsQAAAAYAAAAGA+HAaZsb2NhAAAG3AAAABYAAAAWBoYE+m1heHAAAAb0AAAAHQAAACAAJAHEbmFtZQAABxQAAAP3AAAJ+o6N8lFwb3N0AAALDAAAABQAAAAg/1EAgXByZXAAAAsgAAAAiQAAANN4vfIOeJxjYGHfzjiBgZWBgXUWqzEDA6M0hGa+yJDGJMTBysrFyMQIBgxAIMCAAL7BCgoMDs8Z3ulxgPkQkgGsjgXCU2BgAADc3QgGeJxjYGBgZoBgGQZGBhCoAfIYwXwWhgQgLcIgABRhec7wXPG50XO/54df7H5x4mXBO73//xkYsIlKMko8lLgqsVXCUdxL3E5shuBtqMkYgJENu/hIAgCdyyInAAB4nGPQYghlKGBoYFjFyMDYwOzAeIDBAYsIEAAAqhwHlXicXY+/TsNADMZzJLSEJ0A6IZ11KkOViJ3phksk1CUlDOelgNRKpO+AlIXFA8/ibhnzYgjMEf4utr/P+ny/c6f5yXx2nKVHKilWnDfhoNQLDurtmf35IU/vNmVhTNV5VvdlwWoJomtOF/VNsGjI0PWWTG0eH7acLWKXxY7w0nDShk7qbQB2qL/HHeJVPJLFI4QS30/xfYxL+rUsVobTiyasA/des/OoAUzFYxN49BoQf8ikP3VnE+NsOWXbwE5zgkSfygL3RJqE+0uPf/Wgkv+G+23Iv6tB9U3c9Bb0h2HBgrChl2fbUAkaYPkOhPxkxgABAAIACAAK//8AD3icXVNNaBtXEJ55b1dPsl0165UqUOJ1dze7mx+quFrJilwQwgQ3P8UpOGCKUhNfSnrqJb/Q4BcoGAr9CfSUGHpyLr2VJCT0klsv7SVXQ29uySmJCbQr7646byWnpjvsm583b+bNN/OAwX0A7Sv9GnAQAC3DNjzbsO/zP+JH7FFyFvRr/a9/0BaBPg6AMg85OgAFKMJR+CWzctCOPwY48ATegtzrJzAGnNZ8Juskz7yPdtMuG2+WPPwD//26lDIGKRmurQFTifJE4EKL8tUtrVwqaq7jB5ijtdloYQ2bjY5m1jus2agx1ymycslienf1wcbti/X6xdsbD1ZvbV+KX5jVqm/yA+cvDG3Xn230ehvPro94Hobm4bEL5+OXpl+tmty4tH1raNuFfe4Zp8olSEFE9U9CFYLsjozqCoxGh4VI4NEfEtnoEpquUSHRsAUcrLmlaHu75NYOIsWJCbeESkfJQO6CvPsZJ1lItR/JP/W7yj8BJndlKhEGhHtCR/r37jFIYdgPCdS0vOqHIOwBVSLTLmTcEBBJreehl26hTCGW+lbfy9NZ9KKeTkhHFAPf4D0OUEBRwKCArQJWtDv8izsxEfFsIZUuvV+NlQtAhgkImgwKMw4GVEY3IQRCMww8ewSKQoEqTYH3UEpvczOWzBtAQppGNSZSA21r10OZIy2Vm1sIfckIlL5Us6fCMwnvwTn4fIR6qchc26mxwC7yTGiqHti0VbE7PEQakVY2NLMfYE15DEeFPEazoywirL9TLuWOo8XD3NP5K8thuHxlfgDty0tzE+nribmly+0BzF9drteXr87j0I4TmT2WVnvm8NjDqu9XH44dnmlbAziy0LCsxsIRXe5JA/i/F5Mqh4rpn1o5eXLllK9iq9x7egqTzokpGkh/6oQzqaLsRVN8/x4gfgoi96GI1NMsMNVAtsijWLziIo5eCZJiscMFFzv0HiWwPIhf4W0wqVM+1FW3iAQaNDg50VS8hUYL9SHGOYG6iR2szDYbvuuQKlqcusXenU7WeJd3F+YSme6w038n371MHqd/6c+PnZmdtg4lYbq+wn6fOt0rH50uVseSj5x1HLBPsBt/n75Yw672Mf6YrqY7485P6dM00JbSn7/EdvLtDVz8JpVW88yx4CxFWcGb7LepQ1HZmg4KFGXdgX8Bg/8uhAAAeJxjYGRgYGAxPVwnx6UQz2/zlYGbgwEE9v892ACi78Sumg+iORjA4pwMTCAKAB/CCRAAAHicY2BkYOBgAAE4yciACpgAAsoAHQAAAAUqAKYIAAAAAAAAgACAAAAAgAAAAV0AgAAAABYASACYAN4BAAEiAVQB4gH4AlwAAHicY2BkYGDgYshiYGUAAUYwyQXCjJEgJgAOogDqAAAAeJy1VE+LGzcUf1472S1plhIo5KhDKZvFjLNuIDQ5LUlzyl42YSGXgjySZ0TGIyFpMkzpoccc+jF6CfRTlBZ67LmfoOeeeux7bzReb+yGbaEeRvPT0/v7e08GgLujz2EE/e8LfHs8gju46/Ee7MNXCY9R/jzhCeKvE74BH4NL+CZ8At8mvA9fwvcJH8Cn8EvCt+AYfk/49ujn0SThQzje+xWjjCYf4U7t/ZnwCD4bXyS8B4fjbxIeo/xtwhPEPyZ8A+6Of0v4JojxHwnvg58cJHwAx5PBzy14Ofkh4dvjt5O/Ej6Elwff/fROzO+fPBRnJvc22GUUT6x31stobJ2J06oS56YoYxDnOmj/RqvsmVx4k4uzp8/n4jQEHcO5LppK+u2DbcmF9gE9iwfZ/KQ/pcP+7IUurBYmCCmil0qvpH8t7FLEUm/kV3jbOBLnduVkbXTIdiZfxugezWZt22ar4TxDm1nsnC28dGU3W9o6htmleWicq4xWgg4y8co2YiU70QSNSWBiJBbRitxrGfVUKBNcJbupkLUSzhs8zVFF41cG4bRfmRjR3aLjIiqT65p84UEQ1g9gSRGm26U6b1WTx6kg5tF2SjZDAFOLtjR5uZFZi0FNnVeNwjats7d11Ykjc0/o1QJzuVRHDx/KltWVqQvhdYjYKWL1MgCZr309ZgaODEaJekUt8AajKtvWlZXqKnuyp0p7KsdiKFyb6JoolKYySafUlbvKKA5j3SV1agg6RH5KszCYc3b9bsM7EDCH+3ACDxGdgYEcPFgI+C4houwJIo93nlaJEoOohgxPTqHCR8A5ygoo8SzwTuNXo/YbXBVqPkO7Be7JN8V4iv8sc7YPrEl2ZFVAg/4kal7H4jo6F5xHSDkLeIDZzLHWTdvBctPuBWdjcRWoQ1VJfCMzoFC64ixfo4xYopOSdXfxV/C+QQYH7Ry/K9xLzMkwW9m/YJ54jih9BDN8Wn4y9Pe+fZbizBB37KVgPw49dChdsjeqdrYzeuCcHXbEcB/F2oJ6/4prEsxEh9+GueuZ6BkbtElmuWqPGlSHhinuFes57njHEuKD4jjuTG+bJy867SX7dtxXqjnyGVktOI+hExVXRFZDXr1F4C74LclyXcP0Wl11vFdok+N+ynz1M9/Hna7jvF+B4Ulsmacc192ctalS0s6xmobnTu3knmwqRkeofw+/NKGLxMsu730O/5XbS++KPRUo8zzHMd2pYVZ3VTBE387r8cYMUCV9LZHjDbeA/Pe1KpS0XLnlW/mh2ZNXpkpzX2xa+6p63PDNatiSsh26OfghzYpv8j/PaP/PWKfOXHofbohJLNP8UL4LZrrv7f9wt/8GD0U4iAB4nGNgZgCD/34M5QyYgAsAKTQB0nic28CgzbCJkZNJm3ETF4jcztWaG2qrysChvZ07NdhBTwbE4onwsNCQBLF4nc215YVBLD4dFRkRHhCLX05CmI8DxBLg4+FkZwGxBMEAxBLaMKEgwADIYtjOCDeaCW40M9xoFrjRrHCj2eQkoUazw43mgBvNCTd6kzAju/YGBgXX2kwJFwDEASgaAAAA') format('truetype'); 7 | } 8 | 9 | .ms-Icon { 10 | -moz-osx-font-smoothing: grayscale; 11 | -webkit-font-smoothing: antialiased; 12 | display: inline-block; 13 | font-family: 'FabricMDL2Icons'; 14 | font-style: normal; 15 | font-weight: normal; 16 | speak: none; 17 | } 18 | 19 | // Mixins 20 | @mixin ms-Icon--ChevronRightSmall { content: "\E970"; } 21 | @mixin ms-Icon--ChromeClose { content: "\E8BB"; } 22 | @mixin ms-Icon--Copy { content: "\E8C8"; } 23 | @mixin ms-Icon--GlobalNavButton { content: "\E700"; } 24 | @mixin ms-Icon--MiniLink { content: "\E732"; } 25 | @mixin ms-Icon--Page { content: "\E7C3"; } 26 | @mixin ms-Icon--ProductRelease { content: "\EE2E"; } 27 | @mixin ms-Icon--Save { content: "\E74E"; } 28 | @mixin ms-Icon--Search { content: "\E721"; } 29 | 30 | 31 | // Classes 32 | .ms-Icon--ChevronRightSmall:before { @include ms-Icon--ChevronRightSmall } 33 | .ms-Icon--ChromeClose:before { @include ms-Icon--ChromeClose } 34 | .ms-Icon--Copy:before { @include ms-Icon--Copy } 35 | .ms-Icon--GlobalNavButton:before { @include ms-Icon--GlobalNavButton } 36 | .ms-Icon--MiniLink:before { @include ms-Icon--MiniLink } 37 | .ms-Icon--Page:before { @include ms-Icon--Page } 38 | .ms-Icon--ProductRelease:before { @include ms-Icon--ProductRelease } 39 | .ms-Icon--Save:before { @include ms-Icon--Save } 40 | .ms-Icon--Search:before { @include ms-Icon--Search } -------------------------------------------------------------------------------- /docs/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | html { 4 | font-kerning: normal; 5 | text-rendering: optimizeLegibility; 6 | scroll-behavior: smooth; 7 | } 8 | 9 | body { 10 | margin: $baseline 0; 11 | font-size: $font-size; 12 | font-family: $font-family; 13 | line-height: $line-height; 14 | background: $background; 15 | color: $color; 16 | } 17 | 18 | #wrap { 19 | max-width: 800px; 20 | } 21 | 22 | iframe#youtube { 23 | aspect-ratio: 16 / 9; 24 | width: 100%; 25 | } 26 | 27 | @keyframes fade-in { 28 | 0% { 29 | opacity: 0; 30 | } 31 | 32 | 50% { 33 | opacity: 0.8; 34 | } 35 | 36 | 100% { 37 | opacity: 1; 38 | } 39 | } 40 | 41 | a { 42 | &:link { 43 | color: $links; 44 | text-decoration: none; 45 | } 46 | 47 | &:hover { 48 | color: $hover-links; 49 | } 50 | 51 | &:visited { 52 | color: $visited-links; 53 | } 54 | } 55 | 56 | h1 { 57 | font-size: 3rem; 58 | } 59 | 60 | h2, 61 | h3, 62 | h4 { 63 | .anchor { 64 | visibility: hidden; 65 | text-decoration: none; 66 | cursor: pointer; 67 | line-height: 1; 68 | color: $color; 69 | } 70 | 71 | &:hover { 72 | .anchor { 73 | visibility: visible; 74 | animation: fade-in 0.3s ease-in-out; 75 | font-family: 'FabricMDL2Icons'; 76 | } 77 | } 78 | } 79 | 80 | pre { 81 | margin: $baseline 0; 82 | border-radius: 4px; 83 | padding: $baseline; 84 | overflow: auto; 85 | position: relative; 86 | 87 | code { 88 | background: transparent; 89 | 90 | &::after { 91 | content: attr(data-lang); 92 | font-style: italic; 93 | line-height: 1; 94 | opacity: 0.3; 95 | position: absolute; 96 | bottom: $baseline; 97 | right: $baseline; 98 | z-index: 1; 99 | } 100 | } 101 | } 102 | 103 | .language-filesystem { 104 | // Compact should be as compressed as possible 105 | line-height: 0.95em; 106 | } 107 | 108 | code { 109 | font: $code_font; 110 | } 111 | 112 | .copy-code-button { 113 | font-family: 'FabricMDL2Icons'; 114 | display: none; 115 | background: $background; 116 | border-radius: 4px; 117 | border: none; 118 | cursor: pointer; 119 | animation: fade-in 0.3s ease-in-out; 120 | font-size: $baseline; 121 | color: $color; 122 | z-index: 10; 123 | position: absolute; 124 | top: $baseline; 125 | right: $baseline; 126 | } 127 | 128 | pre:hover .copy-code-button { 129 | display: block; 130 | } 131 | 132 | nav { 133 | position: sticky; 134 | height: 92vh; 135 | top: $baseline; 136 | left: $baseline; 137 | bottom: $baseline; 138 | padding-right: $baseline; 139 | width: 20rem; 140 | 141 | img { 142 | width: 128px; 143 | } 144 | 145 | h1 { 146 | margin: 0; 147 | line-height: 1; 148 | } 149 | } 150 | 151 | #toc { 152 | margin-left: calc(#{$baseline} + #{$font-size}); 153 | padding: 0; 154 | margin: 0 0 0 $baseline; 155 | font-size: 80%; 156 | 157 | li { 158 | color: $color; 159 | margin-left: $font-size; 160 | 161 | &::before { 162 | display: inline-block; 163 | content: ''; 164 | } 165 | 166 | ul { 167 | padding: 0; 168 | } 169 | } 170 | } 171 | 172 | main { 173 | display: flex; 174 | flex-flow: row nowrap; 175 | animation: fade-in 0.4s ease-in-out; 176 | } 177 | 178 | #release { 179 | text-align: left; 180 | margin: $baseline 0; 181 | 182 | &::before { 183 | display: inline-block; 184 | content: '\EE2E'; 185 | font-family: 'FabricMDL2Icons'; 186 | margin-right: calc(#{$baseline} / 8); 187 | } 188 | } 189 | 190 | @keyframes slideIn { 191 | 0% { 192 | max-height: 0; 193 | opacity: 0; 194 | } 195 | 100% { 196 | max-height: 999px; 197 | opacity: 1; 198 | } 199 | } 200 | @keyframes slideOut { 201 | 0% { 202 | height: auto; 203 | opacity: 1; 204 | } 205 | 100% { 206 | height: 0; 207 | opacity: 0; 208 | } 209 | } 210 | 211 | nav label { 212 | display: block; 213 | } 214 | 215 | #trees { 216 | overflow-y: auto; 217 | height: 80%; 218 | } 219 | 220 | .subtree { 221 | overflow: hidden; 222 | margin: calc(#{$baseline} / 8) 0; 223 | transition: overflow 0.2s ease-in-out; 224 | padding: 0; 225 | } 226 | 227 | .tree-toggle-label { 228 | user-select: none; 229 | cursor: pointer; 230 | } 231 | 232 | .tree-toggle-label::before { 233 | display: inline-block; 234 | content: '\E970'; 235 | font-family: 'FabricMDL2Icons'; 236 | font-size: 0.75rem; 237 | transform: rotate(0deg); 238 | transform-origin: 50% 50% 0px; 239 | transition: transform 0.1s linear 0s; 240 | margin-right: 2px; 241 | } 242 | 243 | .tree-toggle { 244 | position: absolute; 245 | opacity: 0; 246 | z-index: -1; 247 | } 248 | 249 | .tree-toggle:checked + .tree-toggle-label::before { 250 | content: '\E970'; 251 | font-family: 'FabricMDL2Icons'; 252 | font-size: 0.75rem; 253 | transform: rotate(90deg); 254 | transform-origin: 50% 50% 0px; 255 | transition: transform 0.1s linear 0s; 256 | margin-right: 2px; 257 | } 258 | 259 | .tree-toggle:checked + .tree-toggle-label { 260 | font-weight: bold; 261 | } 262 | 263 | .tree-toggle + .tree-toggle-label + .subtree { 264 | animation-name: slideOut; 265 | animation-duration: 0.25s; 266 | animation-fill-mode: both; 267 | } 268 | 269 | .tree-toggle:checked + .tree-toggle-label + .subtree { 270 | animation-name: slideIn; 271 | animation-duration: 0.25s; 272 | animation-fill-mode: both; 273 | } 274 | 275 | .subtree li { 276 | list-style-type: none; 277 | margin-left: $baseline; 278 | 279 | a { 280 | color: $color; 281 | } 282 | 283 | &::before { 284 | content: '\E7C3'; 285 | font-family: 'FabricMDL2Icons'; 286 | font-size: 0.75rem; 287 | } 288 | } 289 | 290 | .active a { 291 | font-weight: bold; 292 | } 293 | 294 | article { 295 | width: calc(100% - (#{$baseline} * 4 + 20rem)); 296 | margin-left: calc(#{$baseline} * 2); 297 | 298 | img { 299 | max-width: 100%; 300 | } 301 | } 302 | 303 | #mobile { 304 | display: none; 305 | } 306 | 307 | @media screen and (max-width: 1023px) { 308 | main { 309 | flex-flow: column nowrap; 310 | width: 100%; 311 | } 312 | 313 | nav { 314 | position: inherit; 315 | height: auto; 316 | margin: $baseline $baseline 0 $baseline; 317 | } 318 | 319 | article { 320 | width: calc(100% - (#{$baseline} * 2)); 321 | margin: 0 $baseline; 322 | z-index: 1; 323 | } 324 | 325 | #mobile { 326 | font-family: 'FabricMDL2Icons'; 327 | cursor: pointer; 328 | font-size: $baseline; 329 | margin: 0 $baseline 0 0; 330 | display: block; 331 | color: $color; 332 | } 333 | 334 | #trees { 335 | display: none; 336 | position: absolute; 337 | background: $background; 338 | height: auto; 339 | width: 100vw; 340 | z-index: 10; 341 | box-shadow: 0 2px 0 rgba(0, 0, 0, 0.1); 342 | } 343 | 344 | #on_right { 345 | margin-top: $baseline; 346 | } 347 | } 348 | 349 | @import 'fabric-icons-inline'; 350 | @import 'search'; 351 | -------------------------------------------------------------------------------- /docs/static/CNAME: -------------------------------------------------------------------------------- 1 | docs.gluon.dev 2 | -------------------------------------------------------------------------------- /docs/static/images/userchrome/001_Vanilla_firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-browser/gluon/fbdee4a6cc73b0a9b4a98951e5bc17cf377b3430/docs/static/images/userchrome/001_Vanilla_firefox.png -------------------------------------------------------------------------------- /docs/static/images/userchrome/002_Browser_Toolbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-browser/gluon/fbdee4a6cc73b0a9b4a98951e5bc17cf377b3430/docs/static/images/userchrome/002_Browser_Toolbox.png -------------------------------------------------------------------------------- /docs/templates/anchor-link.html: -------------------------------------------------------------------------------- 1 |   -------------------------------------------------------------------------------- /docs/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if config.extra.favicon %} 7 | 8 | {% endif %} 9 | 10 | {% block title %}{{ config.title }}{% endblock title %} 11 | 12 | 13 | 14 | {% if config.extra.release %} 15 | 26 | {% endif %} 27 |
28 | 29 | {% block nav %} 30 | 31 | 102 | {% endblock nav %} 103 | 104 |
105 | 106 | {% if config.build_search_index %} 107 |
108 | 109 |
110 |
111 | 112 |
113 |
114 |
    115 |
    116 |
    117 | {% endif %} 118 | 119 |
    120 | {% block content %} 121 | {{ section.content | safe }} 122 | {% endblock content %} 123 |
    124 | 125 |
    126 |
    127 | 128 | {% if config.build_search_index %} 129 | 130 | 131 | 132 | {% endif %} 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} {% block title %} {{ page.title }} | {{ config.title 2 | }} {% endblock title %} {% block content %} {{ page.content | safe }} {% 3 | endblock content %} 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | rootDir: 'src', 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gluon-build", 3 | "version": "1.0.0-rc.5", 4 | "description": "🍉 Build Firefox-based browsers with ease", 5 | "main": "index.js", 6 | "reveal": true, 7 | "bin": { 8 | "gluon": "./dist/index.js" 9 | }, 10 | "scripts": { 11 | "test": "jest", 12 | "test:dev": "jest --watch", 13 | "build": "tsc && chmod +x ./dist/index.js && cp src/commands/license-check.txt dist/commands/license-check.txt", 14 | "build:win": "tsc && copy /y .\\src\\commands\\license-check.txt .\\dist\\commands\\license-check.txt", 15 | "format": "prettier . -w", 16 | "lint": "eslint .", 17 | "docs:install": "", 18 | "docs:build": "zola --root docs/ build", 19 | "docs:dev": "zola --root docs/ serve", 20 | "self": "cd testing && node ../dist/index.js" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/pulse-browser/gluon.git" 25 | }, 26 | "keywords": [ 27 | "firefox", 28 | "firefox-fork", 29 | "build-tool" 30 | ], 31 | "authors": [ 32 | "TrickyPR" 33 | ], 34 | "license": "MPL-2.0", 35 | "bugs": { 36 | "url": "https://github.com/pulse-browser/gluon/issues" 37 | }, 38 | "homepage": "https://github.com/pulse-browser/gluon#readme", 39 | "dependencies": { 40 | "@resvg/resvg-js": "^1.4.0", 41 | "async-icns": "^1.0.2", 42 | "axios": "^0.21.1", 43 | "chalk": "^4.1.0", 44 | "cli-progress": "^3.9.1", 45 | "commander": "^6.2.1", 46 | "execa": "^5.1.1", 47 | "fs-extra": "^10.0.0", 48 | "ini": "^3.0.0", 49 | "is-apple-silicon": "trickypr/is-apple-silicon", 50 | "kleur": "^4.1.5", 51 | "modern-async": "^1.1.2", 52 | "picomatch": "^2.3.1", 53 | "png-to-ico": "^2.1.4", 54 | "prompts": "^2.4.1", 55 | "rustic": "^1.2.1", 56 | "semver": "^7.3.7", 57 | "sharp": "^0.30.7", 58 | "tiny-glob": "^0.2.9", 59 | "xmlbuilder2": "^3.0.2" 60 | }, 61 | "devDependencies": { 62 | "@types/cli-progress": "^3.9.2", 63 | "@types/fs-extra": "^9.0.13", 64 | "@types/ini": "^1.3.31", 65 | "@types/jest": "^27.0.3", 66 | "@types/listr": "^0.14.4", 67 | "@types/node": "^14.14.16", 68 | "@types/picomatch": "^2.3.0", 69 | "@types/prompts": "^2.0.14", 70 | "@types/rimraf": "^3.0.0", 71 | "@types/semver": "^7.3.10", 72 | "@types/sharp": "^0.29.2", 73 | "@typescript-eslint/eslint-plugin": "^5.22.0", 74 | "@typescript-eslint/parser": "^5.22.0", 75 | "eslint": "^8.15.0", 76 | "eslint-plugin-unicorn": "^44.0.2", 77 | "jest": "^27.4.5", 78 | "prettier": "^2.2.1", 79 | "ts-jest": "^27.1.2", 80 | "typescript": "^4.1.3" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /patches.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | action=$1 7 | 8 | # Make sure the user has specified the location of mozilla-central in their 9 | # environment. Some people (i.e. trickypr) prefer to clone it into other places 10 | if [ ! -f .moz-central ] 11 | then 12 | echo "Please make sure you specify the location of your checkout of `mozilla-central`" 13 | echo "inside of the `.moz-central` file." 14 | exit 1 15 | fi 16 | 17 | mozilla_centeral_repo=$(cat .moz-central) 18 | last_patch=`exec ls -1 ./template/patches.optional | sed 's/-.*//g' | sort -n | tail -1` 19 | next_patch=`expr 1 + ${last_patch:=0}` 20 | root_pwd=$PWD 21 | 22 | if [ $action = "import" ] 23 | then 24 | echo "Importing:" 25 | echo 26 | 27 | cd $mozilla_centeral_repo 28 | for file in $root_pwd/template/patches.optional/*.patch 29 | do 30 | echo " $file..." 31 | # --forward is used to skip the patch if it has already been applied 32 | patch -p1 --forward < $file 33 | done 34 | 35 | cd $root_pwd 36 | elif [ $action = "export" ] 37 | then 38 | if [ -x "$2" ] 39 | then 40 | echo "Please provide a file name. Usage: $0 $action " 41 | exit 1 42 | fi 43 | 44 | echo "Exporting: ${@:2}" 45 | echo 46 | 47 | cd $mozilla_centeral_repo 48 | git add ${@:2} 49 | git commit 50 | git format-patch --start-number $next_patch -1 -o $root_pwd/template/patches.optional 51 | cd $root_pwd 52 | else 53 | echo "Usage: $0 import|export" 54 | echo 55 | echo " import: Import all patches in ./template/patches.optional" 56 | echo " export: Exports a specific patch. Usage: $0 export " 57 | fi -------------------------------------------------------------------------------- /src/cmds.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | /* eslint-disable unicorn/no-await-expression-member */ 6 | 7 | import { Cmd } from './types' 8 | 9 | export const commands: Cmd[] = [ 10 | { 11 | cmd: 'bootstrap', 12 | description: 'Bootstrap the gluon app.', 13 | requestController: async () => 14 | (await import('./commands/bootstrap')).bootstrap, 15 | }, 16 | { 17 | cmd: 'build', 18 | aliases: ['b'], 19 | description: 20 | 'Build the gluon app. Specify the OS param for cross-platform builds.', 21 | options: [ 22 | { 23 | arg: '--u, --ui', 24 | description: 25 | 'Only builds the ui. Faster but not as powerful as a regular build.', 26 | }, 27 | { 28 | arg: '--skip-patch-check', 29 | description: 30 | "Doesn't check to see if all of the patches have been applied", 31 | }, 32 | ], 33 | requestController: async () => (await import('./commands/build')).build, 34 | }, 35 | { 36 | cmd: 'config [value]', 37 | aliases: ['set', 'get'], 38 | description: 'Get and set the dynamic config from this project', 39 | requestController: async () => (await import('./commands/set')).set, 40 | disableMiddleware: true, 41 | }, 42 | { 43 | cmd: 'ci', 44 | description: 'Configure the CI', 45 | requestController: async () => (await import('./commands/ci')).ci, 46 | options: [ 47 | { 48 | arg: '--brand ', 49 | description: 'Set the brand that the build is using', 50 | }, 51 | { 52 | arg: '--bump
    ', 53 | description: 'What version should be bumped', 54 | }, 55 | { 56 | arg: '--display-version ', 57 | description: 'Bind this CI instance to a specific version', 58 | }, 59 | ], 60 | }, 61 | { 62 | cmd: 'discard ', 63 | description: 'Discard a files changes.', 64 | requestController: async () => (await import('./commands/discard')).discard, 65 | }, 66 | { 67 | cmd: 'download', 68 | description: 'Download Firefox.', 69 | options: [ 70 | { 71 | arg: '--force', 72 | description: 'Delete the engine directory if it already exists', 73 | } 74 | ], 75 | requestController: async () => 76 | (await import('./commands/download')).download, 77 | }, 78 | { 79 | cmd: 'update', 80 | aliases: ['update-ff'], 81 | description: 'Update Firefox to latest version.', 82 | requestController: async () => 83 | (await import('./commands/update')).update, 84 | disableMiddleware: true, 85 | }, 86 | { 87 | cmd: 'execute', 88 | description: 'Execute a command inside the engine directory.', 89 | requestController: async () => (await import('./commands/execute')).execute, 90 | }, 91 | { 92 | cmd: 'export-file ', 93 | aliases: ['export'], 94 | description: 'Export a changed file as a patch.', 95 | requestController: async () => 96 | (await import('./commands/export-file')).exportFile, 97 | }, 98 | { 99 | cmd: 'import', 100 | aliases: ['import-patches', 'i'], 101 | description: 'Import patches into the browser.', 102 | requestController: async () => 103 | (await import('./commands/patches')).applyPatches, 104 | }, 105 | { 106 | cmd: 'ff-init ', 107 | aliases: ['ff-initialise', 'ff-initialize'], 108 | description: 'Initialise the Firefox directory.', 109 | requestController: async () => (await import('./commands/init')).init, 110 | }, 111 | { 112 | cmd: 'ff-version', 113 | description: 'Retrieves the version of firefox to build against', 114 | requestController: async () => 115 | (await import('./commands/ff-version')).getFFVersion, 116 | }, 117 | { 118 | cmd: 'license-check', 119 | aliases: ['lc'], 120 | options: [ 121 | { 122 | arg: '--fix', 123 | description: "Will add MPL-2.0 headers to files that don't have it", 124 | }, 125 | ], 126 | description: 'Check the src directory for the absence of MPL-2.0 header.', 127 | requestController: async () => 128 | (await import('./commands/license-check')).licenseCheck, 129 | }, 130 | { 131 | cmd: 'package', 132 | aliases: ['pack'], 133 | description: 'Package the browser for distribution.', 134 | requestController: async () => 135 | (await import('./commands/package')).gluonPackage, 136 | }, 137 | { 138 | cmd: 'reset', 139 | description: 'Reset the source directory to stock Firefox.', 140 | requestController: async () => (await import('./commands/reset')).reset, 141 | }, 142 | { 143 | cmd: 'run [chrome]', 144 | aliases: ['r', 'open'], 145 | description: 'Run the browser.', 146 | requestController: async () => (await import('./commands/run')).run, 147 | }, 148 | { 149 | cmd: 'setup-project', 150 | description: 'Sets up a gluon project for the first time', 151 | requestController: async () => 152 | (await import('./commands/setup-project')).setupProject, 153 | }, 154 | { 155 | cmd: 'status', 156 | description: 'Status and files changed for src directory.', 157 | requestController: async () => (await import('./commands/status')).status, 158 | }, 159 | { 160 | cmd: 'updates-browser', 161 | description: 162 | 'Generate update manifest for the browser binary. This should be run after packaging', 163 | requestController: async () => 164 | (await import('./commands/updates/browser')).generateBrowserUpdateFiles, 165 | }, 166 | { 167 | cmd: 'updates-addons', 168 | description: 169 | 'Generates update manifests for system addons that are included in the browser', 170 | requestController: async () => 171 | (await import('./commands/updates/addons')).generateAddonUpdateFiles, 172 | }, 173 | ] 174 | -------------------------------------------------------------------------------- /src/commands/bootstrap.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import { config } from '..' 6 | import { ENGINE_DIR } from '../constants' 7 | import { log } from '../log' 8 | import { configDispatch } from '../utils' 9 | 10 | export const bootstrap = async () => { 11 | log.info(`Bootstrapping ${config.name}...`) 12 | 13 | const arguments_ = ['--application-choice', 'browser'] 14 | 15 | console.debug(`Passing through to |mach bootstrap|`) 16 | await configDispatch('./mach', { 17 | args: ['bootstrap', ...arguments_], 18 | cwd: ENGINE_DIR, 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/build.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import execa from 'execa' 5 | import { existsSync, readFileSync, writeFileSync } from 'node:fs' 6 | import { join, resolve } from 'node:path' 7 | import { bin_name, config } from '..' 8 | import { BUILD_TARGETS, CONFIGS_DIR, ENGINE_DIR } from '../constants' 9 | import { internalMozconfg } from '../constants/mozconfig' 10 | import { log } from '../log' 11 | import { patchCheck } from '../middleware/patch-check' 12 | import { 13 | BrandInfo, 14 | configDispatch, 15 | dynamicConfig, 16 | stringTemplate, 17 | } from '../utils' 18 | 19 | const platform: Record = { 20 | win32: 'windows', 21 | darwin: 'macos', 22 | linux: 'linux', 23 | } 24 | 25 | const applyConfig = async (os: string) => { 26 | log.info('Applying mozconfig...') 27 | 28 | const brandingKey = dynamicConfig.get('brand') 29 | 30 | let changeset 31 | 32 | try { 33 | // Retrieve changeset 34 | const { stdout } = await execa('git', ['rev-parse', 'HEAD']) 35 | changeset = stdout.trim() 36 | } catch (error) { 37 | log.warning( 38 | 'Gluon expects that you are building your browser with git as your version control' 39 | ) 40 | log.warning( 41 | 'If you are using some other version control system, please migrate to git' 42 | ) 43 | log.warning('Otherwise, you can setup git in this folder by running:') 44 | log.warning(' |git init|') 45 | 46 | throw error 47 | } 48 | 49 | const templateOptions = { 50 | name: config.name, 51 | vendor: config.name, 52 | appId: config.appId, 53 | brandingDir: existsSync(join(ENGINE_DIR, 'branding', 'gluon')) 54 | ? 'branding/gluon' 55 | : 'branding/unofficial', 56 | binName: config.binaryName, 57 | changeset, 58 | } 59 | 60 | const commonConfig = stringTemplate( 61 | readFileSync(resolve(CONFIGS_DIR, 'common', 'mozconfig')).toString(), 62 | templateOptions 63 | ) 64 | 65 | const osConfig = stringTemplate( 66 | readFileSync(resolve(CONFIGS_DIR, os, 'mozconfig')).toString(), 67 | templateOptions 68 | ) 69 | 70 | // Allow a custom config to be placed in /mozconfig. This will not be committed 71 | // to origin 72 | let customConfig = existsSync(join(process.cwd(), 'mozconfig')) 73 | ? readFileSync(join(process.cwd(), 'mozconfig')).toString() 74 | : '' 75 | 76 | customConfig = stringTemplate(customConfig, templateOptions) 77 | 78 | const mergedConfig = 79 | `# This file is automatically generated. You should only modify this if you know what you are doing!\n\n` + 80 | commonConfig + 81 | '\n\n' + 82 | osConfig + 83 | '\n\n' + 84 | customConfig + 85 | '\n' + 86 | internalMozconfg(brandingKey, dynamicConfig.get('buildMode')) 87 | 88 | writeFileSync(resolve(ENGINE_DIR, 'mozconfig'), mergedConfig) 89 | 90 | log.info(`Config for this \`${os}\` build:`) 91 | 92 | mergedConfig.split('\n').map((ln) => { 93 | if (ln.startsWith('mk') || ln.startsWith('ac') || ln.startsWith('export')) 94 | log.info( 95 | `\t${ln 96 | .replace(/mk_add_options /, '') 97 | .replace(/ac_add_options /, '') 98 | .replace(/export /, '')}` 99 | ) 100 | }) 101 | 102 | // We need to install the browser display version inside of browser/config/version.txt 103 | // and browser/config/version_display.txt 104 | const brandingConfig: BrandInfo | undefined = config.brands[brandingKey] 105 | const version = brandingConfig?.release?.displayVersion || '1.0.0' 106 | 107 | log.debug(`Writing ${version} to the browser version files`) 108 | writeFileSync(join(ENGINE_DIR, 'browser/config/version.txt'), version) 109 | writeFileSync(join(ENGINE_DIR, 'browser/config/version_display.txt'), version) 110 | } 111 | 112 | const genericBuild = async (os: string, fast = false) => { 113 | log.info(`Building for "${os}"...`) 114 | 115 | log.warning( 116 | `If you get any dependency errors, try running |${bin_name} bootstrap|.` 117 | ) 118 | 119 | const buildOptions = ['build'] 120 | 121 | if (fast) { 122 | buildOptions.push('faster') 123 | } 124 | 125 | log.debug(`Running with build options ${buildOptions.join(', ')}`) 126 | log.debug(`Mach exists: ${existsSync(join(ENGINE_DIR, 'mach'))}`) 127 | log.debug( 128 | `Mach contents: \n ${readFileSync(join(ENGINE_DIR, 'mach'))}\n\n===END===` 129 | ) 130 | 131 | await configDispatch('./mach', { 132 | args: buildOptions, 133 | cwd: ENGINE_DIR, 134 | killOnError: true, 135 | }) 136 | } 137 | 138 | const parseDate = (d: number) => { 139 | d /= 1000 140 | const h = Math.floor(d / 3600) 141 | const m = Math.floor((d % 3600) / 60) 142 | const s = Math.floor((d % 3600) % 60) 143 | 144 | const hDisplay = h > 0 ? h + (h == 1 ? ' hour, ' : ' hours, ') : '' 145 | const mDisplay = m > 0 ? m + (m == 1 ? ' minute, ' : ' minutes, ') : '' 146 | const sDisplay = s > 0 ? s + (s == 1 ? ' second' : ' seconds') : '' 147 | return hDisplay + mDisplay + sDisplay 148 | } 149 | 150 | const success = (date: number) => { 151 | // mach handles the success messages 152 | console.log() 153 | log.info(`Total build time: ${parseDate(Date.now() - date)}.`) 154 | } 155 | 156 | interface Options { 157 | ui: boolean 158 | skipPatchCheck: boolean 159 | } 160 | 161 | export const build = async (options: Options): Promise => { 162 | const d = Date.now() 163 | 164 | // Host build 165 | 166 | const prettyHost = platform[process.platform] 167 | 168 | if (BUILD_TARGETS.includes(prettyHost)) { 169 | if (!options.skipPatchCheck) await patchCheck() 170 | 171 | await applyConfig(prettyHost) 172 | 173 | log.info('Starting build...') 174 | 175 | await genericBuild(prettyHost, options.ui).then(() => success(d)) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/commands/ci.ts: -------------------------------------------------------------------------------- 1 | // Code to handle common, complex tasks that our CI servers have to do to get 2 | // ready to build everything 3 | 4 | import { inc, ReleaseType } from 'semver' 5 | import { config } from '..' 6 | import { log } from '../log' 7 | import { dynamicConfig, saveConfig } from '../utils' 8 | 9 | interface Options { 10 | brand?: string 11 | bump?: ReleaseType 12 | displayVersion?: string 13 | } 14 | 15 | export const ci = (options: Options) => { 16 | log.info('Set the build to release') 17 | dynamicConfig.set('buildMode', 'release') 18 | 19 | if (options.brand) { 20 | log.info(`Setting the brand to be '${options.brand}'`) 21 | dynamicConfig.set('brand', options.brand) 22 | } 23 | 24 | if (options.bump) { 25 | const oldVersion = 26 | config.brands[dynamicConfig.get('brand')].release.displayVersion 27 | const version = inc( 28 | config.brands[dynamicConfig.get('brand')].release.displayVersion, 29 | options.bump 30 | ) 31 | 32 | config.brands[dynamicConfig.get('brand')].release.displayVersion = 33 | version || 34 | config.brands[dynamicConfig.get('brand')].release.displayVersion 35 | saveConfig() 36 | 37 | log.info(`Bumped the version: ${oldVersion} → ${version}`) 38 | } 39 | 40 | if (options.displayVersion) { 41 | config.brands[dynamicConfig.get('brand')].release.displayVersion = 42 | options.displayVersion || 43 | config.brands[dynamicConfig.get('brand')].release.displayVersion 44 | saveConfig() 45 | 46 | log.info( 47 | `Loaded version: ${ 48 | config.brands[dynamicConfig.get('brand')].release.displayVersion 49 | } → ${options.displayVersion}` 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/discard.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import execa from 'execa' 5 | import { existsSync, statSync } from 'node:fs' 6 | import { resolve } from 'node:path' 7 | import { log } from '../log' 8 | import { ENGINE_DIR } from '../constants' 9 | 10 | export const discard = async (file: string): Promise => { 11 | const realFile = resolve(ENGINE_DIR, file) 12 | 13 | log.info(`Discarding ${file}...`) 14 | 15 | if (!existsSync(realFile)) throw new Error(`File ${file} does not exist`) 16 | if (!statSync(realFile).isFile()) throw new Error('Target must be a file.') 17 | 18 | try { 19 | await execa('git', ['restore', file], { cwd: ENGINE_DIR }) 20 | } catch { 21 | log.warning(`The file ${file} was not changed`) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/download.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import { bin_name, config } from '..' 6 | import { log } from '../log' 7 | import { 8 | downloadInternals 9 | } from './download/firefox' 10 | 11 | type Options = { 12 | force?: boolean 13 | } 14 | 15 | export const download = async (options: Options): Promise => { 16 | const version = config.version.version 17 | 18 | // If gFFVersion isn't specified, provide legible error 19 | if (!version) { 20 | log.error( 21 | 'You have not specified a version of firefox in your config file. This is required to build a firefox fork' 22 | ) 23 | process.exit(1) 24 | } 25 | 26 | await downloadInternals({version, force: options.force}) 27 | 28 | log.success( 29 | `You should be ready to make changes to ${config.name}.`, 30 | '', 31 | 'Remember to change the repository in configs/common/mozconfig to your own.', 32 | `You should import the patches next, run |${bin_name} import|.`, 33 | `To begin building ${config.name}, run |${bin_name} build|.` 34 | ) 35 | console.log() 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/download/addon.ts: -------------------------------------------------------------------------------- 1 | import { 2 | existsSync, 3 | readFileSync, 4 | unlinkSync, 5 | writeFileSync, 6 | } from 'node:fs' 7 | import { join } from 'node:path' 8 | import { isMatch } from 'picomatch' 9 | 10 | import { config } from '../..' 11 | import { ENGINE_DIR, MELON_TMP_DIR } from '../../constants' 12 | import { log } from '../../log' 13 | 14 | import { 15 | AddonInfo, 16 | configDispatch, 17 | delay, 18 | ensureDirectory, 19 | walkDirectoryTree, 20 | windowsPathToUnix, 21 | } from '../../utils' 22 | import { downloadFileToLocation } from '../../utils/download' 23 | import { readItem } from '../../utils/store' 24 | import { discard } from '../discard' 25 | import axios from 'axios' 26 | 27 | type GithubReleaseAssets = 28 | | { url: string; browser_download_url: string; name: string }[] 29 | | null 30 | 31 | export const getAddons = (): (AddonInfo & { name: string })[] => 32 | Object.keys(config.addons).map((addon) => ({ 33 | name: addon, 34 | ...config.addons[addon], 35 | })) 36 | 37 | export async function resolveAddonDownloadUrl( 38 | addon: AddonInfo 39 | ): Promise { 40 | switch (addon.platform) { 41 | case 'url': { 42 | return addon.url 43 | } 44 | 45 | case 'amo': { 46 | try { 47 | const mozillaData = await axios.get( 48 | `https://addons.mozilla.org/api/v4/addons/addon/${addon.amoId}/versions/` 49 | ) 50 | 51 | return mozillaData.data.results[0].files[0].url 52 | } catch (error) { 53 | log.warning( 54 | 'The following error occured whilst fetching amo addon metadata' 55 | ) 56 | log.error(error) 57 | 58 | return '' 59 | } 60 | } 61 | 62 | case 'github': { 63 | try { 64 | const githubData = await axios.get( 65 | `https://api.github.com/repos/${addon.repo}/releases/tags/${addon.version}`, 66 | { 67 | headers: { 68 | 'User-Agent': 'gluon-build -> addon downloader' 69 | } 70 | } 71 | ) 72 | 73 | const assets: GithubReleaseAssets = githubData.data.assets 74 | 75 | if (!assets) 76 | throw `The GitHub releases api did not return any assets for ${addon.repo} -> ${addon.version}.` 77 | 78 | const matchingFile = assets.find((asset) => 79 | isMatch(asset.name, addon.fileGlob) 80 | ) 81 | const fileDownloadUrl = matchingFile?.browser_download_url 82 | 83 | if (!matchingFile) 84 | throw `The GitHub releases api did not provide any files that matched '${addon.fileGlob}'` 85 | if (!fileDownloadUrl) 86 | throw `The GitHub releases api did not provide a download url for '${matchingFile.name}'` 87 | 88 | return ( 89 | fileDownloadUrl || 90 | '{Release file does not include a file matching the glob}' 91 | ) 92 | } catch (error) { 93 | log.warning( 94 | 'The following error occurred whilst fetching github metadata' 95 | ) 96 | log.error(error) 97 | 98 | return '' 99 | } 100 | } 101 | } 102 | } 103 | 104 | export async function downloadAddon( 105 | url: string, 106 | addon: AddonInfo & { name: string } 107 | ): Promise { 108 | const temporaryFile = join(MELON_TMP_DIR, addon.name + '.xpi') 109 | 110 | log.info(`Download addon from ${url}`) 111 | 112 | { 113 | const extensionCache = readItem<{ url: string }>(addon.name) 114 | 115 | if (extensionCache.isNone()) { 116 | // We haven't stored it in the cache, therefore we need to redownload 117 | // it 118 | } else { 119 | const cache = extensionCache.unwrap() 120 | if (cache.url == url && existsSync(temporaryFile)) { 121 | log.info(`Using cached version of ${addon.name}`) 122 | return temporaryFile 123 | } 124 | } 125 | } 126 | 127 | if (existsSync(temporaryFile)) { 128 | unlinkSync(temporaryFile) 129 | } 130 | 131 | await downloadFileToLocation(url, temporaryFile) 132 | 133 | // I do not know why, but this delay causes unzip to work reliably 134 | await delay(200) 135 | 136 | return temporaryFile 137 | } 138 | 139 | export async function unpackAddon( 140 | path: string, 141 | addon: AddonInfo & { name: string } 142 | ) { 143 | const outPath = join(ENGINE_DIR, 'browser', 'extensions', addon.name) 144 | 145 | if (existsSync(outPath)) { 146 | log.info( 147 | `The extension ${addon.name} has already been unpacked... skipping` 148 | ) 149 | return 150 | } 151 | 152 | log.info(`Unpacking extension...`) 153 | 154 | // I do not know why, but this delay causes unzip to work reliably 155 | await delay(200) 156 | 157 | ensureDirectory(outPath) 158 | 159 | await configDispatch('unzip', { 160 | args: [windowsPathToUnix(path), '-d', windowsPathToUnix(outPath)], 161 | killOnError: true, 162 | shell: 'unix', 163 | }) 164 | } 165 | 166 | export async function generateAddonMozBuild( 167 | addon: AddonInfo & { name: string } 168 | ) { 169 | const outPath = join(ENGINE_DIR, 'browser', 'extensions', addon.name) 170 | 171 | log.info(`Generating addon mozbuild...`) 172 | 173 | const files = await walkDirectoryTree(outPath) 174 | 175 | // Because the tree has the potential of being infinitely recursive, we 176 | // cannot possibly know the the type of the tree 177 | // 178 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 179 | function runTree(tree: any, parent: string): string { 180 | if (Array.isArray(tree)) { 181 | return tree 182 | .sort() 183 | .map( 184 | (file) => 185 | `FINAL_TARGET_FILES.features["${addon.id}"]${parent} += ["${file 186 | .replace(outPath + '/', '') 187 | .replace(outPath, '')}"]` 188 | ) 189 | .join('\n') 190 | } 191 | 192 | const current = (tree['.'] as string[]) 193 | .sort() 194 | // Don't use windows path, which brick mozbuild 195 | .map((f) => windowsPathToUnix(f)) 196 | .map( 197 | (f) => 198 | `FINAL_TARGET_FILES.features["${addon.id}"]${parent} += ["${f 199 | .replace(outPath + '/', '') 200 | .replace(outPath, '')}"]` 201 | ) 202 | .join('\n') 203 | 204 | const children = Object.keys(tree) 205 | .filter((folder) => folder !== '.') 206 | .filter((folder) => typeof tree[folder] !== 'undefined') 207 | .map((folder) => runTree(tree[folder], `${parent}["${folder}"]`)) 208 | .join('\n') 209 | 210 | return `${current}\n${children}` 211 | } 212 | 213 | writeFileSync( 214 | join(outPath, 'moz.build'), 215 | ` 216 | DEFINES["MOZ_APP_VERSION"] = CONFIG["MOZ_APP_VERSION"] 217 | DEFINES["MOZ_APP_MAXVERSION"] = CONFIG["MOZ_APP_MAXVERSION"] 218 | 219 | ${runTree(files, '')}` 220 | ) 221 | } 222 | 223 | export async function initializeAddon(addon: AddonInfo & { name: string }) { 224 | const outPath = join(ENGINE_DIR, 'browser', 'extensions', addon.name) 225 | 226 | log.info(`Initializing addon...`) 227 | 228 | await configDispatch('git', { 229 | args: ['add', '-f', '.'], 230 | cwd: outPath, 231 | }) 232 | await configDispatch('git', { 233 | args: ['commit', '-m', addon.name], 234 | cwd: ENGINE_DIR, 235 | }) 236 | } 237 | 238 | export async function addAddonsToMozBuild( 239 | addons: (AddonInfo & { name: string })[] 240 | ) { 241 | log.info('Adding addons to mozbuild...') 242 | 243 | // Discard the file to make sure it has no changes 244 | await discard('browser/extensions/moz.build') 245 | 246 | const path = join(ENGINE_DIR, 'browser', 'extensions', 'moz.build') 247 | 248 | // Append all the files to the bottom 249 | writeFileSync( 250 | path, 251 | `${readFileSync(path).toString()}\nDIRS += [${addons 252 | .map((addon) => addon.name) 253 | .sort() 254 | .map((addon) => `"${addon}"`) 255 | .join(',')}]` 256 | ) 257 | } 258 | -------------------------------------------------------------------------------- /src/commands/download/firefox.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { existsSync, rmSync, writeFileSync } from 'node:fs' 3 | import { readdir } from 'node:fs/promises' 4 | import { dirname, resolve } from 'node:path' 5 | 6 | import { bin_name } from '../..' 7 | import { BASH_PATH, ENGINE_DIR, MELON_TMP_DIR } from '../../constants' 8 | import { log } from '../../log' 9 | import { commandExistsSync } from '../../utils/command-exists' 10 | import { downloadFileToLocation } from '../../utils/download' 11 | import { ensureDirectory, windowsPathToUnix } from '../../utils/fs' 12 | import { init } from '../init' 13 | import { config } from '../..' 14 | import { 15 | addAddonsToMozBuild, 16 | downloadAddon, 17 | generateAddonMozBuild, 18 | getAddons, 19 | initializeAddon, 20 | resolveAddonDownloadUrl, 21 | unpackAddon, 22 | } from './addon' 23 | import { configPath } from '../../utils' 24 | 25 | export function shouldSetupFirefoxSource() { 26 | return !( 27 | existsSync(ENGINE_DIR) && 28 | existsSync(resolve(ENGINE_DIR, 'toolkit', 'moz.build')) 29 | ) 30 | } 31 | 32 | export async function setupFirefoxSource(version: string) { 33 | const firefoxSourceTar = await downloadFirefoxSource(version) 34 | 35 | await unpackFirefoxSource(firefoxSourceTar) 36 | 37 | if (!process.env.CI_SKIP_INIT) { 38 | log.info('Init firefox') 39 | await init(ENGINE_DIR) 40 | } 41 | } 42 | 43 | async function unpackFirefoxSource(name: string): Promise { 44 | log.info(`Unpacking Firefox...`) 45 | 46 | ensureDirectory(ENGINE_DIR) 47 | 48 | let tarExec = 'tar' 49 | 50 | // On MacOS, we need to use gnu tar, otherwise tar doesn't behave how we 51 | // would expect it to behave, so this section is responsible for handling 52 | // that 53 | // 54 | // If BSD tar adds --transform support in the future, we can use that 55 | // instead 56 | if (process.platform == 'darwin') { 57 | // GNU Tar doesn't come preinstalled on any MacOS machines, so we need to 58 | // check for it and ask for the user to install it if necessary 59 | if (!commandExistsSync('gtar')) { 60 | throw new Error( 61 | `GNU Tar is required to extract Firefox's source on MacOS. Please install it using the command |brew install gnu-tar| and try again` 62 | ) 63 | } 64 | 65 | tarExec = 'gtar' 66 | } 67 | 68 | await execa( 69 | tarExec, 70 | [ 71 | '--strip-components=1', 72 | process.platform == 'win32' ? '--force-local' : undefined, 73 | '-xf', 74 | windowsPathToUnix(resolve(MELON_TMP_DIR, name)), 75 | '-C', 76 | windowsPathToUnix(ENGINE_DIR), 77 | ].filter(Boolean) as string[], 78 | { 79 | // HACK: Use bash shell on windows to get a sane version of tar that works 80 | shell: BASH_PATH || false, 81 | } 82 | ) 83 | } 84 | 85 | async function downloadFirefoxSource(version: string) { 86 | const base = `https://archive.mozilla.org/pub/firefox/releases/${version}/source/` 87 | const filename = `firefox-${version}.source.tar.xz` 88 | 89 | const url = base + filename 90 | 91 | const fsParent = MELON_TMP_DIR 92 | const fsSaveLocation = resolve(fsParent, filename) 93 | 94 | log.info(`Locating Firefox release ${version}...`) 95 | 96 | await ensureDirectory(dirname(fsSaveLocation)) 97 | 98 | if (existsSync(fsSaveLocation)) { 99 | log.info('Using cached download') 100 | return filename 101 | } 102 | 103 | // Do not re-download if there is already an existing workspace present 104 | if (existsSync(ENGINE_DIR)) 105 | log.error( 106 | `Workspace already exists.\nRemove that workspace and run |${bin_name} download ${version}| again.` 107 | ) 108 | 109 | log.info(`Downloading Firefox release ${version}...`) 110 | 111 | await downloadFileToLocation(url, resolve(MELON_TMP_DIR, filename)) 112 | return filename 113 | } 114 | 115 | export async function downloadInternals({ version, force }: { version: string, force?: boolean }) { 116 | // Provide a legible error if there is no version specified 117 | if (!version) { 118 | log.error( 119 | 'You have not specified a version of firefox in your config file. This is required to build a firefox fork.' 120 | ) 121 | process.exit(1) 122 | } 123 | 124 | if (force && existsSync(ENGINE_DIR)) { 125 | log.info('Removing existing workspace') 126 | rmSync(ENGINE_DIR, { recursive: true }) 127 | } 128 | 129 | // If the engine directory is empty, we should delete it. 130 | const engineIsEmpty = existsSync(ENGINE_DIR) && await readdir(ENGINE_DIR).then((files) => files.length === 0) 131 | if (engineIsEmpty) { 132 | log.info("'engine/' is empty, it...") 133 | rmSync(ENGINE_DIR, { recursive: true }) 134 | } 135 | 136 | if (!existsSync(ENGINE_DIR)) { 137 | await setupFirefoxSource(version) 138 | } 139 | 140 | for (const addon of getAddons()) { 141 | const downloadUrl = await resolveAddonDownloadUrl(addon) 142 | const downloadedXPI = await downloadAddon(downloadUrl, addon) 143 | 144 | await unpackAddon(downloadedXPI, addon) 145 | await generateAddonMozBuild(addon) 146 | await initializeAddon(addon) 147 | } 148 | 149 | await addAddonsToMozBuild(getAddons()) 150 | 151 | config.version.version = version 152 | writeFileSync(configPath, JSON.stringify(config, undefined, 2)) 153 | } -------------------------------------------------------------------------------- /src/commands/execute.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { existsSync } from 'node:fs' 5 | import { log } from '../log' 6 | import { ENGINE_DIR } from '../constants' 7 | import { dispatch } from '../utils' 8 | 9 | export const execute = async (cmd: string[]) => { 10 | if (existsSync(ENGINE_DIR)) { 11 | if (!cmd || cmd.length === 0) 12 | log.error('You need to specify a command to run.') 13 | 14 | const bin = cmd[0] 15 | const arguments_ = cmd 16 | arguments_.shift() 17 | 18 | log.info( 19 | `Executing \`${bin}${arguments_.length > 0 ? ` ` : ``}${arguments_.join( 20 | ' ' 21 | )}\` in \`src\`...` 22 | ) 23 | dispatch(bin, arguments_, ENGINE_DIR) 24 | } else { 25 | log.error(`Unable to locate src directory.`) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/export-file.test.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { getPatchName } from './export-file' 5 | 6 | describe('getPatchName', () => { 7 | it('works on root files', () => { 8 | const name = getPatchName('foo.js') 9 | expect(name).toBe('foo-js.patch') 10 | }) 11 | 12 | it('works on embedded files', () => { 13 | const name = getPatchName('foo/bar.js') 14 | expect(name).toBe('bar-js.patch') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/commands/export-file.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import execa from 'execa' 5 | import { existsSync, writeFileSync } from 'node:fs' 6 | import { basename, resolve } from 'node:path' 7 | import { log } from '../log' 8 | import { ENGINE_DIR, SRC_DIR } from '../constants' 9 | import { delay, ensureDirectory } from '../utils' 10 | 11 | export const getPatchName = (file: string): string => 12 | `${basename(file).replace(/\./g, '-')}.patch` 13 | 14 | export const exportFile = async (file: string): Promise => { 15 | log.info(`Exporting ${file}...`) 16 | 17 | if (!existsSync(resolve(ENGINE_DIR, file))) 18 | throw new Error( 19 | `File ${file} could not be found in engine directory. Check the path for any mistakes and try again.` 20 | ) 21 | 22 | const proc = await execa( 23 | 'git', 24 | [ 25 | 'diff', 26 | '--src-prefix=a/', 27 | '--dst-prefix=b/', 28 | '--full-index', 29 | resolve(ENGINE_DIR, file), 30 | ], 31 | { 32 | cwd: ENGINE_DIR, 33 | stripFinalNewline: false, 34 | } 35 | ) 36 | const name = getPatchName(file) 37 | const patchPath = file.replace(/\./g, '-').split('/').slice(0, -1) 38 | 39 | await ensureDirectory(resolve(SRC_DIR, ...patchPath)) 40 | 41 | if (proc.stdout.length >= 8000) { 42 | log.warning('') 43 | log.warning( 44 | `Exported patch is over 8000 characters. This patch may become hard to manage in the future.` 45 | ) 46 | log.warning( 47 | `We recommend trying to decrease your patch size by making minimal edits to the source.` 48 | ) 49 | log.warning('') 50 | await delay(2000) 51 | } 52 | 53 | writeFileSync(resolve(SRC_DIR, ...patchPath, name), proc.stdout) 54 | log.info(`Wrote "${name}" to patches directory.`) 55 | console.log() 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/ff-version.test.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import * as config from '../utils/config' 5 | import { getFFVersion } from './ff-version' 6 | 7 | describe('getFFVersion', () => { 8 | it('Returns not specified with an empty config', () => { 9 | const log = jest.spyOn(console, 'log') 10 | config.setMockRawConfig('{}') 11 | 12 | getFFVersion() 13 | 14 | expect(log).toBeCalled() 15 | expect(log).toBeCalledWith('Not Specified') 16 | 17 | log.mockRestore() 18 | }) 19 | 20 | it('Returns the version from the config', () => { 21 | const log = jest.spyOn(console, 'log') 22 | config.setMockRawConfig( 23 | `{"version": { "version": "1.2.3", "product": "firefox" }}` 24 | ) 25 | 26 | getFFVersion() 27 | 28 | expect(log).toBeCalled() 29 | expect(log).toBeCalledWith('1.2.3') 30 | 31 | log.mockRestore() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/commands/ff-version.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { getConfig } from '../utils/config' 5 | 6 | export const getFFVersion = (): void => { 7 | console.log(getConfig().version.version || 'Not Specified') 8 | } 9 | -------------------------------------------------------------------------------- /src/commands/init.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { Command } from 'commander' 5 | import { existsSync, readFileSync } from 'node:fs' 6 | import { resolve } from 'node:path' 7 | import { bin_name } from '..' 8 | import { log } from '../log' 9 | import { config, configDispatch } from '../utils' 10 | 11 | export const init = async (directory: Command | string): Promise => { 12 | const cwd = process.cwd() 13 | 14 | const absoluteInitDirectory = resolve(cwd as string, directory.toString()) 15 | 16 | if (!existsSync(absoluteInitDirectory)) { 17 | log.error( 18 | `Directory "${directory}" not found.\nCheck the directory exists and run |${bin_name} init| again.` 19 | ) 20 | } 21 | 22 | let version = readFileSync( 23 | resolve( 24 | cwd, 25 | directory.toString(), 26 | 'browser', 27 | 'config', 28 | 'version_display.txt' 29 | ) 30 | ).toString() 31 | 32 | if (!version) 33 | log.error( 34 | `Directory "${directory}" not found.\nCheck the directory exists and run |${bin_name} init| again.` 35 | ) 36 | 37 | version = version.trim().replace(/\\n/g, '') 38 | 39 | // TODO: Use bash on windows, this may significantly improve performance. 40 | // Still needs testing though 41 | log.info('Initializing git, this may take some time') 42 | 43 | await configDispatch('git', { 44 | args: ['init'], 45 | cwd: absoluteInitDirectory, 46 | shell: 'unix', 47 | }) 48 | 49 | await configDispatch('git', { 50 | args: ['init'], 51 | cwd: absoluteInitDirectory, 52 | shell: 'unix', 53 | }) 54 | 55 | await configDispatch('git', { 56 | args: ['checkout', '--orphan', version], 57 | cwd: absoluteInitDirectory, 58 | shell: 'unix', 59 | }) 60 | 61 | await configDispatch('git', { 62 | args: ['add', '-f', '.'], 63 | cwd: absoluteInitDirectory, 64 | shell: 'unix', 65 | }) 66 | 67 | log.info('Committing...') 68 | 69 | await configDispatch('git', { 70 | args: ['commit', '-aqm', `"Firefox ${version}"`], 71 | cwd: absoluteInitDirectory, 72 | shell: 'unix', 73 | }) 74 | 75 | await configDispatch('git', { 76 | args: ['checkout', '-b', config.name.toLowerCase().replace(/\s/g, '_')], 77 | cwd: absoluteInitDirectory, 78 | shell: 'unix', 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/commands/license-check.test.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import { join } from 'node:path' 6 | import { isValidLicense } from './license-check' 7 | 8 | describe('isValidLicense', () => { 9 | it('Returns true if the file contains a valid license', async () => { 10 | expect(await isValidLicense(join(__dirname, 'license-check.test.ts'))).toBe( 11 | true 12 | ) 13 | }) 14 | 15 | it('Returns false if the file contains an invalid license header', async () => { 16 | expect( 17 | await isValidLicense( 18 | join(__dirname, '../../tests/assets/invalid-license.txt') 19 | ) 20 | ).toBe(false) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/commands/license-check.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { readFile, writeFile } from 'node:fs/promises' 5 | import { join } from 'node:path' 6 | 7 | import { SRC_DIR } from '../constants' 8 | import { walkDirectory } from '../utils/fs' 9 | import { Task, TaskList } from '../utils/task-list' 10 | 11 | const ignoredFiles = new RegExp('.*\\.(json|patch|md|jpeg|png|gif|tiff|ico)') 12 | const licenseIgnore = new RegExp('(//|#) Ignore license in this file', 'g') 13 | const fixableFiles = [ 14 | { regex: new RegExp('.*\\.(j|t)s'), comment: '// ', commentClose: '\n' }, 15 | { 16 | regex: new RegExp('.*(\\.inc)?\\.css'), 17 | commentOpen: '/*\n', 18 | comment: ' * ', 19 | commentClose: '\n */', 20 | }, 21 | { 22 | regex: new RegExp('.*\\.(html|svg|xml)'), 23 | commentOpen: '', 26 | }, 27 | { 28 | regex: new RegExp('.*\\.py|moz\\.build|jar\\.mn'), 29 | comment: '# ', 30 | commentClose: '\n', 31 | }, 32 | ] 33 | 34 | export async function isValidLicense(path: string): Promise { 35 | const file = await readFile(path, { encoding: 'utf8' }) 36 | const contents = file.split('\n') 37 | 38 | // We need to grab the top 5 lines just in case there are newlines in the 39 | // comment blocks 40 | const lines = [ 41 | contents[0], 42 | contents[1], 43 | contents[2], 44 | contents[3], 45 | contents[4], 46 | ].join('\n') 47 | const hasLicense = 48 | (lines.includes('the Mozilla Public') && 49 | lines.includes('If a copy of the MPL was') && 50 | lines.includes('http://mozilla.org/MPL/2.0/')) || 51 | licenseIgnore.test(contents.join('\n')) 52 | 53 | return hasLicense 54 | } 55 | 56 | export function createTask(path: string, noFix: boolean): Task { 57 | return { 58 | skip: () => ignoredFiles.test(path), 59 | name: path.replace(SRC_DIR, ''), 60 | task: async () => { 61 | const contents = await readFile(path, { encoding: 'utf8' }) 62 | const contentsSplitNewline = contents.split('\n') 63 | const hasLicense = await isValidLicense(path) 64 | 65 | if (hasLicense) { 66 | return 67 | } 68 | 69 | const fixable = fixableFiles.find(({ regex }) => regex.test(path)) 70 | 71 | if (!fixable || noFix) { 72 | throw new Error( 73 | `${path} does not have a license. Please add the source code header` 74 | ) 75 | } 76 | 77 | const mplHeader = // eslint-disable-next-line unicorn/prefer-module 78 | await readFile(join(__dirname, 'license-check.txt'), { 79 | encoding: 'utf8', 80 | }) 81 | const { comment, commentOpen, commentClose } = fixable 82 | let header = mplHeader 83 | .split('\n') 84 | .map((ln) => (comment || '') + ln) 85 | .join('\n') 86 | 87 | if (commentOpen) { 88 | header = commentOpen + header + commentClose 89 | } 90 | 91 | await writeFile(path, header + '\n' + contentsSplitNewline.join('\n')) 92 | }, 93 | } 94 | } 95 | 96 | interface Options { 97 | fix: boolean 98 | } 99 | 100 | export const licenseCheck = async (options: Options): Promise => { 101 | const files = await walkDirectory(SRC_DIR) 102 | 103 | await new TaskList(files.map((file) => createTask(file, !options.fix))) 104 | .onError('inline') 105 | .run() 106 | } 107 | -------------------------------------------------------------------------------- /src/commands/license-check.txt: -------------------------------------------------------------------------------- 1 | This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this 3 | file, You can obtain one at http://mozilla.org/MPL/2.0/. -------------------------------------------------------------------------------- /src/commands/package.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { existsSync } from 'node:fs' 5 | import { copyFile, mkdir, readdir, unlink } from 'node:fs/promises' 6 | import { join, resolve } from 'node:path' 7 | 8 | import { bin_name, config } from '..' 9 | import { DIST_DIR, ENGINE_DIR, OBJ_DIR } from '../constants' 10 | import { log } from '../log' 11 | import { 12 | configDispatch, 13 | dispatch, 14 | dynamicConfig, 15 | windowsPathToUnix, 16 | } from '../utils' 17 | import { generateBrowserUpdateFiles } from './updates/browser' 18 | 19 | const machPath = resolve(ENGINE_DIR, 'mach') 20 | 21 | export const gluonPackage = async () => { 22 | const brandingKey = dynamicConfig.get('brand') as string 23 | const brandingDetails = config.brands[brandingKey] 24 | 25 | const version = brandingDetails.release.displayVersion 26 | const channel = brandingKey || 'unofficial' 27 | 28 | // The engine directory must have been downloaded for this to be valid 29 | // TODO: Make this a reusable function that can be used by everything 30 | if (!existsSync(ENGINE_DIR)) { 31 | log.error( 32 | `Unable to locate any source directories.\nRun |${bin_name} download| to generate the source directory.` 33 | ) 34 | } 35 | 36 | if (!existsSync(machPath)) { 37 | log.error(`Cannot locate the 'mach' binary within ${ENGINE_DIR}`) 38 | } 39 | 40 | const arguments_ = ['package'] 41 | 42 | log.info( 43 | `Packaging \`${config.binaryName}\` with args ${JSON.stringify( 44 | arguments_.slice(1, 0) 45 | )}...` 46 | ) 47 | 48 | await dispatch(machPath, arguments_, ENGINE_DIR, true) 49 | 50 | log.info('Copying results up') 51 | 52 | log.debug("Creating the dist directory if it doesn't exist") 53 | if (!existsSync(DIST_DIR)) await mkdir(DIST_DIR, { recursive: true }) 54 | 55 | log.debug('Indexing files to copy') 56 | const filesInMozillaDistrobution = await readdir(join(OBJ_DIR, 'dist'), { 57 | withFileTypes: true, 58 | }) 59 | const files = filesInMozillaDistrobution 60 | .filter((entry) => entry.isFile()) 61 | .map((entry) => entry.name) 62 | 63 | for (const file of files) { 64 | const destinationFile = join(DIST_DIR, file) 65 | log.debug(`Copying ${file}`) 66 | if (existsSync(destinationFile)) await unlink(destinationFile) 67 | await copyFile(join(OBJ_DIR, 'dist', file), destinationFile) 68 | } 69 | 70 | // Windows has some special dist files that are available within the dist 71 | // directory. 72 | if (process.platform == 'win32') { 73 | const installerDistributionDirectory = join( 74 | OBJ_DIR, 75 | 'dist', 76 | 'install', 77 | 'sea' 78 | ) 79 | 80 | if (!existsSync(installerDistributionDirectory)) { 81 | log.error( 82 | `Could not find windows installer files located at '${installerDistributionDirectory}'` 83 | ) 84 | } 85 | 86 | const installerDistributionDirectoryContents = await readdir( 87 | installerDistributionDirectory, 88 | { withFileTypes: true } 89 | ) 90 | const windowsInstallerFiles = installerDistributionDirectoryContents 91 | .filter((entry) => entry.isFile()) 92 | .map((entry) => entry.name) 93 | 94 | for (const file of windowsInstallerFiles) { 95 | let newFileName = file 96 | 97 | // There are some special cases that I want to reformat the name for 98 | if (file.includes('.installer.exe')) { 99 | newFileName = `${config.binaryName}.installer.exe` 100 | } 101 | if (file.includes('.installer-stub.exe')) { 102 | newFileName = `${config.binaryName}.installer.pretty.exe` 103 | log.warning( 104 | `The installer ${newFileName} requires that your binaries are available from the internet and everything is correctly configured. I recommend you ship '${config.binaryName}.installer.exe' if you have not set this up correctly yet` 105 | ) 106 | } 107 | 108 | // Actually copy 109 | const destinationFile = join(DIST_DIR, newFileName) 110 | log.debug(`Copying ${file}`) 111 | if (existsSync(destinationFile)) await unlink(destinationFile) 112 | await copyFile( 113 | join(installerDistributionDirectory, file), 114 | destinationFile 115 | ) 116 | } 117 | } 118 | 119 | const marPath = await createMarFile(version, channel) 120 | dynamicConfig.set('marPath', marPath) 121 | 122 | await generateBrowserUpdateFiles() 123 | 124 | log.info() 125 | log.info(`Output written to ${DIST_DIR}`) 126 | 127 | log.success('Packaging complected!') 128 | } 129 | 130 | function getCurrentBrandName(): string { 131 | const brand = dynamicConfig.get('brand') as string 132 | 133 | if (brand == 'unofficial') { 134 | return 'Nightly' 135 | } 136 | 137 | return config.brands[brand].brandFullName 138 | } 139 | 140 | async function createMarFile(version: string, channel: string) { 141 | log.info(`Creating mar file...`) 142 | let marBinary: string = windowsPathToUnix( 143 | join(OBJ_DIR, 'dist/host/bin', 'mar') 144 | ) 145 | 146 | if (process.platform == 'win32') { 147 | marBinary += '.exe' 148 | } 149 | 150 | // On macos this should be 151 | // /dist/${binaryName}/${brandFullName}.app and on everything else, 152 | // the contents of the folder /dist/${binaryName} 153 | const binary = 154 | process.platform == 'darwin' 155 | ? join(OBJ_DIR, 'dist', config.binaryName, `${getCurrentBrandName()}.app`) 156 | : join(OBJ_DIR, 'dist', config.binaryName) 157 | 158 | const marPath = windowsPathToUnix(join(DIST_DIR, 'output.mar')) 159 | await configDispatch('./tools/update-packaging/make_full_update.sh', { 160 | args: [ 161 | // The mar output location 162 | windowsPathToUnix(join(DIST_DIR)), 163 | binary, 164 | ], 165 | cwd: ENGINE_DIR, 166 | env: { 167 | MOZ_PRODUCT_VERSION: version, 168 | MAR_CHANNEL_ID: channel, 169 | MAR: marBinary, 170 | }, 171 | }) 172 | return marPath 173 | } 174 | -------------------------------------------------------------------------------- /src/commands/patches/command.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { join } from 'node:path' 5 | import { existsSync } from 'node:fs' 6 | import glob from 'tiny-glob' 7 | 8 | import { ENGINE_DIR, PATCHES_DIR, SRC_DIR } from '../../constants' 9 | import * as gitPatch from './git-patch' 10 | import * as copyPatch from './copy-patches' 11 | import * as brandingPatch from './branding-patch' 12 | import { patchCountFile } from '../../middleware/patch-check' 13 | import { checkHash } from '../../utils' 14 | import { Task, TaskList } from '../../utils/task-list' 15 | import { writeFile } from 'node:fs/promises' 16 | 17 | export interface IMelonPatch { 18 | name: string 19 | skip?: () => boolean | Promise 20 | } 21 | 22 | function patchMethod( 23 | name: string, 24 | patches: T[], 25 | patchFunction: (patch: T, index: number) => Promise 26 | ): Task { 27 | return { 28 | name: `Apply ${patches.length} ${name} patches`, 29 | long: true, 30 | task: () => 31 | new TaskList( 32 | patches.map((patch, index) => ({ 33 | name: `Apply ${patch.name}`, 34 | task: () => patchFunction(patch, index), 35 | skip: patch.skip, 36 | })) 37 | ), 38 | } 39 | } 40 | 41 | function importMelonPatches(): Task { 42 | return patchMethod( 43 | 'branding', 44 | [ 45 | ...(brandingPatch.get().map((name) => ({ 46 | type: 'branding', 47 | name, 48 | value: name, 49 | skip: async () => { 50 | const logoCheck = checkHash( 51 | join(brandingPatch.BRANDING_DIR, name, 'logo.png') 52 | ) 53 | const macosInstallerCheck = checkHash( 54 | join(brandingPatch.BRANDING_DIR, name, 'MacOSInstaller.svg') 55 | ) 56 | 57 | if ( 58 | (await logoCheck) && 59 | (await macosInstallerCheck) && 60 | existsSync(join(ENGINE_DIR, 'browser/branding', name)) 61 | ) { 62 | return true 63 | } 64 | 65 | return false 66 | }, 67 | })) as brandingPatch.IBrandingPatch[]), 68 | ], 69 | async (patch) => await brandingPatch.apply(patch.value as string) 70 | ) 71 | } 72 | 73 | async function importFolders(): Promise { 74 | return patchMethod( 75 | 'folder', 76 | await copyPatch.get(), 77 | async (patch) => await copyPatch.apply(patch.src) 78 | ) 79 | } 80 | 81 | async function importGitPatch(): Promise { 82 | let patches = await glob('**/*.patch', { 83 | filesOnly: true, 84 | cwd: SRC_DIR, 85 | }) 86 | patches = patches.map((path) => join(SRC_DIR, path)) 87 | 88 | await writeFile(patchCountFile, patches.length.toString()) 89 | 90 | return patchMethod( 91 | 'git', 92 | patches.map((path) => ({ name: path, path })), 93 | async (patch) => await gitPatch.apply(patch.path) 94 | ) 95 | } 96 | 97 | async function importInternalPatch(): Promise { 98 | const patches = await glob('*.patch', { 99 | filesOnly: true, 100 | cwd: PATCHES_DIR, 101 | }) 102 | const structuredPatches = patches.map((path) => ({ 103 | name: path, 104 | path: join(PATCHES_DIR, path), 105 | })) 106 | 107 | return patchMethod( 108 | 'gluon', 109 | structuredPatches, 110 | async (patch) => await gitPatch.apply(patch.path) 111 | ) 112 | } 113 | 114 | export async function applyPatches(): Promise { 115 | await new TaskList([ 116 | await importInternalPatch(), 117 | importMelonPatches(), 118 | await importFolders(), 119 | await importGitPatch(), 120 | ]).run() 121 | } 122 | -------------------------------------------------------------------------------- /src/commands/patches/copy-patches.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { existsSync } from 'node:fs' 5 | import { lstatSync, readFileSync } from 'node:fs' 6 | import { ensureSymlink, remove } from 'fs-extra' 7 | import { copyFile } from 'node:fs/promises' 8 | import { dirname, resolve } from 'node:path' 9 | import glob from 'tiny-glob' 10 | 11 | import { appendToFileSync, mkdirp } from '../../utils' 12 | import { config } from '../..' 13 | import { ENGINE_DIR, SRC_DIR } from '../../constants' 14 | import { IMelonPatch } from './command' 15 | 16 | // ============================================================================= 17 | // Utilities 18 | 19 | const getChunked = (location: string) => location.replace(/\\/g, '/').split('/') 20 | 21 | export const copyManual = async (name: string): Promise => { 22 | // If the file exists and is not a symlink, we want to replace it with a 23 | // symlink to our file, so remove it 24 | if ( 25 | existsSync(resolve(ENGINE_DIR, ...getChunked(name))) && 26 | !lstatSync(resolve(ENGINE_DIR, ...getChunked(name))).isSymbolicLink() 27 | ) { 28 | await remove(resolve(ENGINE_DIR, ...getChunked(name))) 29 | } 30 | 31 | if ( 32 | process.platform == 'win32' && 33 | !config.buildOptions.windowsUseSymbolicLinks 34 | ) { 35 | // Make the directory if it doesn't already exist. 36 | await mkdirp(dirname(resolve(ENGINE_DIR, ...getChunked(name)))) 37 | 38 | // By default, windows users do not have access to the permissions to create 39 | // symbolic links. As a work around, we will just copy the files instead 40 | await copyFile( 41 | resolve(SRC_DIR, ...getChunked(name)), 42 | resolve(ENGINE_DIR, ...getChunked(name)) 43 | ) 44 | } else { 45 | // Create the symlink 46 | await ensureSymlink( 47 | resolve(SRC_DIR, ...getChunked(name)), 48 | resolve(ENGINE_DIR, ...getChunked(name)) 49 | ) 50 | } 51 | 52 | const gitignore = readFileSync(resolve(ENGINE_DIR, '.gitignore')).toString() 53 | 54 | if (!gitignore.includes(getChunked(name).join('/'))) 55 | appendToFileSync( 56 | resolve(ENGINE_DIR, '.gitignore'), 57 | `\n${getChunked(name).join('/')}` 58 | ) 59 | } 60 | 61 | // ============================================================================= 62 | // Data types 63 | 64 | export interface ICopyPatch extends IMelonPatch { 65 | name: string 66 | src: string[] 67 | } 68 | 69 | // ============================================================================= 70 | // Exports 71 | 72 | export async function get(): Promise { 73 | const allFilesInSource = await glob('**/*', { 74 | filesOnly: true, 75 | cwd: SRC_DIR, 76 | }) 77 | const files = allFilesInSource.filter( 78 | (f) => !(f.endsWith('.patch') || f.split('/').includes('node_modules')) 79 | ) 80 | 81 | const manualPatches: ICopyPatch[] = [] 82 | 83 | files.map((index) => { 84 | const group = index.split('/')[0] 85 | 86 | if (!manualPatches.some((m) => m.name == group)) { 87 | manualPatches.push({ 88 | name: group, 89 | src: files.filter((f) => f.split('/')[0] == group), 90 | }) 91 | } 92 | }) 93 | 94 | return manualPatches 95 | } 96 | 97 | export async function apply(source: string[]): Promise { 98 | for (const item of source) { 99 | await copyManual(item) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/commands/patches/git-patch.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import execa from 'execa' 6 | import { PATCH_ARGS, ENGINE_DIR } from '../../constants' 7 | import { log } from '../../log' 8 | import { IMelonPatch } from './command' 9 | 10 | export interface IGitPatch extends IMelonPatch { 11 | path: string 12 | } 13 | 14 | export async function apply(path: string): Promise { 15 | try { 16 | await execa('git', ['apply', '-R', ...PATCH_ARGS, path], { 17 | cwd: ENGINE_DIR, 18 | }) 19 | } catch { 20 | // If the patch has already been applied, we want to revert it. Because 21 | // there is no good way to check this we are just going to catch and 22 | // discard the error 23 | undefined 24 | } 25 | 26 | const { stdout, exitCode } = await execa( 27 | 'git', 28 | ['apply', ...PATCH_ARGS, path], 29 | { cwd: ENGINE_DIR } 30 | ) 31 | 32 | if (exitCode != 0) log.error(stdout) 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/patches/index.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | export * from './command' 5 | -------------------------------------------------------------------------------- /src/commands/reset.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import execa from 'execa' 5 | 6 | import { bin_name } from '..' 7 | import { ENGINE_DIR } from '../constants' 8 | import { log } from '../log' 9 | 10 | export const reset = async (): Promise => { 11 | log.warning(`This will remove any changes that you have made to firefox`) 12 | log.warning( 13 | `If you have made changes to firefox's internal files, save them with |${bin_name} export [filename]|` 14 | ) 15 | await log.hardWarning( 16 | `You will need to run |${bin_name} import| to bring back your saved changes` 17 | ) 18 | 19 | log.info('Unstaging changes...') 20 | await execa('git', ['reset'], { cwd: ENGINE_DIR }) 21 | 22 | log.info('Reverting uncommitted changes...') 23 | await execa('git', ['checkout', '.'], { cwd: ENGINE_DIR }) 24 | 25 | log.info('Removing all untracked files...') 26 | await execa('git', ['clean', '-fdx'], { cwd: ENGINE_DIR }) 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/run.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { existsSync, readdirSync } from 'node:fs' 5 | import { resolve } from 'node:path' 6 | import { bin_name } from '..' 7 | import { ENGINE_DIR } from '../constants' 8 | import { log } from '../log' 9 | import { config, dispatch } from '../utils' 10 | 11 | export const run = async (chrome?: string) => { 12 | const directories = readdirSync(ENGINE_DIR) 13 | const objectDirname = directories.find((directory) => 14 | directory.startsWith('obj-') 15 | ) 16 | 17 | if (!objectDirname) { 18 | throw new Error(`${config.name} needs to be built before you can do this.`) 19 | } 20 | 21 | const objectDirectory = resolve(ENGINE_DIR, objectDirname) 22 | 23 | if (existsSync(objectDirectory)) { 24 | dispatch( 25 | './mach', 26 | ['run', ...(chrome ? ['-chrome', chrome] : [])], 27 | ENGINE_DIR, 28 | true 29 | ) 30 | } else { 31 | log.error( 32 | `Unable to locate any built binaries.\nRun |${bin_name} build| to initiate a build.` 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/set.ts: -------------------------------------------------------------------------------- 1 | import { config } from '..' 2 | import { log } from '../log' 3 | import { dynamicConfig } from '../utils' 4 | 5 | export const set = (key: string, value?: string) => { 6 | if (key == 'version') { 7 | console.log( 8 | config.brands[dynamicConfig.get('brand')].release.displayVersion 9 | ) 10 | return 11 | } 12 | 13 | if (!(key in dynamicConfig.defaultValues)) { 14 | log.warning(`The key ${key} is not found within the dynamic config options`) 15 | return 16 | } 17 | 18 | if (value) { 19 | dynamicConfig.set(key as dynamicConfig.DefaultValuesKeys, value) 20 | log.info(`Set ${key} to ${value}`) 21 | return 22 | } 23 | 24 | console.log(dynamicConfig.get(key as dynamicConfig.DefaultValuesKeys)) 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/setup-project.test.ts: -------------------------------------------------------------------------------- 1 | import { shouldSkipOptionalCopy } from './setup-project' 2 | 3 | describe('shouldSkipOptionalCopy', () => { 4 | it('Returns true if the file is not optional', () => { 5 | expect( 6 | shouldSkipOptionalCopy('something/somethingelse', ["doesn't matter"]) 7 | ).toBe(true) 8 | }) 9 | 10 | it('Returns true if the file is not in the array', () => { 11 | expect( 12 | shouldSkipOptionalCopy('something/somethingelse.optional', ['not_here']) 13 | ).toBe(true) 14 | }) 15 | 16 | it('Returns false if the file is optional and in the array', () => { 17 | expect( 18 | shouldSkipOptionalCopy('something/somethingelse.optional', ['something']) 19 | ).toBe(false) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/commands/status.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { existsSync } from 'node:fs' 5 | import { log } from '../log' 6 | import { BIN_NAME, ENGINE_DIR } from '../constants' 7 | import { dispatch, hasConfig } from '../utils' 8 | 9 | export const status = async (): Promise => { 10 | const configExists = hasConfig() 11 | const engineExists = existsSync(ENGINE_DIR) 12 | 13 | if (!configExists && !engineExists) { 14 | log.info( 15 | `Gluon doesn't appear to be setup for this project. You can set it up by running |${BIN_NAME} setup-project|` 16 | ) 17 | 18 | return 19 | } 20 | 21 | if (engineExists) { 22 | log.info("The following changes have been made to firefox's source code") 23 | await dispatch('git', ['diff'], ENGINE_DIR) 24 | 25 | return 26 | } else { 27 | log.info( 28 | `It appears that ${BIN_NAME} has been configured, but you haven't run |${BIN_NAME} download|` 29 | ) 30 | 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/update.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import { bin_name, config } from '..' 6 | import { log } from '../log' 7 | import { downloadInternals } from './download/firefox' 8 | import { getLatestFF } from '../utils' 9 | 10 | export const update = async (): Promise => { 11 | const version = await getLatestFF(config.version.product) 12 | 13 | if (version == config.version.version) { 14 | log.error(`Firefox is already the latest version.`) 15 | process.exit(1) 16 | } 17 | 18 | // We are using force here to delete the engine directory if it already 19 | // exists to make way for the new version. 20 | await downloadInternals({ version, force: true }) 21 | 22 | log.success( 23 | `Firefox has successfully been updated to ${version}.`, 24 | `You should be ready to make changes to ${config.name}.`, 25 | '', 26 | `You should import the patches next, run |${bin_name} import|.`, 27 | `To begin building ${config.name}, run |${bin_name} build|.` 28 | ) 29 | console.log() 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/updates/addons.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises' 2 | import { dirname, join } from 'node:path' 3 | import { create } from 'xmlbuilder2' 4 | import { DIST_DIR } from '../../constants' 5 | import { 6 | dynamicConfig, 7 | ensureDirectory, 8 | generateHash, 9 | getSize, 10 | } from '../../utils' 11 | import { 12 | downloadAddon, 13 | getAddons, 14 | resolveAddonDownloadUrl, 15 | } from '../download/addon' 16 | 17 | export async function generateAddonUpdateFiles() { 18 | const addons = [] 19 | 20 | for (const addon of getAddons()) { 21 | const url = await resolveAddonDownloadUrl(addon) 22 | const xpi = await downloadAddon(url, addon) 23 | 24 | addons.push({ 25 | ...addon, 26 | url, 27 | xpi, 28 | hash: await generateHash(xpi, 'sha256'), 29 | hashType: 'sha256', 30 | size: await getSize(xpi), 31 | }) 32 | } 33 | 34 | const root = create().ele('updates').ele('addons') 35 | 36 | for (const addon of addons) { 37 | const addonNode = root.ele('addon') 38 | addonNode.att('id', addon.id) 39 | addonNode.att('URL', addon.url) 40 | addonNode.att('hashFunction', addon.hashType) 41 | addonNode.att('hashValue', addon.hash) 42 | addonNode.att('size', addon.size.toString()) 43 | addonNode.att('version', addon.version) 44 | } 45 | 46 | const path = join( 47 | DIST_DIR, 48 | 'update/browser/addons', 49 | dynamicConfig.get('brand'), 50 | 'update.xml' 51 | ) 52 | 53 | await ensureDirectory(dirname(path)) 54 | await writeFile(path, root.end({ prettyPrint: true })) 55 | } 56 | -------------------------------------------------------------------------------- /src/commands/updates/browser.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs' 2 | import { readFile, writeFile } from 'node:fs/promises' 3 | import { parse } from 'ini' 4 | import { isAppleSilicon } from 'is-apple-silicon' 5 | import { dirname, join } from 'node:path' 6 | import { create } from 'xmlbuilder2' 7 | import { bin_name, config } from '../..' 8 | import { DIST_DIR, OBJ_DIR } from '../../constants' 9 | import { log } from '../../log' 10 | import { 11 | dynamicConfig, 12 | ensureEmpty, 13 | generateHash, 14 | getSize, 15 | ReleaseInfo, 16 | } from '../../utils' 17 | 18 | /** 19 | * These are all of the different platforms that aus should deploy to. Note that 20 | * the names have been simplified and they are all only the ones that are 21 | * supported by Pulse Browser. If you need something else, open an issue on gh. 22 | * 23 | * Based off the code from mozrelease: 24 | * https://searchfox.org/mozilla-central/source/python/mozrelease/mozrelease/platforms.py 25 | * https://searchfox.org/mozilla-central/source/taskcluster/gecko_taskgraph/util/partials.py 26 | */ 27 | const ausPlatformsMap = { 28 | linux64: ['Linux_x86_64-gcc3'], 29 | macosIntel: [ 30 | 'Darwin_x86_64-gcc3-u-i386-x86_64', 31 | 'Darwin_x86-gcc3-u-i386-x86_64', 32 | 'Darwin_x86-gcc3', 33 | 'Darwin_x86_64-gcc3', 34 | ], 35 | macosArm: ['Darwin_aarch64-gcc3'], 36 | win64: ['WINNT_x86_64-msvc', 'WINNT_x86_64-msvc-x64'], 37 | } 38 | 39 | export async function getPlatformConfig() { 40 | let platformINI = join(OBJ_DIR, 'dist', config.binaryName, 'platform.ini') 41 | if (!existsSync(platformINI)) 42 | platformINI = join(OBJ_DIR, 'dist', 'bin', 'platform.ini') 43 | 44 | const iniContents = await readFile(platformINI) 45 | return parse(iniContents.toString()) 46 | } 47 | 48 | function getReleaseMarName(releaseInfo: ReleaseInfo): string | undefined { 49 | if (isAppleSilicon()) { 50 | log.askForReport() 51 | log.warning('Apple silicon is not yet supported by the distribution script') 52 | return 53 | } 54 | 55 | switch (process.platform) { 56 | case 'win32': { 57 | return releaseInfo.x86?.windowsMar 58 | } 59 | case 'darwin': { 60 | return releaseInfo.x86?.macosMar 61 | } 62 | case 'linux': { 63 | return releaseInfo.x86?.linuxMar 64 | } 65 | } 66 | } 67 | 68 | function getReleaseMarURL(releaseInfo: ReleaseInfo) { 69 | const releaseMarName = getReleaseMarName(releaseInfo) 70 | let completeMarURL = `https://${config.updateHostname || 'localhost:8000'}/${ 71 | releaseMarName || 'output.mar' 72 | }` 73 | 74 | // The user is using github to distribute release binaries for this version. 75 | if (releaseInfo.github) { 76 | completeMarURL = `https://github.com/${releaseInfo.github.repo}/releases/download/${releaseInfo.displayVersion}/${releaseMarName}` 77 | log.info(`Using '${completeMarURL}' as the distribution url`) 78 | } else { 79 | log.warning( 80 | `No release information found! Default release location will be "${completeMarURL}"` 81 | ) 82 | } 83 | return completeMarURL 84 | } 85 | 86 | async function writeUpdateFileToDisk( 87 | target: string, 88 | channel: string, 89 | updateObject: { 90 | updates: { 91 | update: Record | undefined> 92 | } 93 | } 94 | ) { 95 | const xmlPath = join( 96 | DIST_DIR, 97 | 'update', 98 | 'browser', 99 | target, 100 | channel, 101 | 'update.xml' 102 | ) 103 | const document = create(updateObject) 104 | 105 | ensureEmpty(dirname(xmlPath)) 106 | await writeFile(xmlPath, document.end({ prettyPrint: true })) 107 | } 108 | 109 | function getTargets(): string[] { 110 | if (process.platform == 'win32') { 111 | return ausPlatformsMap.win64 112 | } 113 | 114 | if (process.platform == 'linux') { 115 | return ausPlatformsMap.linux64 116 | } 117 | 118 | // Everything else will have to be darwin of some kind. So, for future possible 119 | // Apple silicon support, we should chose between the two wisely 120 | // TODO: This is a hack, fix it 121 | if (isAppleSilicon()) { 122 | return ausPlatformsMap.macosArm 123 | } 124 | 125 | return ausPlatformsMap.macosIntel 126 | } 127 | 128 | export async function generateBrowserUpdateFiles() { 129 | log.info('Creating browser AUS update files') 130 | 131 | const brandingKey = dynamicConfig.get('brand') as string 132 | const channel = brandingKey 133 | const brandingDetails = config.brands[brandingKey] 134 | const releaseInfo = brandingDetails.release 135 | const { displayVersion: version } = releaseInfo 136 | 137 | const marPath = dynamicConfig.get('marPath') 138 | 139 | if (!marPath || marPath == '') { 140 | log.error( 141 | `No mar file has been built! Make sure you ran |${bin_name} package| before this command` 142 | ) 143 | return 144 | } 145 | 146 | // We need the sha512 hash of the mar file for the update file. AUS will use 147 | // this to ensure that the mar file has not been modified on the distribution 148 | // server 149 | const marHash = generateHash(marPath, 'sha512') 150 | 151 | // We need platform information, primarily for the BuildID, but other stuff 152 | // might be helpful later 153 | const platform = await getPlatformConfig() 154 | 155 | const completeMarURL = getReleaseMarURL(releaseInfo) 156 | 157 | const updateObject = { 158 | updates: { 159 | update: { 160 | // TODO: Correct update type from semvar, store the old version somewhere 161 | '@type': 'minor', 162 | '@displayVersion': version, 163 | '@appVersion': version, 164 | '@platformVersion': config.version.version, 165 | '@buildID': platform.Build.BuildID, 166 | 167 | patch: { 168 | // TODO: Partial patches might be nice for download speed 169 | '@type': 'complete', 170 | '@URL': completeMarURL, 171 | '@hashFunction': 'sha512', 172 | '@hashValue': await marHash, 173 | '@size': await getSize(marPath), 174 | }, 175 | }, 176 | }, 177 | } 178 | 179 | for (const target of getTargets()) { 180 | await writeUpdateFileToDisk(target, channel, updateObject) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import execa from 'execa' 5 | import { existsSync, mkdirSync, readdirSync } from 'node:fs' 6 | import { resolve } from 'node:path' 7 | import { log } from '../log' 8 | 9 | export const BIN_NAME = 'gluon' 10 | 11 | export const BUILD_TARGETS = ['linux', 'windows', 'macos'] 12 | export const ARCHITECTURE = ['i686', 'x86_64'] 13 | 14 | export const PATCH_ARGS = [ 15 | '--ignore-space-change', 16 | '--ignore-whitespace', 17 | '--verbose', 18 | ] 19 | 20 | export const ENGINE_DIR = resolve(process.cwd(), 'engine') 21 | export const SRC_DIR = resolve(process.cwd(), 'src') 22 | // eslint-disable-next-line unicorn/prefer-module 23 | export const PATCHES_DIR = resolve( 24 | __dirname, 25 | '../..', 26 | 'template', 27 | 'patches.optional' 28 | ) 29 | export const COMMON_DIR = resolve(process.cwd(), 'common') 30 | export const CONFIGS_DIR = resolve(process.cwd(), 'configs') 31 | export const MELON_DIR = resolve(process.cwd(), '.gluon') 32 | export const MELON_TMP_DIR = resolve(MELON_DIR, 'engine') 33 | export const DIST_DIR = resolve(process.cwd(), 'dist') 34 | 35 | mkdirSync(MELON_TMP_DIR, { recursive: true }) 36 | 37 | /** 38 | * What we think the current platform might be. Should not be used outside of this 39 | * file 40 | */ 41 | let CONFIG_GUESS = '' 42 | 43 | // We can find the current obj-* dir simply by searching. This shouldn't be to 44 | // hard and is more reliable than autoconf is. This command should only be run 45 | // if the engine directory has already been created 46 | if (existsSync(ENGINE_DIR)) { 47 | const possibleFolders = readdirSync(ENGINE_DIR, { 48 | withFileTypes: true, 49 | }) 50 | .filter((entry) => entry.isDirectory()) 51 | .map((entry) => entry.name) 52 | .filter((entry) => entry.startsWith('obj-')) 53 | .map((entry) => entry.replace('obj-', '')) 54 | 55 | if (possibleFolders.length === 0) 56 | log.debug( 57 | "There are no obj-* folders. This may mean you haven't completed a build yet" 58 | ) 59 | else if (possibleFolders.length === 1) CONFIG_GUESS = possibleFolders[0] 60 | else { 61 | log.warning( 62 | `There are multiple obj-* folders. Defaulting to obj-${possibleFolders[0]}` 63 | ) 64 | CONFIG_GUESS = possibleFolders[0] 65 | } 66 | } 67 | 68 | export const OBJ_DIR = resolve(ENGINE_DIR, `obj-${CONFIG_GUESS}`) 69 | 70 | // TODO: Remove this, it is unused 71 | export const FTL_STRING_LINE_REGEX = 72 | /(([\dA-Za-z-]*|\.[a-z-]*) =(.*|\.)|\[[\dA-Za-z]*].*(\n\s?\s?})?|\*\[[\dA-Za-z]*] .*(\n\s?\s?})?)/gm 73 | 74 | // ================= 75 | // Windows constants 76 | // ================= 77 | 78 | export let BASH_PATH: string | undefined 79 | 80 | // All windows specific code should be located inside of this if statement 81 | if (process.platform == 'win32') { 82 | const gitPath = execa.sync('where.exe git.exe').stdout.toString() 83 | 84 | if (gitPath.includes('git.exe')) { 85 | // Git is installed on the computer, the bash terminal is probably located 86 | // somewhere nearby 87 | log.debug('Fount git at ' + gitPath) 88 | 89 | log.debug(`Searching for bash`) 90 | BASH_PATH = resolve(gitPath, '../..', 'bin/bash.exe') 91 | if (!existsSync(BASH_PATH)) { 92 | log.debug(`Could not find bash at ${BASH_PATH}`) 93 | 94 | BASH_PATH = execa.sync('where.exe bash.exe').stdout.toString() 95 | if (!BASH_PATH.includes('bash.exe')) { 96 | log.error('Could not find bash, aborting') 97 | } 98 | } 99 | 100 | log.debug(`Found bash at ${BASH_PATH}`) 101 | } else { 102 | log.error( 103 | "Git doesn't appear to be installed on this computer. Please install it before continuing" 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/constants/mozconfig.ts: -------------------------------------------------------------------------------- 1 | import { config } from '..' 2 | 3 | const otherBuildModes = `# You can change to other build modes by running: 4 | # $ gluon set buildMode [dev|debug|release]` 5 | 6 | const platformOptimize = getPlatformOptimiseFlags() 7 | 8 | export const internalMozconfg = ( 9 | brand: string, 10 | buildMode: 'dev' | 'debug' | 'release' | string 11 | ) => { 12 | let buildOptions = `# Unknown build mode ${buildMode}` 13 | 14 | // Get the specific build options for the current build mode 15 | switch (buildMode) { 16 | case 'dev': { 17 | buildOptions = `# Development build settings 18 | ${otherBuildModes} 19 | ac_add_options --disable-debug` 20 | break 21 | } 22 | case 'debug': { 23 | buildOptions = `# Debug build settings 24 | ${otherBuildModes} 25 | ac_add_options --enable-debug 26 | ac_add_options --disable-optimize` 27 | break 28 | } 29 | 30 | case 'release': { 31 | buildOptions = `# Release build settings 32 | ac_add_options --disable-debug 33 | ac_add_options --enable-optimize 34 | ${platformOptimize} # Taken from waterfox` 35 | break 36 | } 37 | } 38 | 39 | return ` 40 | # ===================== 41 | # Internal gluon config 42 | # ===================== 43 | 44 | ${buildOptions} 45 | ac_add_options --disable-geckodriver 46 | ac_add_options --disable-profiling 47 | ac_add_options --disable-tests 48 | 49 | # Custom branding 50 | ac_add_options --with-branding=browser/branding/${brand} 51 | 52 | # Config for updates 53 | ac_add_options --enable-unverified-updates 54 | ac_add_options --enable-update-channel=${brand} 55 | export MOZ_APPUPDATE_HOST=${ 56 | config.updateHostname || 'localhost:7648 # This should not resolve' 57 | } 58 | ` 59 | } 60 | 61 | function getPlatformOptimiseFlags(): string { 62 | let optimiseFlags = `# Unknown platform ${process.platform}` 63 | 64 | switch (process.platform) { 65 | case 'linux': { 66 | optimiseFlags = `ac_add_options --enable-optimize="-O3 -march=haswell -mtune=haswell -w"` 67 | break 68 | } 69 | case 'darwin': { 70 | optimiseFlags = `ac_add_options --enable-optimize="-O3 -march=nehalem -mtune=haswell -w"` 71 | break 72 | } 73 | case 'win32': { 74 | optimiseFlags = `ac_add_options --enable-optimize="-O2 -Qvec -w -clang:-ftree-vectorize -clang:-msse3 -clang:-mssse3 -clang:-msse4.1 -clang:-mtune=haswell"` 75 | break 76 | } 77 | } 78 | 79 | return optimiseFlags 80 | } 81 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, v. 2.0. If a copy of the MPL was not distributed with this 5 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | import commander, { Command } from 'commander' 7 | import { existsSync, readFileSync } from 'node:fs' 8 | import { resolve } from 'node:path' 9 | 10 | import { errorHandler, config as configInited, versionFormatter } from './utils' 11 | import { commands } from './cmds' 12 | import { BIN_NAME, ENGINE_DIR } from './constants' 13 | import { updateCheck } from './middleware/update-check' 14 | import { registerCommand } from './middleware/register-command' 15 | import { log } from './log' 16 | 17 | // We have to use a dynamic require here, otherwise the typescript compiler 18 | // mucks up the directory structure 19 | // eslint-disable-next-line @typescript-eslint/no-var-requires, unicorn/prefer-module 20 | const { version: gluonVersion } = require('../package.json') 21 | 22 | export const config = configInited 23 | 24 | const program = new Command() 25 | 26 | let reportedFFVersion 27 | 28 | if (existsSync(resolve(ENGINE_DIR, 'browser', 'config', 'version.txt'))) { 29 | const version = readFileSync( 30 | resolve(ENGINE_DIR, 'browser', 'config', 'version.txt') 31 | ) 32 | .toString() 33 | .replace(/\n/g, '') 34 | 35 | if (version !== config.version.version) reportedFFVersion = version 36 | } 37 | 38 | export const bin_name = BIN_NAME 39 | 40 | const programVersions = [] 41 | 42 | for (const brand in config.brands) { 43 | const brandConfig = config.brands[brand] 44 | programVersions.push({ 45 | name: brandConfig.brandFullName, 46 | value: brandConfig.release.displayVersion, 47 | }) 48 | } 49 | 50 | program 51 | .storeOptionsAsProperties(false) 52 | .passCommandToAction(false) 53 | .name(bin_name) 54 | .option('-v, --verbose', 'Outputs extra debugging messages to the console') 55 | .version( 56 | versionFormatter([ 57 | ...programVersions, 58 | { 59 | name: 'Firefox', 60 | value: `${config.version.version} ${ 61 | reportedFFVersion ? `(being reported as ${reportedFFVersion})` : '' 62 | }`, 63 | }, 64 | { name: 'Gluon', value: gluonVersion }, 65 | reportedFFVersion 66 | ? `Mismatch detected between expected Firefox version and the actual version.\nYou may have downloaded the source code using a different version and\nthen switched to another branch.` 67 | : '', 68 | ]) 69 | ) 70 | 71 | async function middleware(command: commander.Command) { 72 | // If the program is verbose, store that fact within the logger 73 | log.isDebug = program.opts().verbose 74 | 75 | await updateCheck() 76 | registerCommand(command.name()) 77 | } 78 | 79 | for (const command of commands) { 80 | if ( 81 | command.flags && 82 | command.flags.platforms && 83 | !command.flags.platforms.includes(process.platform) 84 | ) { 85 | continue 86 | } 87 | 88 | let buildCommand = program 89 | .command(command.cmd) 90 | .description(command.description) 91 | .aliases(command?.aliases || []) 92 | 93 | // Register all of the required options 94 | for (const opt of command?.options || []) { 95 | buildCommand = buildCommand.option(opt.arg, opt.description) 96 | } 97 | 98 | buildCommand = buildCommand.action(async (...arguments_) => { 99 | // Start loading the controller in the background whilst middleware is 100 | // executing 101 | const controller = command.requestController() 102 | 103 | if (!command.disableMiddleware) { 104 | await middleware(buildCommand) 105 | } 106 | 107 | // Finish loading the controller and execute it 108 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 109 | ;(await controller)(...arguments_) 110 | }) 111 | } 112 | 113 | process 114 | .on('uncaughtException', errorHandler) 115 | .on('unhandledException', (error) => errorHandler(error, true)) 116 | 117 | program.parse(process.argv) 118 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import chalk from 'chalk' 5 | import prompts from 'prompts' 6 | 7 | const formatToDoubleDigit = (r: number) => 8 | r.toString().length == 1 ? `0${r}` : r 9 | 10 | class Log { 11 | private startTime: number 12 | 13 | _isDebug = false 14 | 15 | constructor() { 16 | const d = new Date() 17 | 18 | this.startTime = d.getTime() 19 | } 20 | 21 | getDiff(): string { 22 | const d = new Date() 23 | 24 | const currentTime = d.getTime() 25 | 26 | const elapsedTime = currentTime - this.startTime 27 | 28 | const secs = Math.floor((elapsedTime / 1000) % 60) 29 | const mins = Math.floor((elapsedTime / (60 * 1000)) % 60) 30 | const hours = Math.floor((elapsedTime / (60 * 60 * 1000)) % 24) 31 | 32 | return `${formatToDoubleDigit(hours)}:${formatToDoubleDigit( 33 | mins 34 | )}:${formatToDoubleDigit(secs)}` 35 | } 36 | 37 | set isDebug(value: boolean) { 38 | log.debug(`Logger debug mode has been ${value ? 'enabled' : 'disabled'}`) 39 | this._isDebug = value 40 | log.debug(`Logger debug mode has been ${value ? 'enabled' : 'disabled'}`) 41 | } 42 | 43 | get isDebug() { 44 | return this._isDebug 45 | } 46 | 47 | /** 48 | * A version of info that only outputs when in debug mode. 49 | * 50 | * @param args The information you want to provide to the user 51 | */ 52 | debug(...arguments_: unknown[]): void { 53 | if (this.isDebug) console.debug(...arguments_) 54 | } 55 | 56 | /** 57 | * Provides information to the user. If you intend to provide debugging 58 | * information that should be hidden unless verbose mode is enabled, use 59 | * `debug` instead. 60 | * 61 | * @param args The information you want to provide to the user 62 | */ 63 | info(...arguments_: unknown[]): void { 64 | console.info(chalk.blueBright.bold(this.getDiff()), ...arguments_) 65 | } 66 | 67 | /** 68 | * Provides text intended to be a warning to the user. If it is not critical, 69 | * for example, something is missing, but probably doesn't matter, use `info` 70 | * or even `debug` instead. 71 | * 72 | * @param args The information you want to provide to the user 73 | */ 74 | warning(...arguments_: unknown[]): void { 75 | console.warn(chalk.yellowBright.bold(' WARNING'), ...arguments_) 76 | } 77 | 78 | /** 79 | * A warning that requires the user to take an action to continue, otherwise 80 | * the process will exit. 81 | * 82 | * @param args The information you want to provide to the user 83 | */ 84 | async hardWarning(...arguments_: unknown[]): Promise { 85 | console.info('', chalk.bgRed.bold('WARNING'), ...arguments_) 86 | 87 | const { answer } = await prompts({ 88 | type: 'confirm', 89 | name: 'answer', 90 | message: 'Are you sure you want to continue?', 91 | }) 92 | 93 | if (!answer) process.exit(0) 94 | } 95 | 96 | /** 97 | * Outputs a success message to the console 98 | * 99 | * @param args The information you want to provide to the user 100 | */ 101 | success(...arguments_: unknown[]): void { 102 | console.log() 103 | console.log( 104 | `\n${chalk.greenBright.bold('SUCCESS')}`, 105 | arguments_.join('\n\t') 106 | ) 107 | } 108 | 109 | /** 110 | * Throws an error based on the input 111 | * 112 | * @param args The error you want to throw or a type that you want to convert to an error 113 | */ 114 | error(...arguments_: (Error | unknown)[]): never { 115 | throw arguments_[0] instanceof Error 116 | ? arguments_[0] 117 | : new Error( 118 | ...arguments_.map((a) => 119 | typeof a !== 'undefined' ? (a as object).toString() : a 120 | ) 121 | ) 122 | } 123 | 124 | /** 125 | * Asks for an error report to our issue tracker. Should be used in chases 126 | * where we don't think an error will occur, but we want to know if it does 127 | * to fix it 128 | */ 129 | askForReport(): void { 130 | console.info( 131 | 'The following error is a bug. Please open an issue on the gluon issue structure with a link to your repository and the output from this command.' 132 | ) 133 | console.info( 134 | 'The gluon issue tracker is located at: https://github.com/pulse-browser/gluon/issues' 135 | ) 136 | } 137 | } 138 | 139 | export const log = new Log() 140 | -------------------------------------------------------------------------------- /src/middleware/patch-check.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | /** 5 | * Responsible for checking if all new patches have been applied 6 | */ 7 | 8 | import { existsSync, readFileSync, writeFileSync } from 'node:fs' 9 | import { resolve } from 'node:path' 10 | import { log } from '../log' 11 | import { MELON_DIR, SRC_DIR } from '../constants' 12 | import { walkDirectory } from '../utils' 13 | 14 | export const patchCountFile = resolve(MELON_DIR, 'patchCount') 15 | 16 | export const patchCheck = async (): Promise => { 17 | const directoryCotnents = await walkDirectory(resolve(SRC_DIR)) 18 | 19 | const fileList = directoryCotnents.filter((file) => file.endsWith('.patch')) 20 | const patchCount = fileList.length 21 | 22 | if (!existsSync(patchCountFile)) writeFileSync(patchCountFile, '0') 23 | 24 | const recordedPatchCount = Number(readFileSync(patchCountFile).toString()) 25 | 26 | if (patchCount !== recordedPatchCount) { 27 | await log.hardWarning( 28 | 'You have not imported all of your patches. This may lead to unexpected behavior' 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/middleware/register-command.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { writeFileSync } from 'node:fs' 5 | import { resolve } from 'node:path' 6 | import { MELON_DIR } from '../constants' 7 | 8 | /** 9 | * Stores the name of the current command on the file system to be accessed if 10 | * the command crashes to provide more helpful error reporting 11 | * 12 | * @param command The name of the command about to be run 13 | */ 14 | export function registerCommand(command: string): void { 15 | writeFileSync(resolve(MELON_DIR, 'command'), command) 16 | } 17 | -------------------------------------------------------------------------------- /src/middleware/update-check.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { bin_name, config } from '..' 5 | import { log } from '../log' 6 | import { getLatestFF } from '../utils' 7 | 8 | export const updateCheck = async (): Promise => { 9 | const firefoxVersion = config.version.version 10 | 11 | try { 12 | const version = await getLatestFF(config.version.product) 13 | 14 | if (firefoxVersion && version !== firefoxVersion) 15 | log.warning( 16 | `Latest version of Firefox (${version}) does not match frozen version (${firefoxVersion}). Update Firefox with the command |${bin_name} update|.` 17 | ) 18 | } catch (error) { 19 | log.warning(`Failed to check for updates.`) 20 | log.askForReport() 21 | log.error(error) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | export interface Cmd { 5 | cmd: string 6 | description: string 7 | 8 | /** 9 | * A function that returns the controller as a promise. We want to dynamically 10 | * load them to reduce the startup time of gluon, which, at the time of 11 | * writing, is getting a touch long 12 | */ 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | requestController: () => Promise<(...arguments_: any) => void> 15 | 16 | options?: CmdOption[] 17 | aliases?: string[] 18 | flags?: { 19 | platforms?: CmdFlagPlatform[] 20 | } 21 | 22 | disableMiddleware?: boolean 23 | } 24 | 25 | export interface CmdOption { 26 | arg: string 27 | description: string 28 | } 29 | 30 | export type CmdFlagPlatform = NodeJS.Platform 31 | -------------------------------------------------------------------------------- /src/utils/change-tracking.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { readFile } from 'node:fs/promises' 5 | import { createHash } from 'node:crypto' 6 | import { readItem, writeItem } from './store' 7 | 8 | /** 9 | * Generates a hash for a file. The file must be an absolute, normalized path 10 | * @param file File to generate a hash for 11 | * @returns The generated hash 12 | */ 13 | export async function generateHash( 14 | file: string, 15 | type = 'sha1' 16 | ): Promise { 17 | return createHash(type) 18 | .update(await readFile(file)) 19 | .digest('hex') 20 | } 21 | 22 | /** 23 | * Adds the hash to the store 24 | * @param file File to check 25 | */ 26 | export async function addHash(file: string): Promise { 27 | const sha1 = await generateHash(file) 28 | 29 | // Add it to the hash file 30 | await writeItem('hashes', { 31 | ...readItem>('hashes').unwrapOr({}), 32 | [file]: sha1, 33 | }) 34 | } 35 | 36 | export async function checkHash(file: string): Promise { 37 | const hash = getHash(file) 38 | 39 | // If the hash doesn't exist, return false 40 | if (!hash) { 41 | return false 42 | } 43 | 44 | // Check if the hash matches 45 | return hash === (await generateHash(file)) 46 | } 47 | 48 | /** 49 | * Return the stored hash of a file 50 | * @param file The file path you want to get the hash from 51 | * @returns The hash of the file 52 | */ 53 | export function getHash(file: string): string { 54 | const hashes = readItem>('hashes') 55 | // We need to provide a backup in case the hash has not been created 56 | return (hashes.unwrapOr({})[file] as string) || '' 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/command-exists.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | // This file was originally under the MIT license, but is now under the MPL 2.0 5 | // license. The following license notice applies to only this file 6 | // 7 | // The MIT License (MIT) 8 | // 9 | // Copyright (c) 2014 Matthew Conlen 10 | // 11 | // Permission is hereby granted, free of charge, to any person obtaining a copy 12 | // of this software and associated documentation files (the "Software"), to deal 13 | // in the Software without restriction, including without limitation the rights 14 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | // copies of the Software, and to permit persons to whom the Software is 16 | // furnished to do so, subject to the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be included in all 19 | // copies or substantial portions of the Software. 20 | // 21 | // Adapted from the `command-exists` node module 22 | // https://github.com/mathisonian/command-exists 23 | 24 | import { execSync } from 'node:child_process' 25 | import { accessSync, constants } from 'node:fs' 26 | import path from 'node:path' 27 | 28 | const onWindows = process.platform == 'win32' 29 | 30 | const fileNotExistsSync = (commandName: string): boolean => { 31 | try { 32 | accessSync(commandName, constants.F_OK) 33 | return false 34 | } catch { 35 | return true 36 | } 37 | } 38 | 39 | const localExecutableSync = (commandName: string): boolean => { 40 | try { 41 | accessSync(commandName, constants.F_OK | constants.X_OK) 42 | return true 43 | } catch { 44 | return false 45 | } 46 | } 47 | 48 | const commandExistsUnixSync = function ( 49 | commandName: string, 50 | cleanedCommandName: string 51 | ): boolean { 52 | if (fileNotExistsSync(commandName)) { 53 | try { 54 | const stdout = execSync( 55 | 'command -v ' + 56 | cleanedCommandName + 57 | ' 2>/dev/null' + 58 | ' && { echo >&1 ' + 59 | cleanedCommandName + 60 | '; exit 0; }' 61 | ) 62 | return !!stdout 63 | } catch { 64 | return false 65 | } 66 | } 67 | return localExecutableSync(commandName) 68 | } 69 | 70 | const commandExistsWindowsSync = function ( 71 | commandName: string, 72 | cleanedCommandName: string 73 | ): boolean { 74 | // Regex from Julio from: https://stackoverflow.com/questions/51494579/regex-windows-path-validator 75 | if ( 76 | !/^(?!(?:.*\s|.*\.|\W+)$)(?:[A-Za-z]:)?(?:[^\n"*:<>?|]+(?:\/\/|\/|\\\\|\\)?)+$/m.test( 77 | commandName 78 | ) 79 | ) { 80 | return false 81 | } 82 | try { 83 | const stdout = execSync('where ' + cleanedCommandName, { stdio: [] }) 84 | return !!stdout 85 | } catch { 86 | return false 87 | } 88 | } 89 | 90 | function cleanInput(toBeCleaned: string): string { 91 | // Windows has a different cleaning process to Unix, so we should go through 92 | // that process first 93 | if (onWindows) { 94 | const isPathName = /\\/.test(toBeCleaned) 95 | if (isPathName) { 96 | const dirname = '"' + path.dirname(toBeCleaned) + '"' 97 | const basename = '"' + path.basename(toBeCleaned) + '"' 98 | return `${dirname}:${basename}` 99 | } 100 | 101 | return `"${toBeCleaned}"` 102 | } 103 | 104 | // Otherwise go through the unix cleaning process 105 | if (/[^\w/:=\\-]/.test(toBeCleaned)) { 106 | toBeCleaned = "'" + toBeCleaned.replace(/'/g, "'\\''") + "'" 107 | toBeCleaned = toBeCleaned 108 | .replace(/^(?:'')+/g, '') // unduplicate single-quote at the beginning 109 | .replace(/\\'''/g, "\\'") // remove non-escaped single-quote if there are enclosed between 2 escaped 110 | } 111 | 112 | return toBeCleaned 113 | } 114 | 115 | export function commandExistsSync(commandName: string): boolean { 116 | const cleanedCommandName = cleanInput(commandName) 117 | return onWindows ? commandExistsWindowsSync(commandName, cleanedCommandName) : commandExistsUnixSync(commandName, cleanedCommandName); 118 | } 119 | -------------------------------------------------------------------------------- /src/utils/config.test.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs' 5 | import { 6 | configPath, 7 | defaultConfig, 8 | getConfig, 9 | hasConfig, 10 | rawConfig, 11 | } from './config' 12 | 13 | export function preserveExistingConfig(): void { 14 | let configExists = false 15 | let configContents = '' 16 | 17 | beforeAll(() => { 18 | if (existsSync(configPath)) { 19 | configContents = readFileSync(configPath, 'utf8') 20 | configExists = true 21 | unlinkSync(configPath) 22 | } 23 | }) 24 | 25 | afterAll(() => { 26 | if (configExists) { 27 | writeFileSync(configPath, configContents) 28 | } 29 | }) 30 | } 31 | 32 | describe('hasConfig', () => { 33 | preserveExistingConfig() 34 | 35 | it('returns false when the config file does not exist', () => 36 | expect(hasConfig()).toBe(false)) 37 | 38 | it('returns true when the config file exists', () => { 39 | writeFileSync(configPath, '{}') 40 | expect(hasConfig()).toBe(true) 41 | unlinkSync(configPath) 42 | }) 43 | }) 44 | 45 | describe('rawConfig', () => { 46 | preserveExistingConfig() 47 | 48 | it('Returns "{}" when no config exists', () => expect(rawConfig()).toBe('{}')) 49 | 50 | it('Returns the contents of the config file', () => { 51 | writeFileSync(configPath, '{"test": "val"}') 52 | expect(rawConfig()).toBe('{"test": "val"}') 53 | unlinkSync(configPath) 54 | }) 55 | }) 56 | 57 | describe('getConfig', () => { 58 | preserveExistingConfig() 59 | 60 | it('Returns the default config when none exists', () => 61 | expect(getConfig()).toEqual(defaultConfig)) 62 | 63 | it('Returns the default config when the config is empty', () => { 64 | writeFileSync(configPath, '{}') 65 | expect(getConfig()).toEqual(defaultConfig) 66 | unlinkSync(configPath) 67 | }) 68 | 69 | it('Returns a merged config when there is a specified value', () => { 70 | writeFileSync(configPath, '{"name": "val"}') 71 | expect(getConfig()).toEqual({ ...defaultConfig, name: 'val' }) 72 | unlinkSync(configPath) 73 | }) 74 | 75 | it('Throws an error if there is invalid JSON', () => { 76 | writeFileSync(configPath, '{invalid json') 77 | expect(() => getConfig()).toThrowError() 78 | unlinkSync(configPath) 79 | }) 80 | 81 | it('Throws an error if the product is invalid', () => { 82 | writeFileSync(configPath, '{"version": {"product": "invalid"}}') 83 | expect(() => getConfig()).toThrowError() 84 | unlinkSync(configPath) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | export const delay = (delay: number): Promise => 5 | new Promise((resolve) => { 6 | setTimeout(() => resolve(true), delay) 7 | }) 8 | -------------------------------------------------------------------------------- /src/utils/dispatch.test.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { removeTimestamp } from './dispatch' 5 | 6 | describe('removeTimestamp', () => { 7 | it('removes " 0:00.00"', () => 8 | expect(removeTimestamp(' 0:00.00 This is a test')).toMatch( 9 | 'This is a test' 10 | )) 11 | 12 | it('removes " 5:05.05"', () => 13 | expect(removeTimestamp(' 5:05.05 This is a test')).toMatch( 14 | 'This is a test' 15 | )) 16 | 17 | it('removes " 10:10.10"', () => 18 | expect(removeTimestamp(' 10:10.10 This is a test')).toMatch( 19 | 'This is a test' 20 | )) 21 | 22 | it('removes "\t10:41.76"', () => 23 | expect(removeTimestamp('\t10:41.76 This is a test')).toMatch( 24 | 'This is a test' 25 | )) 26 | 27 | it('removes " 50:50.50"', () => 28 | expect(removeTimestamp(' 50:50.50 This is a test')).toMatch( 29 | 'This is a test' 30 | )) 31 | }) 32 | -------------------------------------------------------------------------------- /src/utils/dispatch.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import execa from 'execa' 6 | import { BASH_PATH } from '../constants' 7 | import { log } from '../log' 8 | 9 | export const removeTimestamp = (input: string): string => 10 | input.replace(/\s\d{1,5}:\d\d\.\d\d /g, '') 11 | 12 | export const configDispatch = ( 13 | cmd: string, 14 | config?: { 15 | args?: string[] 16 | /** 17 | * The current working directory this should be run in. Defaults to 18 | * `process.cwd()` 19 | */ 20 | cwd?: string 21 | killOnError?: boolean 22 | logger?: (data: string) => void 23 | /** 24 | * Chose what shell you should be using for the operating system 25 | */ 26 | shell?: 'default' | 'unix' 27 | env?: Record 28 | } 29 | ): Promise => { 30 | // Provide a default logger if none was specified by the user 31 | const logger = config?.logger || ((data: string) => log.info(data)) 32 | 33 | // Decide what shell we should be using. False will use the system default 34 | let shell: string | boolean = false 35 | 36 | if (config?.shell) { 37 | switch (config.shell) { 38 | // Don't change anything if we are using the default shell 39 | case 'default': { 40 | break 41 | } 42 | 43 | case 'unix': { 44 | // Bash path provides a unix shell on windows 45 | shell = BASH_PATH || false 46 | break 47 | } 48 | 49 | default: { 50 | log.error(`dispatch() does not understand the shell '${shell}'`) 51 | break 52 | } 53 | } 54 | } 55 | 56 | const handle = (data: string | Error, killOnError?: boolean) => { 57 | const dataAsString = data.toString() 58 | 59 | for (const line of dataAsString.split('\n')) { 60 | if (line.length > 0) logger(removeTimestamp(line)) 61 | } 62 | 63 | if (killOnError) { 64 | log.error('Command failed. See error above.') 65 | } 66 | } 67 | 68 | return new Promise((resolve) => { 69 | const proc = execa(cmd, config?.args, { 70 | cwd: config?.cwd || process.cwd(), 71 | shell: shell, 72 | env: { 73 | ...config?.env, 74 | ...process.env, 75 | }, 76 | }) 77 | 78 | proc.stdout?.on('data', (d) => handle(d)) 79 | proc.stderr?.on('data', (d) => handle(d)) 80 | 81 | proc.stdout?.on('error', (d) => handle(d, config?.killOnError || false)) 82 | proc.stderr?.on('error', (d) => handle(d, config?.killOnError || false)) 83 | 84 | proc.on('exit', () => { 85 | resolve(true) 86 | }) 87 | }) 88 | } 89 | 90 | /** 91 | * @deprecated Use configDispatch instead 92 | */ 93 | export const dispatch = ( 94 | cmd: string, 95 | arguments_?: string[], 96 | cwd?: string, 97 | killOnError?: boolean, 98 | logger = (data: string) => log.info(data) 99 | ): Promise => { 100 | return configDispatch(cmd, { 101 | args: arguments_, 102 | cwd: cwd, 103 | killOnError: killOnError, 104 | logger: logger, 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /src/utils/download.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { createWriteStream } from 'node:fs' 5 | 6 | import axios from 'axios' 7 | import cliProgress from 'cli-progress' 8 | import { Duplex } from 'node:stream' 9 | import { log } from '../log' 10 | 11 | export async function downloadFileToLocation( 12 | url: string, 13 | writeOutPath: string, 14 | consoleWriter?: (message: string) => void 15 | ): Promise { 16 | return new Promise((resolve, reject) => 17 | (async () => { 18 | const { data, headers } = await axios.get(url, { 19 | responseType: 'stream', 20 | }) 21 | 22 | const length = headers['content-length'] 23 | 24 | const writer = createWriteStream(writeOutPath) 25 | 26 | let receivedBytes = 0 27 | 28 | const progressBar = new cliProgress.SingleBar({ 29 | stream: consoleWriter 30 | ? new Duplex({ 31 | write: (chunk, enconding, next) => { 32 | consoleWriter(chunk.toString()) 33 | next() 34 | }, 35 | read: () => { 36 | /* Empty output */ 37 | }, 38 | }) 39 | : process.stdout, 40 | }) 41 | progressBar.start(length, receivedBytes) 42 | 43 | data.on('data', (chunk: { length: number }) => { 44 | receivedBytes += chunk.length 45 | }) 46 | data.pipe(writer) 47 | data.on('error', (error: unknown) => { 48 | log.warning( 49 | `An error occured whilst downloading ${url}. It might be ignored` 50 | ) 51 | reject(error) 52 | }) 53 | 54 | const progressInterval = setInterval( 55 | () => progressBar.update(receivedBytes), 56 | 500 57 | ) 58 | 59 | data.on('end', () => { 60 | clearInterval(progressInterval) 61 | progressBar.stop() 62 | resolve() 63 | }) 64 | })() 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/dynamic-config.test.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import * as dynamicConfig from './dynamic-config' 6 | import { readItem, removeItem } from './store' 7 | 8 | describe('set', () => { 9 | it('runs without errors', () => dynamicConfig.set('brand', 'test')) 10 | it('stores the value', () => { 11 | dynamicConfig.set('brand', 'test2') 12 | expect(readItem('dynamicConfig.brand').unwrap()).toBe('test2') 13 | }) 14 | }) 15 | 16 | describe('get', () => { 17 | it('returns a value when there is nothing', () => { 18 | removeItem('dynamicConfig.buildMode') 19 | expect(dynamicConfig.get('buildMode')).toBe('dev') 20 | }) 21 | 22 | it('returns the value just stored', () => { 23 | dynamicConfig.set('buildMode', 'debug') 24 | expect(dynamicConfig.get('buildMode')).toBe('debug') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/dynamic-config.ts: -------------------------------------------------------------------------------- 1 | // Defines config that should be set dynamically on the users system. This allows 2 | // for interfacing between these values 3 | 4 | import { log } from '../log' 5 | import { readItem, writeItem } from './store' 6 | 7 | export const defaultValues: { 8 | brand: string 9 | buildMode: 'dev' | 'debug' | 'release' 10 | marPath: string 11 | } = { 12 | brand: 'unofficial', 13 | buildMode: 'dev', 14 | marPath: '', 15 | } 16 | 17 | export type DefaultValuesType = typeof defaultValues 18 | export type DefaultValuesKeys = keyof DefaultValuesType 19 | 20 | type DynamicGetter = ( 21 | key: K 22 | ) => DefaultValuesType[K] 23 | type DynamicSetter = ( 24 | key: K, 25 | value: DefaultValuesType[K] 26 | ) => void 27 | 28 | export const get: DynamicGetter = (key) => 29 | readItem(`dynamicConfig.${key}`).unwrapOrElse(() => { 30 | log.info( 31 | `Dynamic config '${key} not set, defaulting to '${defaultValues[key]}'` 32 | ) 33 | return defaultValues[key] 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | }) as any 36 | 37 | export const set: DynamicSetter = (key, value) => 38 | writeItem(`dynamicConfig.${key}`, value) 39 | -------------------------------------------------------------------------------- /src/utils/error-handler.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import chalk from 'chalk' 5 | import { readFileSync } from 'node:fs' 6 | import { resolve } from 'node:path' 7 | import { MELON_DIR } from '../constants' 8 | import { log } from '../log' 9 | 10 | export const errorHandler = (error: Error, isUnhandledRej: boolean): never => { 11 | let cc = readFileSync(resolve(MELON_DIR, 'command')).toString() 12 | cc = cc.replace(/(\r\n|\n|\r)/gm, '') 13 | 14 | console.log( 15 | `\n ${chalk.redBright.bold( 16 | 'ERROR' 17 | )} An error occurred while running command ["${cc 18 | .split(' ') 19 | .join('", "')}"]:` 20 | ) 21 | console.log( 22 | `\n\t`, 23 | isUnhandledRej 24 | ? error.toString().replace(/\n/g, '\n\t ') 25 | : error.message.replace(/\n/g, '\n\t ') 26 | ) 27 | if (error.stack || isUnhandledRej) { 28 | const stack: string[] | undefined = error.stack?.split('\n') 29 | 30 | if (!stack) process.exit(1) 31 | 32 | stack.shift() 33 | stack.shift() 34 | console.log( 35 | `\t`, 36 | stack 37 | .join('\n') 38 | .replace(/(\r\n|\n|\r)/gm, '') 39 | .replace(/ {4}at /g, '\n\t • ') 40 | ) 41 | } 42 | 43 | console.log() 44 | log.info('Exiting due to error.') 45 | process.exit(1) 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { 5 | closeSync, 6 | existsSync, 7 | mkdirSync, 8 | openSync, 9 | rmSync, 10 | writeSync, 11 | } from 'node:fs' 12 | import { mkdir, readdir, stat } from 'node:fs/promises' 13 | import { join, isAbsolute } from 'node:path' 14 | 15 | import { log } from '../log' 16 | 17 | /** 18 | * On windows, converts a windows style path to a unix path. On unix, passes the 19 | * output through to the other side 20 | * 21 | * @param path The path that you want to be converted to a unix path 22 | * @returns A unix path 23 | */ 24 | export const windowsPathToUnix = (path: string): string => 25 | process.platform == 'win32' ? path.replace(/\\/g, '/') : path 26 | 27 | export async function walkDirectory(directory: string): Promise { 28 | const output = [] 29 | 30 | if (!isAbsolute(directory)) { 31 | log.askForReport() 32 | log.error('Please provide an absolute input to walkDirectory') 33 | } 34 | 35 | try { 36 | const directoryContents = await readdir(directory) 37 | 38 | for (const file of directoryContents) { 39 | const fullPath = join(directory, file) 40 | const fStat = await stat(fullPath) 41 | 42 | if (fStat.isDirectory()) { 43 | for (const newFile of await walkDirectory(fullPath)) { 44 | output.push(newFile) 45 | } 46 | } else { 47 | output.push(fullPath) 48 | } 49 | } 50 | } catch (error) { 51 | log.askForReport() 52 | log.error(error) 53 | } 54 | 55 | return output 56 | } 57 | 58 | export type TreeType = { [property: string]: string[] | TreeType } 59 | 60 | export async function walkDirectoryTree(directory: string): Promise { 61 | const output: TreeType = {} 62 | 63 | if (!isAbsolute(directory)) { 64 | log.askForReport() 65 | log.error('Please provide an absolute input to walkDirectory') 66 | } 67 | 68 | try { 69 | const directoryContents = await readdir(directory) 70 | 71 | const currentOut = [] 72 | 73 | for (const file of directoryContents) { 74 | const fullPath = join(directory, file) 75 | const fStat = await stat(fullPath) 76 | 77 | if (fStat.isDirectory()) { 78 | output[file] = await walkDirectoryTree(fullPath) 79 | } else { 80 | currentOut.push(fullPath) 81 | } 82 | } 83 | 84 | output['.'] = currentOut 85 | } catch (error) { 86 | log.askForReport() 87 | log.error(error) 88 | } 89 | 90 | return output 91 | } 92 | 93 | export async function ensureDirectory(directory: string): Promise { 94 | if (!existsSync(directory)) { 95 | await mkdirp(directory) 96 | } 97 | } 98 | 99 | export function mkdirp(directory: string): Promise { 100 | return mkdir(directory, { recursive: true }) 101 | } 102 | 103 | export function mkdirpSync(directory: string): string | undefined { 104 | return mkdirSync(directory, { recursive: true }) 105 | } 106 | 107 | export function appendToFileSync(fileName: string, content: string): void { 108 | const file = openSync(fileName, 'a') 109 | writeSync(file, content) 110 | closeSync(file) 111 | } 112 | 113 | export function filesExist(files: string[]): boolean { 114 | return files.every((file) => existsSync(file)) 115 | } 116 | 117 | export function ensureEmpty(path: string) { 118 | if (existsSync(path)) { 119 | rmSync(path, { recursive: true }) 120 | } 121 | 122 | mkdirSync(path, { recursive: true }) 123 | } 124 | 125 | export async function getSize(path: string): Promise { 126 | const fileInfo = await stat(path) 127 | return fileInfo.size 128 | } 129 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | export * from './command-exists' 5 | export * from './change-tracking' 6 | export * from './delay' 7 | export * from './dispatch' 8 | export * from './error-handler' 9 | export * from './version' 10 | export * from './config' 11 | export * from './string-template' 12 | export * from './fs' 13 | export * from './version-formatter' 14 | export * as dynamicConfig from './dynamic-config' 15 | -------------------------------------------------------------------------------- /src/utils/store.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs' 5 | import { join } from 'node:path' 6 | import { equip, None, OptionEquipped } from 'rustic' 7 | 8 | import { MELON_DIR } from '../constants' 9 | 10 | export const readItem = (key: string): OptionEquipped => { 11 | const fileLocation = join(MELON_DIR, `${key}.json`) 12 | 13 | if (!existsSync(fileLocation)) { 14 | return equip(None) 15 | } 16 | 17 | const data = readFileSync(fileLocation).toString() 18 | 19 | return equip(JSON.parse(data)) 20 | } 21 | 22 | export const writeItem = (key: string, data: T) => { 23 | const fileLocation = join(MELON_DIR, `${key}.json`) 24 | writeFileSync(fileLocation, JSON.stringify(data, undefined, 2)) 25 | } 26 | 27 | export const removeItem = (key: string) => { 28 | if (existsSync(join(MELON_DIR, `${key}.json`))) 29 | unlinkSync(join(MELON_DIR, `${key}.json`)) 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/string-template.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | /** 5 | * Allows for the usage of template strings inside from a file 6 | */ 7 | export function stringTemplate( 8 | template: string, 9 | variables: { [key: string]: string | number } 10 | ): string { 11 | let temporary = template 12 | 13 | for (const variable in variables) { 14 | // Replace only replaces the first instance of a string. We want to 15 | // replace all instances 16 | while (temporary.includes(`\${${variable}}`)) { 17 | temporary = temporary.replace(`\${${variable}}`, variables[variable].toString()) 18 | } 19 | } 20 | 21 | return temporary 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/task-list.ts: -------------------------------------------------------------------------------- 1 | import kleur from 'kleur' 2 | 3 | export type LoggingMode = 'silent' | 'normal' 4 | export type LoggingErrorMode = 'inline' | 'throw' 5 | 6 | export interface Task { 7 | name: string 8 | skip?: () => boolean | Promise 9 | long?: boolean 10 | task: ( 11 | log: (message: string) => void 12 | ) => void | Promise | TaskList | Promise 13 | } 14 | 15 | export class TaskList { 16 | tasks: Task[] 17 | loggingStyle: LoggingMode = 'normal' 18 | loggingIndentation = '' 19 | loggingOnError: LoggingErrorMode = 'throw' 20 | 21 | error?: Error 22 | 23 | constructor(tasks: Task[]) { 24 | this.tasks = tasks 25 | } 26 | 27 | style(style: LoggingMode) { 28 | this.loggingStyle = style 29 | return this 30 | } 31 | 32 | indent(indentation: string) { 33 | this.loggingIndentation = indentation 34 | return this 35 | } 36 | 37 | onError(mode: LoggingErrorMode) { 38 | this.loggingOnError = mode 39 | return this 40 | } 41 | 42 | private log( 43 | type: 'start' | 'finish' | 'fail' | 'skip' | 'info', 44 | name: string 45 | ) { 46 | if (this.loggingStyle == 'silent') return 47 | 48 | let prefix = this.loggingIndentation 49 | const prefixTemplate = `[${type.toUpperCase()}]` 50 | 51 | switch (type) { 52 | case 'start': { 53 | prefix += kleur.bold().gray(prefixTemplate) 54 | break 55 | } 56 | case 'finish': { 57 | prefix += kleur.bold().green(prefixTemplate) 58 | break 59 | } 60 | case 'fail': { 61 | prefix += kleur.bold().red(prefixTemplate) 62 | break 63 | } 64 | case 'skip': { 65 | prefix += kleur.bold().yellow(prefixTemplate) 66 | break 67 | } 68 | case 'info': { 69 | prefix += ' ' 70 | prefix += kleur.bold().cyan(prefixTemplate) 71 | break 72 | } 73 | } 74 | 75 | console.log(`${prefix} ${name}`) 76 | } 77 | 78 | async run() { 79 | for (const task of this.tasks) { 80 | if (task.skip && (await task.skip())) { 81 | this.log('skip', task.name) 82 | continue 83 | } 84 | 85 | if (task.long) { 86 | this.log('start', task.name) 87 | } 88 | 89 | try { 90 | const result = await task.task((message: string) => 91 | this.log('info', message) 92 | ) 93 | 94 | if (result instanceof TaskList) { 95 | // We want to provide a start point if none exists already 96 | if (!task.long) { 97 | this.log('start', task.name) 98 | } 99 | 100 | await result.indent(this.loggingIndentation + ' ').run() 101 | } 102 | } catch (error) { 103 | if (this.loggingOnError == 'throw') { 104 | this.log('fail', task.name) 105 | throw error 106 | } 107 | 108 | if (this.loggingOnError == 'inline') { 109 | this.log('fail', `${task.name}: ${error}`) 110 | } 111 | 112 | this.error = error as Error 113 | } 114 | 115 | this.log('finish', task.name) 116 | } 117 | 118 | if (this.error) { 119 | throw this.error 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/utils/version-formatter.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import chalk from 'chalk' 5 | 6 | export const versionFormatter = ( 7 | options: ({ name: string; value: string } | null | string)[] 8 | ): string => { 9 | const spacesValue = Math.max( 10 | ...options.map((argument) => 11 | typeof argument === 'string' ? 0 : argument?.value?.length || 0 12 | ) 13 | ) 14 | 15 | let versionResponse = '' 16 | 17 | for (const argument of options) { 18 | if (argument === null) { 19 | versionResponse += '\n' 20 | continue 21 | } 22 | 23 | if (typeof argument === 'string') { 24 | versionResponse += `\n${argument}\n` 25 | continue 26 | } 27 | 28 | versionResponse += `\t${chalk.bold(argument.name)} ${' '.repeat( 29 | Math.max(spacesValue - argument.name.length, 0) 30 | )} ${argument.value}\n` 31 | } 32 | 33 | return versionResponse 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/version.test.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import { validProducts } from './config' 6 | import { getLatestFF } from './version' 7 | 8 | const firefoxVersions = validProducts 9 | 10 | describe('getLatestFF', () => { 11 | for (const firefoxVersion of firefoxVersions) { 12 | it(`returns the latest ${firefoxVersion} version`, async () => 13 | expect(await getLatestFF(firefoxVersion)).toBeTruthy()) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/utils/version.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import axios from 'axios' 5 | import { log } from '../log' 6 | import { SupportedProducts } from './config' 7 | 8 | const firefoxTargets = JSON.parse(`{ 9 | "${SupportedProducts.Firefox}": "LATEST_FIREFOX_VERSION", 10 | "${SupportedProducts.FirefoxBeta}": "LATEST_FIREFOX_DEVEL_VERSION", 11 | "${SupportedProducts.FirefoxDevelopment}": "FIREFOX_DEVEDITION", 12 | "${SupportedProducts.FirefoxESR}": "FIREFOX_ESR", 13 | "${SupportedProducts.FirefoxNightly}": "FIREFOX_NIGHTLY" 14 | }`) 15 | 16 | export const getLatestFF = async ( 17 | product: SupportedProducts = SupportedProducts.Firefox 18 | ): Promise => { 19 | try { 20 | const { data } = await axios.get( 21 | 'https://product-details.mozilla.org/1.0/firefox_versions.json' 22 | ) 23 | 24 | return data[firefoxTargets[product]] 25 | } catch (error) { 26 | log.warning('Failed to get latest firefox version with error:') 27 | log.error(error) 28 | 29 | return '' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /template/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.jsm": "javascript", 4 | "moz.build": "python", 5 | "moz.configure": "python", 6 | "app.mozbuild": "python", 7 | "Makefile.in": "makefile", 8 | "mozconfig": "shellscript" 9 | }, 10 | "files.watcherExclude": { 11 | "**/.git/objects/**": true, 12 | "**/.git/subtree-cache/**": true, 13 | "**/node_modules/*/**": true, 14 | "*.tar.xz": true 15 | }, 16 | "files.exclude": { 17 | "**/node_modules": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /template/branding.optional/configure.sh: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | MOZ_APP_DISPLAYNAME="${brandFullName}" 6 | 7 | if test "$DEVELOPER_OPTIONS"; then 8 | if test "$MOZ_DEBUG"; then 9 | # Local debug builds 10 | MOZ_HANDLER_CLSID="398ffd8d-5382-48f7-9e3b-19012762d39a" 11 | MOZ_IHANDLERCONTROL_IID="a218497e-8b10-460b-b668-a92b7ee39ff2" 12 | MOZ_ASYNCIHANDLERCONTROL_IID="ca18b9ab-04b6-41be-87f7-d99913d6a2e8" 13 | MOZ_IGECKOBACKCHANNEL_IID="231c4946-4479-4c8e-aadc-8a0e48fc4c51" 14 | else 15 | # Local non-debug builds 16 | MOZ_HANDLER_CLSID="ce573faf-7815-4fc2-a031-b092268ace9e" 17 | MOZ_IHANDLERCONTROL_IID="2b715cce-1790-4fe1-aef5-48bb5acdf3a1" 18 | MOZ_ASYNCIHANDLERCONTROL_IID="8e089670-4f57-41a7-89c0-37f17482fa6f" 19 | MOZ_IGECKOBACKCHANNEL_IID="18e2488d-310f-400f-8339-0e50b513e801" 20 | fi 21 | fi 22 | -------------------------------------------------------------------------------- /template/branding.optional/locales/en-US/brand.dtd: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /template/branding.optional/locales/en-US/brand.ftl: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | ## Firefox and Mozilla Brand 6 | ## 7 | ## Firefox and Mozilla must be treated as a brand. 8 | ## 9 | ## They cannot be: 10 | ## - Transliterated. 11 | ## - Translated. 12 | ## 13 | ## Declension should be avoided where possible, leaving the original 14 | ## brand unaltered in prominent UI positions. 15 | ## 16 | ## For further details, consult: 17 | ## https://mozilla-l10n.github.io/styleguides/mozilla_general/#brands-copyright-and-trademark 18 | 19 | -brand-shorter-name = ${brandShorterName} 20 | -brand-short-name = ${brandShortName} 21 | -brand-full-name = ${brandFullName} 22 | # This brand name can be used in messages where the product name needs to 23 | # remain unchanged across different versions (Nightly, Beta, etc.). 24 | -brand-product-name = ${brandingGenericName} 25 | -vendor-short-name = ${brandingVendor} 26 | trademarkInfo = { " " } 27 | -------------------------------------------------------------------------------- /template/branding.optional/locales/en-US/brand.properties: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | brandShorterName=${brandShorterName} 6 | brandShortName=${brandShortName} 7 | brandFullName=${brandFullName} 8 | vendorShortName=${brandingVendor} 9 | -------------------------------------------------------------------------------- /template/configs/common/mozconfig: -------------------------------------------------------------------------------- 1 | ac_add_options --with-app-name=${binName} 2 | export MOZ_USER_DIR="${name}" 3 | export MOZ_APP_VENDOR="${vendor}" 4 | export MOZ_APP_BASENAME=${binName} 5 | export MOZ_APP_PROFILE=${binName} 6 | export MOZ_APP_DISPLAYNAME="${name}" 7 | export MOZ_MACBUNDLE_ID=${appId} 8 | export MOZ_DISTRIBUTION_ID=${appId} 9 | 10 | # Uncomment if builds are too resource hungry 11 | # mk_add_options MOZ_MAKE_FLAGS="-j4" 12 | # ac_add_options --enable-linker=gold 13 | 14 | # Misc 15 | export MOZ_STUB_INSTALLER=1 16 | export MOZ_INCLUDE_SOURCE_INFO=1 17 | # Change the below repository to your GitHub repository. 18 | export MOZ_SOURCE_REPO=https://github.com/example/example 19 | export MOZ_SOURCE_CHANGESET=${changeset} 20 | 21 | # Bootstrap 22 | ac_add_options --enable-bootstrap 23 | -------------------------------------------------------------------------------- /template/configs/linux/mozconfig: -------------------------------------------------------------------------------- 1 | # Optimise builds 2 | ac_add_options --enable-application=browser 3 | ac_add_options --enable-hardening 4 | ac_add_options --enable-rust-simd 5 | ac_add_options --enable-release 6 | ac_add_options --enable-optimize 7 | ac_add_options --with-ccache=sccache 8 | ac_add_options --disable-debug 9 | ac_add_options --enable-updater 10 | 11 | # Disable telemetry and tracking 12 | mk_add_options MOZ_TELEMETRY_REPORTING= 13 | mk_add_options MOZ_DATA_REPORTING= 14 | -------------------------------------------------------------------------------- /template/configs/macos/mozconfig: -------------------------------------------------------------------------------- 1 | # Optimise builds 2 | ac_add_options --enable-application=browser 3 | ac_add_options --enable-hardening 4 | ac_add_options --enable-rust-simd 5 | ac_add_options --enable-release 6 | ac_add_options --enable-optimize 7 | ac_add_options --with-ccache=sccache 8 | ac_add_options --disable-debug 9 | ac_add_options --enable-updater 10 | mk_add_options MOZ_MAKE_FLAGS="-j4" 11 | 12 | # Disable telemetry and tracking 13 | mk_add_options MOZ_TELEMETRY_REPORTING= 14 | mk_add_options MOZ_DATA_REPORTING= 15 | -------------------------------------------------------------------------------- /template/configs/windows/mozconfig: -------------------------------------------------------------------------------- 1 | ac_add_options --target=x86_64-pc-mingw32 2 | ac_add_options --enable-js-shell 3 | ac_add_options --enable-rust-simd 4 | ac_add_options --enable-crashreporter 5 | 6 | # Disable telemetry and tracking 7 | mk_add_options MOZ_TELEMETRY_REPORTING= 8 | mk_add_options MOZ_DATA_REPORTING= -------------------------------------------------------------------------------- /template/patches.optional/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/custom/linux/linux.inc.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-browser/gluon/fbdee4a6cc73b0a9b4a98951e5bc17cf377b3430/template/src/browser/themes.optional/custom/linux/linux.inc.css -------------------------------------------------------------------------------- /template/src/browser/themes.optional/custom/macos/macos.inc.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-browser/gluon/fbdee4a6cc73b0a9b4a98951e5bc17cf377b3430/template/src/browser/themes.optional/custom/macos/macos.inc.css -------------------------------------------------------------------------------- /template/src/browser/themes.optional/custom/shared/shared.inc.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-browser/gluon/fbdee4a6cc73b0a9b4a98951e5bc17cf377b3430/template/src/browser/themes.optional/custom/shared/shared.inc.css -------------------------------------------------------------------------------- /template/src/browser/themes.optional/custom/windows/windows.inc.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-browser/gluon/fbdee4a6cc73b0a9b4a98951e5bc17cf377b3430/template/src/browser/themes.optional/custom/windows/windows.inc.css -------------------------------------------------------------------------------- /template/src/browser/themes.optional/linux/browser-css.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css 2 | index ed31817d9e2be619d0d3e8f3b51a6e6d29ff22c3..711073553c5f1aff54337526985d2076facb8cba 100644 3 | --- a/browser/themes/linux/browser.css 4 | +++ b/browser/themes/linux/browser.css 5 | @@ -7,6 +7,8 @@ 6 | 7 | @namespace html url("http://www.w3.org/1999/xhtml"); 8 | 9 | +%include ../custom/linux/linux.inc.css 10 | + 11 | /** 12 | * We intentionally do not include browser-custom-colors.css, 13 | * instead choosing to fall back to system colours and transparencies 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/linux/jar-mn.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn 2 | index 404a88b218c652afac0cb2004676d22da53d48f3..5a4668ef2970dd773536907f51f3e7e7e3e023cb 100644 3 | --- a/browser/themes/linux/jar.mn 4 | +++ b/browser/themes/linux/jar.mn 5 | @@ -6,7 +6,7 @@ browser.jar: 6 | % skin browser classic/1.0 %skin/classic/browser/ 7 | #include ../shared/jar.inc.mn 8 | skin/classic/browser/sanitizeDialog.css 9 | - skin/classic/browser/browser.css 10 | +* skin/classic/browser/browser.css 11 | skin/classic/browser/contextmenu.css (../shared/contextmenu.css) 12 | skin/classic/browser/monitor-base.png 13 | skin/classic/browser/monitor-border.png 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/osx/browser-css.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css 2 | index f9860d3e66e8db7ee32d073bfe48704d16a47106..da0c36b7413aa910b2fd339e2735fcce744f5a4e 100644 3 | --- a/browser/themes/osx/browser.css 4 | +++ b/browser/themes/osx/browser.css 5 | @@ -7,6 +7,8 @@ 6 | 7 | @namespace html url("http://www.w3.org/1999/xhtml"); 8 | 9 | +%include ../custom/macos/macos.inc.css 10 | + 11 | :root { 12 | --toolbar-non-lwt-bgcolor: -moz-dialog; 13 | --toolbar-non-lwt-textcolor: -moz-dialogtext; 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/osx/jar-mn.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn 2 | index 6245c67a8d2957e301f9888603f504ce091fb70c..0288241dcb3e9e4603df36b04e1f6b476e935a30 100644 3 | --- a/browser/themes/osx/jar.mn 4 | +++ b/browser/themes/osx/jar.mn 5 | @@ -6,7 +6,7 @@ browser.jar: 6 | % skin browser classic/1.0 %skin/classic/browser/ 7 | #include ../shared/jar.inc.mn 8 | skin/classic/browser/sanitizeDialog.css 9 | - skin/classic/browser/browser.css 10 | +* skin/classic/browser/browser.css 11 | skin/classic/browser/browser-custom-colors.css (../shared/browser-custom-colors.css) 12 | skin/classic/browser/pageInfo.css 13 | skin/classic/browser/customizableui/panelUI.css (customizableui/panelUI.css) 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/shared/browser-shared-css.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css 2 | index 946807ef8623b49c2ad9c72374a38be2942ca012..1ceda5f77b57d34d841e141bf5ac531b68f44189 100644 3 | --- a/browser/themes/shared/browser-shared.css 4 | +++ b/browser/themes/shared/browser-shared.css 5 | @@ -24,6 +24,8 @@ 6 | 7 | @namespace html url("http://www.w3.org/1999/xhtml"); 8 | 9 | +%include ../custom/shared/shared.inc.css 10 | + 11 | :root { 12 | --toolbar-bgcolor: var(--toolbar-non-lwt-bgcolor); 13 | --toolbar-bgimage: var(--toolbar-non-lwt-bgimage); 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/shared/jar-inc-mn.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn 2 | index 14ef7ff731145a42b28cbb9afe192bf56f756ed5..9bfb7cdfaef3688c2064320f85b9196daf8a8215 100644 3 | --- a/browser/themes/shared/jar.inc.mn 4 | +++ b/browser/themes/shared/jar.inc.mn 5 | @@ -15,7 +15,7 @@ 6 | skin/classic/browser/addon-notification.css (../shared/addon-notification.css) 7 | skin/classic/browser/autocomplete.css (../shared/autocomplete.css) 8 | skin/classic/browser/blockedSite.css (../shared/blockedSite.css) 9 | - skin/classic/browser/browser-shared.css (../shared/browser-shared.css) 10 | +* skin/classic/browser/browser-shared.css (../shared/browser-shared.css) 11 | skin/classic/browser/ctrlTab.css (../shared/ctrlTab.css) 12 | skin/classic/browser/light-dark-overrides.css (../shared/light-dark-overrides.css) 13 | skin/classic/browser/menupanel.css (../shared/menupanel.css) 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/windows/browser-css.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css 2 | index fe2eaa0ef90733894fc026e694beae4a079d78c4..f3be10989da624b9a8b50923cbb5e583d6006726 100644 3 | --- a/browser/themes/windows/browser.css 4 | +++ b/browser/themes/windows/browser.css 5 | @@ -8,6 +8,8 @@ 6 | 7 | @namespace html url("http://www.w3.org/1999/xhtml"); 8 | 9 | +%include ../custom/windows/windows.inc.css 10 | + 11 | :root { 12 | --toolbar-non-lwt-bgcolor: -moz-dialog; 13 | --toolbar-non-lwt-textcolor: -moz-dialogtext; 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/windows/jar-mn.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn 2 | index 9133c04c3062b00ef801f18a932e0a0933625560..b993c0a7fcd6777ee64a076fd843d7c572088746 100644 3 | --- a/browser/themes/windows/jar.mn 4 | +++ b/browser/themes/windows/jar.mn 5 | @@ -6,7 +6,7 @@ browser.jar: 6 | % skin browser classic/1.0 %skin/classic/browser/ 7 | #include ../shared/jar.inc.mn 8 | skin/classic/browser/sanitizeDialog.css 9 | - skin/classic/browser/browser.css 10 | +* skin/classic/browser/browser.css 11 | skin/classic/browser/browser-custom-colors.css (../shared/browser-custom-colors.css) 12 | skin/classic/browser/browser-aero.css 13 | skin/classic/browser/contextmenu.css (../shared/contextmenu.css) 14 | -------------------------------------------------------------------------------- /tests/assets/invalid-license.txt: -------------------------------------------------------------------------------- 1 | This file doesn't contain a valid mpl license for testing reasons. -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Basic Options */ 5 | "incremental": true /* Enable incremental compilation */, 6 | "target": "es2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 8 | "lib": [ 9 | "es2020" 10 | ] /* Specify library files to be included in the compilation. */, 11 | // "allowJs": true, /* Allow javascript files to be compiled. */ 12 | // "checkJs": true, /* Report errors in .js files. */ 13 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 14 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 15 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 16 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "./dist" /* Redirect output structure to the directory. */, 19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | // "composite": true, /* Enable project compilation */ 21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 22 | // "removeComments": true, /* Do not emit comments to output. */ 23 | // "noEmit": true, /* Do not emit outputs. */ 24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 42 | /* Module Resolution Options */ 43 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */, 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | /* Advanced Options */ 62 | "skipLibCheck": true /* Skip type checking of declaration files. */, 63 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 64 | 65 | "resolveJsonModule": true 66 | }, 67 | "exclude": [ 68 | "node_modules/**/*", 69 | "**/firefox-*/**/*", 70 | "gecko", 71 | "**/engine/**/*" 72 | ] 73 | } 74 | --------------------------------------------------------------------------------