Absorb element-desktop into monorepo (#32903)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: RiotRobot <releases@riot.im>
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: Ben Banfield-Zanin <benbz@element.io>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
Co-authored-by: R Midhun Suresh <rmidhunsuresh@gmail.com>
Co-authored-by: Aditya Cherukuru <145639927+aditya-cherukuru@users.noreply.github.com>
Co-authored-by: Guilhem Saurel <guilhem.saurel@laas.fr>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
This commit is contained in:
Michael Telatynski
2026-03-25 11:19:42 +01:00
committed by GitHub
166 changed files with 20724 additions and 447 deletions
+3 -1
View File
@@ -1,6 +1,6 @@
* @element-hq/element-web-reviewers
/.github/workflows/** @element-hq/element-web-team
package.json @element-hq/element-web-team
**/package.json @element-hq/element-web-team
/pnpm-lock.yaml @element-hq/element-web-team
/apps/web/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers
@@ -26,8 +26,10 @@ package.json @element-hq/element-web-team
# Ignore translations as those will be updated by GHA for Localazy download
/apps/web/src/i18n/strings
/apps/desktop/src/i18n/strings
/packages/shared-components/src/i18n/strings
/apps/web/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
/apps/desktop/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
/packages/shared-components/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
# Ignore the synapse & mas plugins as this is updated by GHA for docker image updating
+35
View File
@@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGBzCCA++gAwIBAgIIaI6ivggL++4wDQYJKoZIhvcNAQELBQAwgZAxCzAJBgNV
BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE
CgwPU1NMIENvcnBvcmF0aW9uMUUwQwYDVQQDDDxTU0wuY29tIEVWIFJvb3QgQ2Vy
dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyIC0gRGV2ZWxvcG1lbnQwHhcNMTgw
MTE2MTIxNjM2WhcNNDMwMTE1MTIxNjM2WjCBkDELMAkGA1UEBhMCVVMxDjAMBgNV
BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9y
YXRpb24xRTBDBgNVBAMMPFNTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1
dGhvcml0eSBSU0EgUjIgLSBEZXZlbG9wbWVudDCCAiIwDQYJKoZIhvcNAQEBBQAD
ggIPADCCAgoCggIBAK/qcD65JCkueKp0+KXG2kAw8euDHuraLR3lJoUFz4ilGK1M
t+RjSuY6dHQw8ku7TnW9ejWoSFjCBSDx7tP/fzOwOxmBW6+F1NDuV/IaUtn3G2lk
CZglVk9z3n1HuWDN10xNiLoo5nzeIlvNAoDbXDGhI4Y6Z0qouAIS607JpJMWHOqZ
OUiiOuM11gI5Kz9GtVttXCjRmwlkU8WiJVIUuVedQAQt2FChrzNQewGFFi0uIau/
wFRclx6hd4JRIImC6VMJd9lcitWsqMcM94pD3fX2ozNgWX+MVlmcDYFSN9Sv8tG4
yCj4ONS8HZGzbxeyQXJhEJSi2FnBi0j6MD/d4DNFj0hCg9wz3fgVLDGCO0pNMO0Y
oXdrzfoj1/zEv0Ibgh7zKG2JHkPfapn3ExFI5d6xi66u5tPVI8cvLxqrgybRPs7Z
y1dQA7ew3LyTPAHoGtbTMvewtx1TkTtRxxhRRm0l58owqSVbSYrixFtosNobCERo
uiknaQqoY1ZDsdKsaqFoZDbntNRYhN3Ea4OPWVqDUU5ZPz9MTIRAi3MIq854yyQo
BjX9nv+kYa+Esr19pxUW0z7BWFhbXsMVpt0QMVyhwgzXvEreaZHFwHHaGb9d5x5P
VBDhsigMmtzBk9NlbCsy+uGXWHgZA/DVefueEq0sv38VoU30uYa5Tj0FLm09AgMB
AAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUI9PCucv3G9fRoTDu
ZQ4Hw6g4PkIwHQYDVR0OBBYEFCPTwrnL9xvX0aEw7mUOB8OoOD5CMA4GA1UdDwEB
/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAk43CCrC0Zbvi7YUsSePKi+KzvyQ9
mjKa5NBU/A5/sLeZS3R+wqCX7l5euYVDsUuNgNVD/QL9jNIonuHBrvKaxkmqxE1r
IcDEaUdjy2lQ0uqD7UDoS3ctrjGkPpUahrTdr3gaKcQBtUhn9v4Y2OBm6J1hDVwI
CIKcxIzRv6AUpApOtk+++m5tzDU48t8+GzrVl1hkspSYcumA+zuHllbPDL1ADdo5
kK/bBQtZrGqzPqKzeqaB1A5Wm0Igwf++7nyzdKNdjxtv907D9vg8EB4Swavuv/Ne
5/jbpI32pz0NIzzSl5ARAHuFhILsO/cEAlloDoTHzibHqFDIeU9/59HMUsJYMOtD
Ii0/LmQ6dBE4TeukCCLJwtkFYZ2eBgDjF/LHBB+z/UBs4milRgwx+Pe5UDUEjtGe
G/XMVnTSKZTy9jMaXJD5EmfP+Cfh8EEgFgjg4AmLUbEo9gXzPxyXSLgd8JGSsjg8
EV/Ri4Mmmt4XUwlSVvEOezxxDGd17gwbottCIC+rqPHonHkGmKpLMH80Bk0uOOCs
ui1oVwSifMyIcudgCcOfRLUf/f2j2NW7N7E7Vw/Zqfn+pqp/EG0KCqOM2vfJAc0s
u3rSrOJZGtB6txgtmTjoadxApWf4U/FCi3uArt6gS5MJqZjuiRNXs/K3SlSAqLGl
5UiG52ew+VdBHzE=
-----END CERTIFICATE-----
@@ -0,0 +1,313 @@
name: Build and Deploy
on:
# Nightly build
schedule:
- cron: "0 9 * * *"
# Release build
release:
types: [published]
# Manual nightly & release
workflow_dispatch:
inputs:
mode:
description: What type of build to trigger. Release builds MUST be ran from the `master` branch.
required: true
default: nightly
type: choice
options:
- nightly
- release
macos:
description: Build macOS
required: true
type: boolean
default: true
windows:
description: Build Windows
required: true
type: boolean
default: true
linux:
description: Build Linux
required: true
type: boolean
default: true
deploy:
description: Deploy artifacts
required: true
type: boolean
default: true
run-name: Element ${{ inputs.mode != 'release' && github.event_name != 'release' && 'Nightly' || 'Desktop' }}
concurrency: ${{ github.workflow }}
env:
R2_BUCKET: ${{ vars.R2_BUCKET }}
permissions: {} # Uses ELEMENT_BOT_TOKEN
jobs:
prepare:
uses: ./.github/workflows/build_desktop_prepare.yaml
permissions:
contents: read
with:
config: element.io/${{ inputs.mode || (github.event_name == 'release' && 'release') || 'nightly' }}
version: ${{ (inputs.mode != 'release' && github.event_name != 'release') && 'develop' || '' }}
nightly: ${{ inputs.mode != 'release' && github.event_name != 'release' }}
deploy: ${{ inputs.deploy || (github.event_name != 'workflow_dispatch' && github.event.release.prerelease != true) }}
secrets:
CF_R2_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
CF_R2_TOKEN: ${{ secrets.CF_R2_TOKEN }}
trigger-pro-pipeline:
name: Trigger Pro pipeline
needs: prepare
runs-on: ubuntu-24.04
steps:
- uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
repository: element-hq/element-web-pro
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
event-type: trigger-pipeline
client-payload: |-
{
"base-ref": "${{ github.ref_name }}"
}
windows:
if: github.event_name != 'workflow_dispatch' || inputs.windows
needs: prepare
name: Windows ${{ matrix.arch }}
strategy:
matrix:
arch: [x64, arm64]
uses: ./.github/workflows/build_desktop_windows.yaml
secrets: inherit # zizmor: ignore[secrets-inherit]
with:
sign: true
arch: ${{ matrix.arch }}
version: ${{ needs.prepare.outputs.nightly-version }}
macos:
if: github.event_name != 'workflow_dispatch' || inputs.macos
needs: prepare
name: macOS
uses: ./.github/workflows/build_desktop_macos.yaml
secrets: inherit # zizmor: ignore[secrets-inherit]
with:
sign: true
base-url: https://packages.element.io/${{ needs.prepare.outputs.packages-dir }}
version: ${{ needs.prepare.outputs.nightly-version }}
linux:
if: github.event_name != 'workflow_dispatch' || inputs.linux
needs: prepare
name: Linux ${{ matrix.arch }} (sqlcipher ${{ matrix.sqlcipher }})
strategy:
matrix:
arch: [amd64, arm64]
sqlcipher: [static]
uses: ./.github/workflows/build_desktop_linux.yaml
with:
arch: ${{ matrix.arch }}
sqlcipher: ${{ matrix.sqlcipher }}
version: ${{ needs.prepare.outputs.nightly-version }}
deploy:
needs:
- prepare
- macos
- linux
- windows
runs-on: ubuntu-24.04
name: ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }}
if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
environment: ${{ needs.prepare.outputs.deploy == 'true' && 'packages.element.io' || '' }}
steps:
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
- name: Prepare artifacts for deployment
run: |
set -ex
# Windows
for arch in x64 arm64
do
if [ -d "win-$arch" ]; then
mkdir -p packages.element.io/{install,update}/win32/$arch
mv win-$arch/squirrel-windows*/*.exe "packages.element.io/install/win32/$arch/"
mv win-$arch/squirrel-windows*/*.nupkg "packages.element.io/update/win32/$arch/"
mv win-$arch/squirrel-windows*/RELEASES "packages.element.io/update/win32/$arch/"
fi
done
# macOS
if [ -d macos ]; then
mkdir -p packages.element.io/{install,update}/macos
mv macos/*.dmg packages.element.io/install/macos/
mv macos/*-mac.zip packages.element.io/update/macos/
mv macos/*.json packages.element.io/update/macos/
fi
# Linux
if [ -d linux-amd64-sqlcipher-static ]; then
mkdir -p packages.element.io/install/linux/glibc-x86-64
mv linux-amd64-sqlcipher-static/*.tar.gz packages.element.io/install/linux/glibc-x86-64
fi
if [ -d linux-arm64-sqlcipher-static ]; then
mkdir -p packages.element.io/install/linux/glibc-aarch64
mv linux-arm64-sqlcipher-static/*.tar.gz packages.element.io/install/linux/glibc-aarch64
fi
# We don't wish to store the installer for every nightly ever, so we only keep the latest
- name: "[Nightly] Strip version from installer file"
if: needs.prepare.outputs.nightly-version != ''
run: |
set -ex
# Windows
for arch in x64 arm64
do
if [ -d "win-$arch" ]; then mv packages.element.io/install/win32/$arch/{*,"Element Nightly Setup"}.exe; fi
done
# macOS
if [ -d macos ]; then mv packages.element.io/install/macos/{*,"Element Nightly"}.dmg; fi
# Linux
if [ -d linux-amd64-sqlcipher-static ]; then mv packages.element.io/install/linux/glibc-x86-64/{*,element-desktop-nightly}.tar.gz; fi
if [ -d linux-arm64-sqlcipher-static ]; then mv packages.element.io/install/linux/glibc-aarch64/{*,element-desktop-nightly}.tar.gz; fi
- name: "[Release] Prepare release latest symlink"
if: needs.prepare.outputs.nightly-version == ''
run: |
set -ex
# Windows
for arch in x64 arm64
do
if [ -d "win-$arch" ]; then
pushd packages.element.io/install/win32/$arch
ln -s "$(find . -type f -iname "*.exe" | xargs -0 -n1 -- basename)" "Element Setup.exe"
popd
fi
done
# macOS
if [ -d macos ]; then
pushd packages.element.io/install/macos
ln -s "$(find . -type f -iname "*.dmg" | xargs -0 -n1 -- basename)" "Element.dmg"
popd
fi
# Linux
if [ -d linux-amd64-sqlcipher-static ]; then
pushd packages.element.io/install/linux/glibc-x86-64
ln -s "$(find . -type f -iname "*.tar.gz" | xargs -0 -n1 -- basename)" "element-desktop.tar.gz"
popd
fi
if [ -d linux-arm64-sqlcipher-static ]; then
pushd packages.element.io/install/linux/glibc-aarch64
ln -s "$(find . -type f -iname "*.tar.gz" | xargs -0 -n1 -- basename)" "element-desktop.tar.gz"
popd
fi
- name: Stash packages.element.io
if: needs.prepare.outputs.deploy == 'false'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: packages.element.io
path: packages.element.io
# Checksum algorithm specified as per https://developers.cloudflare.com/r2/examples/aws/aws-cli/
- name: Deploy artifacts
if: needs.prepare.outputs.deploy == 'true'
run: |
set -x
aws s3 cp --recursive packages.element.io/ s3://$R2_BUCKET/$DEPLOYMENT_DIR --endpoint-url $R2_URL --region auto --checksum-algorithm CRC32
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
R2_URL: ${{ vars.CF_R2_S3_API }}
DEPLOYMENT_DIR: ${{ needs.prepare.outputs.packages-dir }}
- name: Notify packages.element.io of new files
if: needs.prepare.outputs.deploy == 'true'
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: element-hq/packages.element.io
event-type: packages-index
- name: Find debs
id: deb
if: needs.linux.result == 'success'
run: |
set -x
for arch in amd64 arm64
do
echo "$arch=$(ls linux-$arch-sqlcipher-static/*.deb | tail -n1)" >> $GITHUB_OUTPUT
done
- name: Stash debs
if: needs.prepare.outputs.deploy == 'false' && needs.linux.result == 'success'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: debs
path: |
${{ steps.deb.outputs.amd64 }}
${{ steps.deb.outputs.arm64 }}
- name: Publish amd64 deb to packages.element.io
uses: element-hq/packages.element.io@master # zizmor: ignore[unpinned-uses]
if: needs.prepare.outputs.deploy == 'true' && needs.linux.result == 'success'
with:
file: ${{ steps.deb.outputs.amd64 }}
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
bucket-api: ${{ vars.CF_R2_S3_API }}
bucket-key-id: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
bucket-access-key: ${{ secrets.CF_R2_TOKEN }}
- name: Publish arm64 deb to packages.element.io
uses: element-hq/packages.element.io@master # zizmor: ignore[unpinned-uses]
if: needs.prepare.outputs.deploy == 'true' && needs.linux.result == 'success'
with:
file: ${{ steps.deb.outputs.arm64 }}
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
bucket-api: ${{ vars.CF_R2_S3_API }}
bucket-key-id: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
bucket-access-key: ${{ secrets.CF_R2_TOKEN }}
deploy-ess:
needs: deploy
runs-on: ubuntu-24.04
name: Deploy builds to ESS
if: needs.prepare.outputs.deploy == 'true' && github.event_name == 'release'
env:
BUCKET_NAME: "element-desktop-msi.onprem.element.io"
AWS_REGION: "eu-central-1"
permissions:
id-token: write # This is required for requesting the JWT
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
with:
role-to-assume: arn:aws:iam::264135176173:role/Push-ElementDesktop-MSI
role-session-name: githubaction-run-${{ github.run_id }}
aws-region: ${{ env.AWS_REGION }}
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: win-*
- name: Copy files to S3
run: |
set -x
PREFIX="${VERSION%.*}"
for file in win-*/*.msi; do
filename=$(basename "$file")
aws s3 cp "$file" "s3://${{ env.BUCKET_NAME }}/$PREFIX/$filename"
done
env:
VERSION: ${{ github.event.release.tag_name }}
@@ -0,0 +1,89 @@
name: Build and Test
on:
pull_request: {}
push:
branches: [develop, staging, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {} # No permissions required
jobs:
fetch:
uses: ./.github/workflows/build_desktop_prepare.yaml
permissions:
contents: read
with:
config: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'element.io/nightly' || 'element.io/release' }}
version: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'develop' || '' }}
branch-matching: true
windows:
needs: fetch
name: Windows
uses: ./.github/workflows/build_desktop_windows.yaml
strategy:
matrix:
arch: [x64, ia32, arm64]
with:
arch: ${{ matrix.arch }}
blob_report: true
linux:
needs: fetch
name: "Linux (${{ matrix.arch }}) (sqlcipher: ${{ matrix.sqlcipher }})"
uses: ./.github/workflows/build_desktop_linux.yaml
strategy:
matrix:
sqlcipher: [system, static]
arch: [amd64, arm64]
with:
sqlcipher: ${{ matrix.sqlcipher }}
arch: ${{ matrix.arch }}
blob_report: true
macos:
needs: fetch
name: macOS
uses: ./.github/workflows/build_desktop_macos.yaml
with:
blob_report: true
tests-done:
needs: [windows, linux, macos]
runs-on: ubuntu-24.04
if: ${{ !cancelled() }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
cache: "pnpm"
node-version: "lts/*"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: blob-report-*
path: apps/desktop/all-blob-reports
merge-multiple: true
- name: Merge into HTML Report
working-directory: apps/desktop
run: pnpm playwright merge-reports -c ./playwright.config.ts --reporter=html ./all-blob-reports
- name: Upload HTML report
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: html-report
path: apps/desktop/playwright-report
retention-days: 14
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
+279
View File
@@ -0,0 +1,279 @@
# This workflow relies on actions/cache to store the hak dependency artifacts as they take a long time to build
# Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure
# the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch.
on:
workflow_call:
inputs:
ref:
type: string
required: false
description: "The git ref to checkout, defaults to the default branch"
arch:
type: string
required: true
description: "The architecture to build for, one of 'amd64' | 'arm64'"
version:
type: string
required: false
description: "Version string to override the one in package.json, used for non-release builds"
sqlcipher:
type: string
required: true
description: "How to link sqlcipher, one of 'system' | 'static'"
blob_report:
type: boolean
required: false
description: "Whether to run the blob report"
prepare-artifact-name:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'webapp'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
+ hakHash - the hash of the .hak directory to use for cache keying
+ changelog.Debian - the changelog file to embed in the Debian package
+ variant.json - the variant configuration to use for the build
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "webapp"
test:
type: boolean
required: false
default: true
description: "Whether to run the test stage after building"
test-args:
type: string
required: false
description: "Additional arguments to pass to playwright"
runs-on:
type: string
required: false
description: "The runner image to use, normally set for you, may be needed for running in private repos."
artifact-prefix:
type: string
required: false
description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos."
default: ""
targets:
type: string
required: false
description: "List of targets to build"
default: "tar.gz deb"
env:
SQLCIPHER_BUNDLED: ${{ inputs.sqlcipher == 'static' && '1' || '' }}
MAX_GLIBC: 2.31 # bullseye-era glibc, used by glibc-check.sh
permissions: {} # No permissions required
jobs:
build:
name: Build Linux ${{ inputs.arch }} SQLCipher ${{ inputs.sqlcipher }}
# We build on native infrastructure as matrix-seshat fails to cross-compile properly
# https://github.com/matrix-org/seshat/issues/135
runs-on: ${{ inputs.runs-on || (inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04') }}
env:
HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-desktop-dockerbuild
steps:
- name: Resolve docker image tag for push
if: github.event_name == 'push'
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:$REF" >> $GITHUB_ENV
env:
REF: ${{ inputs.ref || github.ref_name }}
- name: Resolve docker image tag for release
if: github.event_name == 'release'
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:staging" >> $GITHUB_ENV
- name: Resolve docker image tag for other triggers
if: github.event_name != 'push' && github.event_name != 'release'
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:develop" >> $GITHUB_ENV
- uses: nbucic/variable-mapper@0673f6891a0619ba7c002ecfed0f9f4f39017b6f
id: config
with:
key: "${{ inputs.arch }}"
export_to: output
map: |
{
"amd64": {
"target": "x86_64-unknown-linux-gnu",
"arch": "x86-64"
},
"arm64": {
"target": "aarch64-unknown-linux-gnu",
"arch": "aarch64",
"build-args": "--arm64"
}
}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: element-hq/element-web
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.prepare-artifact-name }}
path: apps/desktop
- name: Cache .hak
id: cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
key: ${{ runner.os }}-${{ github.ref_name }}-${{ inputs.sqlcipher }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion', 'dockerbuild/*') }}
path: |
apps/desktop/.hak
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"
env:
# Workaround for https://github.com/actions/setup-node/issues/317
FORCE_COLOR: 0
- name: Install Deps
working-directory: apps/desktop
run: pnpm install --frozen-lockfile
- name: "Get modified files"
id: changed_files
if: steps.cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request' && github.repository == 'element-hq/element-web'
uses: tj-actions/changed-files@823fcebdb31bb35fdf2229d9f769b400309430d0 # v46
with:
files: |
apps/desktop/dockerbuild/**
# This allows contributors to test changes to the dockerbuild image within a pull request
- name: Build docker image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
if: steps.changed_files.outputs.any_modified == 'true'
with:
file: apps/desktop/dockerbuild/Dockerfile
context: apps/desktop
load: true
platforms: linux/${{ inputs.arch }}
tags: ${{ env.HAK_DOCKER_IMAGE }}
- name: Build Natives
if: steps.cache.outputs.cache-hit != 'true'
run: |
docker run \
-v ${{ github.workspace }}:/work -w /work \
-e SQLCIPHER_BUNDLED \
-e CI=1 \
$HAK_DOCKER_IMAGE \
pnpm -C apps/desktop run build:native
- name: Fix permissions
run: |
# For .hak
sudo chown -R $USER:$USER apps/desktop/.hak
# For node_modules pnpm strict security
sudo chmod +x node_modules/7zip-bin/linux/*/7za
- name: Check native libraries in hak dependencies
working-directory: apps/desktop
run: |
shopt -s globstar
for filename in ./.hak/hakModules/**/*.node; do
./scripts/glibc-check.sh $filename
done
- name: Generate debian files and arguments
working-directory: apps/desktop
run: |
if [ -f changelog.Debian ]; then
echo "ED_DEBIAN_CHANGELOG=changelog.Debian" >> $GITHUB_ENV
fi
- name: Build App
working-directory: apps/desktop
run: pnpm run build --publish never $BUILD_ARGS -l $TARGETS
env:
VARIANT_PATH: variant.json
# Only set for Nightly builds
VERSION: ${{ inputs.version }}
# Workaround for https://github.com/electron-userland/electron-builder/issues/5721
USE_HARD_LINKS: false
BUILD_ARGS: ${{ steps.config.outputs.build-args }}
TARGETS: ${{ inputs.targets }}
- name: Check native libraries
working-directory: apps/desktop
run: |
set -x
shopt -s globstar
FILES=$(file dist/**/*.node)
echo $FILES
! echo "$FILES" | grep -v "$ARCH"
LIBS=$(readelf -d dist/**/*.node | grep NEEDED)
echo "$LIBS"
set +x
assert_contains_string() { [[ "$1" == *"$2"* ]]; }
! assert_contains_string "$LIBS" "libcrypto.so.1.1"
if [ "$SQLCIPHER_BUNDLED" == "1" ]; then
! assert_contains_string "$LIBS" "libsqlcipher.so.0"
else
assert_contains_string "$LIBS" "libsqlcipher.so.0"
fi
./scripts/glibc-check.sh dist/linux-*unpacked/element-desktop*
env:
ARCH: ${{ steps.config.outputs.arch }}
# We exclude *-unpacked as it loses permissions and the tarball contains it with correct permissions
- name: Upload Artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: ${{ inputs.artifact-prefix }}linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
path: |
apps/desktop/dist
!apps/desktop/dist/*-unpacked/**
retention-days: 1
- name: Assert deb is present and valid
if: contains(inputs.targets, 'deb')
working-directory: apps/desktop
run: |
test -f ./dist/element-desktop*$ARCH.deb
DEB_LISTING=$(dpkg-deb --fsys-tarfile ./dist/element-desktop*.deb | tar -tv)
echo "deb listing: "
echo "$DEB_LISTING"
! echo "$DEB_LISTING" | grep '^h'
env:
ARCH: ${{ inputs.arch }}
- name: Assert tar.gz is present
if: contains(inputs.targets, 'tar.gz')
working-directory: apps/desktop
run: |
test -f ./dist/element-desktop*.tar.gz
TAR_GZ_LISTING=$(tar -tvf ./dist/element-desktop*.tar.gz)
echo "tar.gz listing: "
echo "$TAR_GZ_LISTING"
! echo "$TAR_GZ_LISTING" | grep '^h'
test:
name: Test Linux ${{ inputs.arch }} SQLCipher ${{ inputs.sqlcipher }}
needs: build
if: inputs.test && contains(inputs.targets, 'deb')
uses: ./.github/workflows/build_desktop_test.yaml
with:
project: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
artifact: ${{ inputs.artifact-prefix }}linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
runs-on: ${{ inputs.runs-on || (inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04') }}
executable: /opt/Element*/element-desktop*
prepare_cmd: |
sudo apt-get -qq update
sudo apt install ./dist/*.deb
blob_report: ${{ inputs.blob_report }}
args: ${{ inputs.test-args }}
+234
View File
@@ -0,0 +1,234 @@
# This workflow relies on actions/cache to store the hak dependency artifacts as they take a long time to build
# Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure
# the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch.
on:
workflow_call:
secrets:
APPLE_ID:
required: false
APPLE_ID_PASSWORD:
required: false
APPLE_CSC_KEY_PASSWORD:
required: false
APPLE_CSC_LINK:
required: false
inputs:
ref:
type: string
required: false
description: "The git ref to checkout, defaults to the default branch"
version:
type: string
required: false
description: "Version string to override the one in package.json, used for non-release builds"
sign:
type: string
required: false
description: "Whether to sign & notarise the build, requires 'Desktop Apple' environment"
base-url:
type: string
required: false
description: "The URL to which the output will be deployed."
blob_report:
type: boolean
required: false
description: "Whether to run the blob report"
prepare-artifact-name:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'webapp'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
+ hakHash - the hash of the .hak directory to use for cache keying
+ variant.json - the variant configuration to use for the build
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "webapp"
test:
type: boolean
required: false
default: true
description: "Whether to run the test stage after building"
test-args:
type: string
required: false
description: "Additional arguments to pass to playwright"
artifact-prefix:
type: string
required: false
description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos."
default: ""
targets:
type: string
required: false
description: "List of targets to build"
default: "dmg zip"
permissions: {} # No permissions required
jobs:
build:
name: Build macOS Universal
runs-on: macos-15 # M1
environment: ${{ inputs.sign && 'Desktop Apple' || '' }}
steps:
- uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
with:
xcode-version: latest-stable
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: element-hq/element-web
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.prepare-artifact-name }}
path: apps/desktop
- name: Cache .hak
id: cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
key: ${{ runner.os }}-${{ hashFiles('hakHash', 'electronVersion') }}
path: |
apps/desktop/.hak
- name: Install Rust
if: steps.cache.outputs.cache-hit != 'true'
run: |
rustup toolchain install stable --profile minimal --no-self-update
rustup default stable
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
# M1 macos-14 comes without Python preinstalled
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
# Install Quartz for DMG badges
# https://github.com/electron-userland/electron-builder/issues/9511#issuecomment-3774092888
- run: sudo pip3 install pyobjc-framework-Quartz
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
- name: Build Natives
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop
run: pnpm run build:native:universal
# We split these because electron-builder gets upset if we set CSC_LINK even to an empty string
- name: "[Signed] Build App"
if: inputs.sign != ''
working-directory: apps/desktop
run: |
pnpm run build:universal --publish never -m ${TARGETS}
env:
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
VARIANT_PATH: variant.json
# Only set for Nightly builds
VERSION: ${{ inputs.version }}
TARGETS: ${{ inputs.targets }}
- name: Check app was signed & notarised successfully
if: inputs.sign != ''
working-directory: apps/desktop
run: |
hdiutil attach dist/*.dmg -mountpoint /Volumes/Element
codesign -dv --verbose=4 /Volumes/Element/*.app
spctl -a -vvv -t install /Volumes/Element/*.app
hdiutil detach /Volumes/Element
- name: "[Unsigned] Build App"
if: inputs.sign == ''
working-directory: apps/desktop
run: |
pnpm run build:universal --publish never -m ${TARGETS}
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
VARIANT_PATH: variant.json
TARGETS: ${{ inputs.targets }}
- name: Generate releases.json
if: inputs.base-url
working-directory: apps/desktop
run: |
PKG_JSON_VERSION=$(cat package.json | jq -r .version)
LATEST=$(find dist -type f -iname "*-mac.zip" | xargs -0 -n1 -- basename)
# Encode spaces in the URL as Squirrel.Mac complains about bad JSON otherwise
URL="${BASE_URL}/update/macos/${LATEST// /%20}"
jq -n --arg version "${VERSION:-$PKG_JSON_VERSION}" --arg url "$URL" '
{
currentRelease: $version,
releases: [{
version: $version,
updateTo: {
version: $version,
url: $url,
},
}],
}
' > dist/releases.json
jq -n --arg url "$URL" '
{ url: $url }
' > dist/releases-legacy.json
env:
VERSION: ${{ inputs.version }}
BASE_URL: ${{ inputs.base-url }}
# We exclude mac-universal as the unpacked app takes forever to upload and zip and dmg already contains it
- name: Upload Artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: ${{ inputs.artifact-prefix }}macos
path: |
apps/desktop/dist
!apps/desktop/dist/mac-universal/**
retention-days: 1
- name: Assert zip is present
if: contains(inputs.targets, 'zip')
working-directory: apps/desktop
run: |
test -f ./dist/Element*-mac.zip
- name: Assert dmg is present
if: contains(inputs.targets, 'dmg')
working-directory: apps/desktop
run: |
test -f ./dist/Element*.dmg
test:
name: Test macOS Universal
needs: build
if: inputs.test && contains(inputs.targets, 'dmg')
uses: ./.github/workflows/build_desktop_test.yaml
with:
project: macos
artifact: ${{ inputs.artifact-prefix }}macos
runs-on: macos-14
executable: /Users/runner/Applications/Element*.app/Contents/MacOS/Element*
# We need to mount the DMG and copy the app to the Applications folder as a mounted DMG is
# read-only and thus would not allow us to override the fuses as is required for Playwright.
prepare_cmd: |
hdiutil attach ./dist/*.dmg -mountpoint /Volumes/Element &&
rsync -a /Volumes/Element/Element*.app ~/Applications/ &&
hdiutil detach /Volumes/Element
blob_report: ${{ inputs.blob_report }}
args: ${{ inputs.test-args }}
@@ -0,0 +1,194 @@
# This action helps perform common actions before the build_* actions are started in parallel.
on:
workflow_call:
inputs:
config:
type: string
required: true
description: "The config directory to use"
version:
type: string
required: false
description: "The version tag to fetch, or 'develop', will pick automatically if not passed"
nightly:
type: boolean
required: false
default: false
description: "Whether the build is a Nightly and to calculate the version strings new builds should use"
deploy:
type: boolean
required: false
default: false
description: "Whether the build should be deployed to production"
branch-matching:
type: boolean
required: false
default: false
description: "Whether the branch name should be matched to find the element-web commit"
secrets:
# Required if `nightly` is set
CF_R2_ACCESS_KEY_ID:
required: false
# Required if `nightly` is set
CF_R2_TOKEN:
required: false
outputs:
nightly-version:
description: "The version string the next Nightly should use, only output for nightly"
value: ${{ jobs.prepare.outputs.nightly-version }}
packages-dir:
description: "The directory non-deb packages for this run should live in within packages.element.io"
value: ${{ inputs.nightly && 'nightly' || 'desktop' }}
# This is just a simple pass-through of the input to simplify reuse of complex inline conditions
deploy:
description: "Whether the build should be deployed to production"
value: ${{ inputs.deploy }}
permissions: {}
jobs:
prepare:
name: Prepare
environment: ${{ inputs.nightly && 'packages.element.io' || '' }}
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
nightly-version: ${{ steps.versions.outputs.nightly }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
- name: Fetch Element Web (matching branch)
id: branch-matching
if: inputs.branch-matching
working-directory: apps/desktop
continue-on-error: true
run: |
scripts/branch-match.sh
cp "$CONFIG_DIR/config.json" element-web/
pnpm --cwd element-web install --frozen-lockfile
pnpm --cwd element-web run build
mv element-web/webapp .
pnpm run asar-webapp
env:
# These must be set for branch-match.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
CONFIG_DIR: ${{ inputs.config }}
- name: Fetch Element Web (${{ inputs.version }})
if: steps.branch-matching.outcome == 'failure' || steps.branch-matching.outcome == 'skipped'
working-directory: apps/desktop
run: pnpm run fetch --noverify -d ${CONFIG} ${VERSION}
env:
CONFIG: ${{ inputs.config }}
VERSION: ${{ inputs.version }}
- name: Copy variant config
working-directory: apps/desktop
run: cp "$CONFIG_DIR/build.json" variant.json
env:
CONFIG_DIR: ${{ inputs.config }}
# We split this out to save the build_* scripts having to do it to make use of `hashFiles` in the cache action
- name: Generate cache hash files
working-directory: apps/desktop
run: |
set -ex
# Add --no-sandbox as otherwise it fails because the helper isn't setuid root. It's only getting the version.
pnpm --silent electron --no-sandbox --version > electronVersion
cat package.json | jq -c .hakDependencies | sha1sum > hakHash
find hak -type f -print0 | xargs -0 sha1sum >> hakHash
find scripts/hak -type f -print0 | xargs -0 sha1sum >> hakHash
- name: "[Nightly] Calculate version"
id: versions
if: inputs.nightly
run: |
# Find all latest Nightly versions
aws s3 cp s3://$R2_BUCKET/nightly/update/macos/releases.json - --endpoint-url $R2_URL --region auto | jq -r .currentRelease >> VERSIONS
aws s3 cp s3://$R2_BUCKET/debian/dists/default/main/binary-amd64/Packages - --endpoint-url $R2_URL --region auto | grep "Package: element-nightly" -A 50 | grep Version -m1 | sed -n 's/Version: //p' >> VERSIONS
aws s3 cp s3://$R2_BUCKET/debian/dists/default/main/binary-arm64/Packages - --endpoint-url $R2_URL --region auto | grep "Package: element-nightly" -A 50 | grep Version -m1 | sed -n 's/Version: //p' >> VERSIONS
aws s3 cp s3://$R2_BUCKET/nightly/update/win32/x64/RELEASES - --endpoint-url $R2_URL --region auto | awk '{print $2}' | cut -d "-" -f 5 | cut -c 8- >> VERSIONS
aws s3 cp s3://$R2_BUCKET/nightly/update/win32/arm64/RELEASES - --endpoint-url $R2_URL --region auto | awk '{print $2}' | cut -d "-" -f 5 | cut -c 8- >> VERSIONS
# Pick the greatest one
VERSION=$(cat VERSIONS | sort -uf | tail -n1)
echo "Found latest nightly version $VERSION"
# Increment it
echo "nightly=$(scripts/generate-nightly-version.ts --latest $VERSION)" >> $GITHUB_OUTPUT
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
R2_BUCKET: ${{ vars.R2_BUCKET }}
R2_URL: ${{ vars.CF_R2_S3_API }}
- name: Check version
id: package
working-directory: apps/desktop
run: |
echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUT
- name: "[Release] Fetch release"
id: release
if: ${{ !inputs.nightly && inputs.version != 'develop' }}
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
env:
GITHUB_TOKEN: ${{ github.token }}
with:
tag: v${{ steps.package.outputs.version }}
- name: "[Release] Write changelog"
if: ${{ !inputs.nightly && inputs.version != 'develop' }}
working-directory: apps/desktop
run: |
TIME=$(date -d "$PUBLISHED_AT" -R)
echo "element-desktop ($VERSION) default; urgency=medium" >> changelog.Debian
echo "$BODY" | sed 's/^##/\n */g;s/^\*/ */g' | perl -pe 's/\[.+?]\((.+?)\)/\1/g' >> changelog.Debian
echo "" >> changelog.Debian
echo " -- $ACTOR <support@element.io> $TIME" >> changelog.Debian
env:
ACTOR: ${{ github.actor }}
VERSION: v${{ steps.package.outputs.version }}
BODY: ${{ steps.release.outputs.body }}
PUBLISHED_AT: ${{ steps.release.outputs.published_at }}
- name: "[Nightly] Write summary"
if: inputs.nightly
working-directory: apps/desktop
run: |
BUNDLE_HASH=$(npx asar l webapp.asar | grep /bundles/ | head -n 1 | sed 's|.*/||')
WEBAPP_VERSION=$(./scripts/get-version.ts)
WEB_VERSION=${WEBAPP_VERSION:0:12}
JS_VERSION=${WEBAPP_VERSION:16:12}
echo "### Nightly build ${NIGHTLY_VERSION}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Component | Version |" >> $GITHUB_STEP_SUMMARY
echo "| ----------- | ------- |" >> $GITHUB_STEP_SUMMARY
echo "| Bundle Hash | $BUNDLE_HASH |" >> $GITHUB_STEP_SUMMARY
echo "| Element Web | [$WEB_VERSION](https://github.com/element-hq/element-web/commit/$WEB_VERSION) |" >> $GITHUB_STEP_SUMMARY
echo "| JS SDK | [$JS_VERSION](https://github.com/matrix-org/matrix-js-sdk/commit/$JS_VERSION) |" >> $GITHUB_STEP_SUMMARY
env:
NIGHTLY_VERSION: ${{ steps.versions.outputs.nightly }}
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: webapp
retention-days: 1
path: |
apps/desktop/webapp.asar
apps/desktop/electronVersion
apps/desktop/hakHash
apps/desktop/changelog.Debian
apps/desktop/variant.json
+109
View File
@@ -0,0 +1,109 @@
# This action helps run Playwright tests within one of the build_* stages.
on:
workflow_call:
inputs:
runs-on:
type: string
required: true
description: "The runner image to use"
artifact:
type: string
required: true
description: "The name of the artifact to download"
project:
type: string
required: true
description: "The Playwright project to use for testing"
executable:
type: string
required: true
description: "Path to the executable to test"
prepare_cmd:
type: string
required: false
description: "Command to run to prepare the executable or environment for testing"
blob_report:
type: boolean
default: false
description: "Whether to upload a blob report instead of the HTML report"
args:
type: string
required: false
description: "Additional arguments to pass to playwright, for e.g. skipping specific tests"
permissions: {}
jobs:
test:
name: Test ${{ inputs.project }}
runs-on: ${{ inputs.runs-on }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ github.repository == 'element-hq/element-web-pro' && 'element-hq/element-web' || github.repository }}
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.artifact }}
path: apps/desktop/dist
- name: Prepare for tests
working-directory: apps/desktop
# This is set by the caller of the reusable workflow, they have the ability to run the command they specify
# directly without our help so this is fine.
run: ${{ inputs.prepare_cmd }} # zizmor: ignore[template-injection]
if: inputs.prepare_cmd
- name: Expand executable path
id: executable
working-directory: apps/desktop
shell: bash
env:
EXECUTABLE: ${{ inputs.executable }}
run: |
FILES=($EXECUTABLE)
echo "path=${FILES[0]}" >> $GITHUB_OUTPUT
# We previously disabled the `EnableNodeCliInspectArguments` fuse, but Playwright requires
# it to be enabled to test Electron apps, so turn it back on.
- name: Set EnableNodeCliInspectArguments fuse enabled
run: $RUN_AS npx @electron/fuses write --app "$EXECUTABLE" EnableNodeCliInspectArguments=on
working-directory: apps/desktop
shell: bash
env:
# We need sudo on Linux as it is installed in /opt/
RUN_AS: ${{ runner.os == 'Linux' && 'sudo' || '' }}
EXECUTABLE: ${{ steps.executable.outputs.path }}
- name: Run tests
uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a
timeout-minutes: 20
with:
run: pnpm -C apps/desktop test --project=${{ inputs.project }} ${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} ${{ inputs.blob_report == false && '--reporter=html' || '' }} ${{ inputs.args }}
env:
ELEMENT_DESKTOP_EXECUTABLE: ${{ steps.executable.outputs.path }}
- name: Upload blob report
if: always() && inputs.blob_report
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: blob-report-${{ inputs.artifact }}
path: apps/desktop/blob-report
retention-days: 1
- name: Upload HTML report
if: always() && inputs.blob_report == false
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: ${{ inputs.artifact }}-test
path: apps/desktop/playwright-report
retention-days: 14
@@ -0,0 +1,314 @@
# This workflow relies on actions/cache to store the hak dependency artifacts as they take a long time to build
# Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure
# the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch.
# Windows GHA runner by default uses the pwsh shell which breaks codeSigningCert in the workflow
# We always sign using eSignerCKA to ensure it keeps working, but aside from release & nightlies we use demo credentials
# which do not yield trusted signatures.
defaults:
run:
shell: powershell
on:
workflow_call:
secrets:
ESIGNER_USER_NAME:
required: false
ESIGNER_USER_PASSWORD:
required: false
ESIGNER_USER_TOTP:
required: false
inputs:
ref:
type: string
required: false
description: "The git ref to checkout, defaults to the default branch"
arch:
type: string
required: true
description: "The architecture to build for, one of 'x64' | 'ia32' | 'arm64'"
version:
type: string
required: false
description: "Version string to override the one in package.json, used for non-release builds"
sign:
type: string
required: false
description: "Whether to sign & notarise the build, requires 'Desktop eSigner' environment"
blob_report:
type: boolean
required: false
description: "Whether to run the blob report"
prepare-artifact-name:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'webapp'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
+ hakHash - the hash of the .hak directory to use for cache keying
+ variant.json - the variant configuration to use for the build
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "webapp"
test:
type: boolean
required: false
default: true
description: "Whether to run the test stage after building"
test-runs-on:
type: string
required: false
description: "The runner image to use for testing, normally set for you, may be needed for running in private repos."
test-args:
type: string
required: false
description: "Additional arguments to pass to playwright"
artifact-prefix:
type: string
required: false
description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos."
default: ""
targets:
type: string
required: false
description: "List of targets to build"
default: "squirrel msi"
permissions: {} # No permissions required
jobs:
build:
name: Build Windows ${{ inputs.arch }}
runs-on: windows-2025
environment: ${{ inputs.sign && 'Desktop eSigner' || '' }}
env:
SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0/x86/signtool.exe"
steps:
- uses: nbucic/variable-mapper@0673f6891a0619ba7c002ecfed0f9f4f39017b6f
id: config
with:
key: "${{ inputs.arch }}"
export_to: output
map: |
{
"x64": {
"target": "x86_64-pc-windows-msvc"
},
"arm64": {
"target": "aarch64-pc-windows-msvc",
"build-args": "--arm64",
"arch": "amd64_arm64"
},
"ia32": {
"target": "i686-pc-windows-msvc",
"build-args": "--ia32",
"arch": "x86",
"extra_config": "{\"user_notice\": {\"title\": \"Your desktop environment is unsupported.\",\"description\": \"Support for 32-bit Windows installations has ended. Transition to the web or mobile app for continued access.\"}}"
}
}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: element-hq/element-web
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.prepare-artifact-name }}
path: apps/desktop/
- name: Cache .hak
id: cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
key: ${{ runner.os }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion') }}
path: |
apps/desktop/.hak
# ActiveTCL package on choco is from 2015,
# this one is newer but includes more than we need
- name: Choco install tclsh
if: steps.cache.outputs.cache-hit != 'true'
shell: pwsh
run: |
choco install -y magicsplat-tcl-tk --no-progress
echo "${HOME}/AppData/Local/Apps/Tcl86/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Choco install NetWide Assembler
if: steps.cache.outputs.cache-hit != 'true'
shell: pwsh
run: |
choco install -y nasm --no-progress
echo "C:/Program Files/NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install Rust
if: steps.cache.outputs.cache-hit != 'true'
run: |
rustup toolchain install stable --profile minimal --no-self-update
rustup default stable
rustup target add $env:TARGET
env:
TARGET: ${{ steps.config.outputs.target }}
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
- name: Insert config snippet
if: steps.config.outputs.extra_config != ''
working-directory: apps/desktop
shell: bash
run: |
mkdir config-edit
pnpm asar extract webapp.asar config-edit
cd config-edit
mv config.json old-config.json
echo '${{ steps.config.outputs.extra_config }}' | jq -s '.[0] * .[1]' old-config.json - > config.json
rm old-config.json
cd ..
rm webapp.asar
pnpm asar pack config-edit/ webapp.asar
- name: Set up sqlcipher macros
if: steps.cache.outputs.cache-hit != 'true' && contains(inputs.arch, 'arm')
shell: pwsh
run: |
echo "NCC=${{ github.workspace }}\scripts\cl.bat" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Set up build tools
if: steps.cache.outputs.cache-hit != 'true'
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
with:
arch: ${{ steps.config.outputs.arch || inputs.arch }}
- name: Build Natives
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop
run: |
refreshenv
pnpm run build:native --target $env:TARGET
env:
TARGET: ${{ steps.config.outputs.target }}
- name: Install and configure eSigner CKA
run: |
Set-StrictMode -Version 'Latest'
# Download, extract, and rename
Invoke-WebRequest -OutFile eSigner_CKA.zip "$env:ESIGNER_URL"
Expand-Archive -Path eSigner_CKA.zip -DestinationPath .
Get-ChildItem -Path * -Include "*_build_*.exe" | Rename-Item -NewName eSigner_CKA.exe
# Install
New-Item -ItemType Directory -Force -Path "$env:INSTALL_DIR"
./eSigner_CKA.exe /CURRENTUSER /VERYSILENT /SUPPRESSMSGBOXES /DIR="${{ env.INSTALL_DIR }}" | Out-Null
# Disable logger
$LogConfig = Get-Content -Path ${{ env.INSTALL_DIR }}/log4net.config
$LogConfig[0] = '<log4net threshold="OFF">'
$LogConfig | Set-Content -Path ${{ env.INSTALL_DIR }}/log4net.config
# Configure - default credentials from https://www.ssl.com/guide/esigner-demo-credentials-and-certificates/
${{ env.INSTALL_DIR }}/eSignerCKATool.exe config `
-mode "$env:ESIGNER_MODE" `
-user "${{ secrets.ESIGNER_USER_NAME || 'esigner_demo' }}" `
-pass "${{ secrets.ESIGNER_USER_PASSWORD || 'esignerDemo#1' }}" `
-totp "${{ secrets.ESIGNER_USER_TOTP || 'RDXYgV9qju+6/7GnMf1vCbKexXVJmUVr+86Wq/8aIGg=' }}" `
-key "${{ env.MASTER_KEY_FILE }}" -r
${{ env.INSTALL_DIR }}/eSignerCKATool.exe unload
${{ env.INSTALL_DIR }}/eSignerCKATool.exe load
# Find certificate
$CodeSigningCert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
echo Certificate: $CodeSigningCert
# Extract thumbprint and subject name
$Thumbprint = $CodeSigningCert.Thumbprint
$SubjectName = ($CodeSigningCert.Subject -replace ", ?", "`n" | ConvertFrom-StringData).CN
echo "ED_SIGNTOOL_THUMBPRINT=$Thumbprint" >> $env:GITHUB_ENV
echo "ED_SIGNTOOL_SUBJECT_NAME=$SubjectName" >> $env:GITHUB_ENV
env:
ESIGNER_MODE: ${{ vars.ESIGNER_MODE || 'sandbox' }}
ESIGNER_URL: https://github.com/SSLcom/eSignerCKA/releases/download/v1.0.6/SSL.COM-eSigner-CKA_1.0.6.zip
INSTALL_DIR: C:\Users\runneradmin\eSignerCKA
MASTER_KEY_FILE: C:\Users\runneradmin\eSignerCKA\master.key
- name: Build App
working-directory: apps/desktop
run: pnpm run build --publish never $BUILD_ARGS -w $TARGETS
shell: bash
env:
VARIANT_PATH: variant.json
# Only set for Nightly builds
# The windows packager relies on parsing this as semver, so we have to make it look like one.
# This will give our update packages really stupid names, but we probably can't change that either
# because squirrel windows parses them for the version too. We don't really care: nobody sees them.
# We just give the installer a static name, so you'll just see this in the 'about' dialog.
# Turns out if you use 0.0.0 here it makes Squirrel windows crash, so we use 0.0.1.
VERSION: ${{ inputs.version && format('0.0.1-nightly.{0}', inputs.version) || '' }}
BUILD_ARGS: ${{ steps.config.outputs.build-args }}
TARGETS: ${{ inputs.targets }}
- name: Trust eSigner sandbox cert
if: inputs.sign == ''
run: |
Set-StrictMode -Version 'Latest'
Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root -FilePath .github/SSLcom-sandbox.crt
- name: Check app was signed successfully
working-directory: apps/desktop
run: |
Set-StrictMode -Version 'Latest'
Get-ChildItem `
-Recurse dist `
-Include *.exe, *.msi `
| ForEach-Object -Process {. $env:SIGNTOOL_PATH verify /pa $_.FullName; if(!$?) { throw }}
- name: Upload Artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: ${{ inputs.artifact-prefix }}win-${{ inputs.arch }}
path: |
apps/desktop/dist
retention-days: 1
- name: Assert executable is present
working-directory: apps/desktop
run: |
Test-Path './dist/win-*unpacked/Element*.exe'
- name: Assert all Squirrel files are present
if: contains(inputs.targets, 'squirrel')
working-directory: apps/desktop
run: |
Test-Path './dist/squirrel-windows*/Element Setup*.exe'
Test-Path './dist/squirrel-windows*/element-desktop-*-full.nupkg'
Test-Path './dist/squirrel-windows*/RELEASES'
- name: Assert MSI is present
if: contains(inputs.targets, 'msi')
working-directory: apps/desktop
run: |
Test-Path './dist/Element*.msi'
test:
name: Test Windows ${{ inputs.arch }}
needs: build
if: inputs.test
uses: ./.github/workflows/build_desktop_test.yaml
with:
project: win-${{ inputs.arch }}
artifact: ${{ inputs.artifact-prefix }}win-${{ inputs.arch }}
runs-on: ${{ inputs.test-runs-on || (inputs.arch == 'arm64' && 'windows-11-arm' || 'windows-2022') }}
executable: ./dist/win*-unpacked/Element*.exe
blob_report: ${{ inputs.blob_report }}
args: ${{ inputs.test-args }}
+74
View File
@@ -0,0 +1,74 @@
name: Dockerbuild
on:
workflow_dispatch: {}
push:
branches: [master, staging, develop]
paths:
- "dockerbuild/**"
pull_request:
concurrency: ${{ github.workflow }}-${{ github.ref_name }}
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}-dockerbuild
permissions: {}
jobs:
build:
name: Docker Build
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
with:
install: true
- name: Build test image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
file: apps/desktop/dockerbuild/Dockerfile
context: apps/desktop
push: false
load: true
tags: element-desktop-dockerbuild
platforms: linux/amd64
- name: Test image
run: docker run -v $PWD:/project element-desktop-dockerbuild pnpm install
- name: Log in to the Container registry
uses: docker/login-action@c144859092e8e353f3e118b0b329679a62351c3e
if: github.event_name != 'pull_request'
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
if: github.event_name != 'pull_request'
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
- name: Build and push Docker image
if: github.event_name != 'pull_request'
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
file: apps/desktop/dockerbuild/Dockerfile
context: apps/desktop
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
+1 -11
View File
@@ -16,13 +16,6 @@ jobs:
name: GitHub Pages
runs-on: ubuntu-24.04
steps:
- name: Fetch element-desktop
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: element-hq/element-desktop
path: element-desktop
persist-credentials: false
- name: Fetch element-web
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
@@ -49,7 +42,7 @@ jobs:
working-directory: element-web
run: |
pnpm install --frozen-lockfile
pnpm node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk > docs/automations.md
pnpm node ./scripts/gen-workflow-mermaid.ts ../element-web ../matrix-js-sdk > docs/automations.md
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
- name: Setup mdBook
@@ -64,9 +57,6 @@ jobs:
run: |
mkdir docs
mv element-desktop/README.md element-desktop/docs/
mv element-desktop/docs "docs/Element Desktop"
mv element-web/README.md element-web/docs/
mv element-web/docs/lib docs/
mv element-web/docs "docs/Element Web"
+1
View File
@@ -11,6 +11,7 @@ jobs:
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
with:
# This relates to the directory in which to reset dependencies, only web needs this
dir: apps/web
dependencies: |
matrix-js-sdk
+10 -14
View File
@@ -29,22 +29,9 @@ jobs:
gpg-fingerprint: ${{ vars.GPG_FINGERPRINT }}
asset-path: dist/*.tar.gz
expected-asset-count: 3
# Desktop has no dist script so we only target web here
dir: apps/web
notify-downstream:
name: Trigger release drafter downstream
needs: release
runs-on: ubuntu-24.04
steps:
- name: Notify element-desktop repo that element-web release has completed to re-trigger release-drafter
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1
with:
workflow: release-drafter.yml
repo: element-hq/element-desktop
ref: staging
# Required when using the `repo` option. Either a PAT or a token generated from the GitHub app or CLI
token: "${{ secrets.ELEMENT_BOT_TOKEN }}"
check:
name: Post release checks
needs: release
@@ -69,3 +56,12 @@ jobs:
wait-interval: 10
check-name: Build package
allowed-conclusions: success
- name: Wait for desktop packaging
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with:
ref: master
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
check-regexp: Prepare|Linux|macOS|Windows|Deploy|deploy
allowed-conclusions: success
+1 -33
View File
@@ -2,11 +2,6 @@ name: Cut branches
on:
workflow_dispatch:
inputs:
element-desktop:
description: Prepare element-desktop
required: true
type: boolean
default: true
element-web:
description: Prepare element-web
required: true
@@ -26,7 +21,6 @@ jobs:
repo:
- matrix-org/matrix-js-sdk
- element-hq/element-web
- element-hq/element-desktop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop # zizmor: ignore[unpinned-uses]
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -38,19 +32,8 @@ jobs:
needs: checks
env:
# The order is specified bottom-up to avoid any races for allchange
REPOS: matrix-js-sdk element-web element-desktop
REPOS: matrix-js-sdk element-web
steps:
- name: Checkout Element Desktop
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
if: inputs.element-desktop
with:
repository: element-hq/element-desktop
path: element-desktop
ref: staging
fetch-depth: 0
fetch-tags: true
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
persist-credentials: true
- name: Checkout Element Web
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
if: inputs.element-web
@@ -79,10 +62,6 @@ jobs:
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- name: Merge Element Desktop
if: inputs.element-desktop
run: |
git -C "element-desktop" merge origin/develop
- name: Merge Element Web
if: inputs.element-web
run: |
@@ -116,14 +95,3 @@ jobs:
wait-interval: 10
check-name: "draft / draft"
allowed-conclusions: success
- name: Wait for element-desktop draft
if: inputs.element-desktop
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with:
ref: staging
repo: element-hq/element-desktop
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
wait-interval: 10
check-name: "draft / draft"
allowed-conclusions: success
+2
View File
@@ -107,6 +107,8 @@ jobs:
labs|sliding_sync_description
settings|voip|noise_suppression_description
settings|voip|echo_cancellation_description
- name: Element Desktop
path: "apps/desktop"
- name: Shared Components
path: "packages/shared-components"
name: "i18n Check (${{ matrix.name }})"
+9 -2
View File
@@ -18,7 +18,7 @@ coverage
/apps/web/src/modules.ts
/apps/web/src/modules.js
src/i18n/strings
/apps/web//build_config.yaml
/apps/web/build_config.yaml
# Raises an error because it contains a template var breaking the script tag
/apps/web/src/vector/index.html
/apps/web/src/vector/modernizr.cjs
@@ -29,7 +29,7 @@ debian/tmp
package-lock.json
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
/CHANGELOG.md
CHANGELOG.md
/docs/changelogs
# Legacy skinning file that some people might still have
@@ -45,6 +45,13 @@ package-lock.json
/apps/web/playwright/logs/
/apps/web/playwright/snapshots/
/apps/desktop/build/
/apps/desktop/dockerbuild/
/apps/desktop/lib/
/apps/desktop/webapp
/apps/desktop/playwright/html-report
/apps/desktop/playwright/test-results
# Shared components generated files
/packages/shared-components/dist/
/packages/shared-components/src/i18n/i18nKeys.d.ts
-19
View File
@@ -71,25 +71,6 @@ the default for PRs labelled with `T-Task`):
_Remove outdated comment from `Ungulates.ts`_
```
Notes: none
```
Sometimes, you're fixing a bug in a downstream project, in which case you want
an entry in that project's changelog. You can do that too:
_Fix another herding bug_
```
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
```
This example is for Element Web. You can specify:
- element-web
- element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
to specify in the notes that it's a breaking change - this will be added
+1 -1
View File
@@ -72,7 +72,7 @@ Element can also be run as a desktop app, wrapped in Electron. You can download
pre-built version from <https://element.io/get-started> or, if you prefer,
build it yourself.
To build it yourself, follow the instructions at <https://github.com/element-hq/element-desktop>.
To build it yourself, follow the instructions at <https://github.com/element-hq/element-web/tree/develop/apps/desktop>.
Many thanks to @aviraldg for the initial work on the Electron integration.
+88
View File
@@ -0,0 +1,88 @@
module.exports = {
plugins: ["matrix-org", "n"],
extends: ["plugin:matrix-org/javascript"],
parserOptions: {
ecmaVersion: 2021,
project: ["tsconfig.json"],
},
env: {
es6: true,
node: true,
// we also have some browser code (ie. the preload script)
browser: true,
},
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
"quotes": "off",
"indent": "off",
"prefer-promise-reject-errors": "off",
"no-async-promise-executor": "off",
"n/file-extension-in-import": ["error", "always"],
"unicorn/prefer-node-protocol": ["error"],
},
overrides: [
{
files: ["src/**/*.ts"],
extends: ["plugin:matrix-org/typescript"],
rules: {
// Things we do that break the ideal style
"prefer-promise-reject-errors": "off",
"quotes": "off",
"@typescript-eslint/no-explicit-any": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
},
},
{
files: ["hak/**/*.ts"],
extends: ["plugin:matrix-org/typescript"],
parserOptions: {
project: ["hak/tsconfig.json"],
},
rules: {
// Things we do that break the ideal style
"prefer-promise-reject-errors": "off",
"quotes": "off",
"@typescript-eslint/no-explicit-any": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
},
},
{
files: ["scripts/**/*.ts"],
extends: ["plugin:matrix-org/typescript"],
parserOptions: {
project: ["scripts/tsconfig.json"],
},
rules: {
// Things we do that break the ideal style
"prefer-promise-reject-errors": "off",
"quotes": "off",
"@typescript-eslint/no-explicit-any": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
},
},
{
files: ["playwright/**/*.ts"],
extends: ["plugin:matrix-org/typescript"],
parserOptions: {
project: ["playwright/tsconfig.json"],
},
rules: {
// Things we do that break the ideal style
"prefer-promise-reject-errors": "off",
"quotes": "off",
"@typescript-eslint/no-explicit-any": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
},
},
],
};
+22
View File
@@ -0,0 +1,22 @@
/dist
/lib
/webapp
/webapp.asar
/packages
/deploys
node_modules/
/pkg/control
/.hak
/.yarnrc
/docker
/.npmrc
.vscode
.vscode/
/test_artifacts/
/coverage/
yarn-error.log
/hak/**/*.js
/scripts/hak/**/*.js
.DS_Store
/playwright-report
.pnpm-store
+4
View File
@@ -0,0 +1,4 @@
{
"*": "prettier --write --ignore-unknown",
"*.(ts|tsx)": ["eslint --fix"]
}
+1
View File
@@ -0,0 +1 @@
24.14.0
+1
View File
@@ -0,0 +1 @@
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");
File diff suppressed because it is too large Load Diff
+173
View File
@@ -0,0 +1,173 @@
![Build](https://github.com/vector-im/element-desktop/actions/workflows/build_desktop_and_deploy.yaml/badge.svg)
![Static Analysis](https://github.com/vector-im/element-desktop/actions/workflows/static_analysis.yaml/badge.svg)
[![Localazy](https://img.shields.io/endpoint?url=https%3A%2F%2Fconnect.localazy.com%2Fstatus%2Felement-web%2Fdata%3Fcontent%3Dall%26title%3Dlocalazy%26logo%3Dtrue)](https://localazy.com/p/element-web)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=element-desktop)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=element-desktop)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=bugs)](https://sonarcloud.io/summary/new_code?id=element-desktop)
# Element Desktop
Element Desktop is a Matrix client for desktop platforms with Element Web at its core.
# First Steps
Before you do anything else, fetch the dependencies:
```
pnpm install
```
# Fetching Element
Since this package is just the Electron wrapper for Element Web, it doesn't contain any of the Element Web code,
so the first step is to get a working copy of Element Web. There are a few ways of doing this:
```
# Fetch the prebuilt release Element package from the element-web GitHub releases page. The version
# fetched will be the same as the local element-desktop package.
# We're explicitly asking for no config, so the packaged Element will have no config.json.
pnpm run fetch --noverify --cfgdir ""
```
...or if you'd like to use GPG to verify the downloaded package:
```
# Fetch the Element public key from the element.io web server over a secure connection and import
# it into your local GPG keychain (you'll need GPG installed). You only need to to do this
# once.
pnpm run fetch --importkey
# Fetch the package and verify the signature
pnpm run fetch --cfgdir ""
```
...or either of the above, but fetching a specific version of Element:
```
# Fetch the prebuilt release Element package from the element-web GitHub releases page. The version
# fetched will be the same as the local element-desktop package.
pnpm run fetch --noverify --cfgdir "" v1.5.6
```
If you only want to run the app locally and don't need to build packages, you can
provide the `webapp` directory directly:
```
ln -s ../web/webapp ./
```
[TODO: add support for fetching develop builds, arbitrary URLs and arbitrary paths]
# Building
## Native Build
TODO: List native pre-requisites
Optionally, [build the native modules](https://github.com/vector-im/element-desktop/blob/develop/docs/native-node-modules.md),
which include support for searching in encrypted rooms and secure storage. Skipping this step is fine, you just won't have those features.
Then, run
```
pnpm run build
```
This will do a couple of things:
- Run the `setversion` script to set the local package version to match whatever
version of Element you installed above.
- Run electron-builder to build a package. The package built will match the operating system
you're running the build process on.
## Docker
Alternatively, you can also build using docker, which will always produce the linux package:
```
# Run this once to make the docker image
pnpm run docker:setup
pnpm run docker:install
# if you want to build the native modules (this will take a while)
pnpm run docker:build:native
pnpm run docker:build
```
After running, the packages should be in `dist/`.
# Starting
If you'd just like to run the electron app locally for development:
```
pnpm start
```
# Config
If you'd like the packaged Element to have a configuration file, you can create a
config directory and place `config.json` in there, then specify this directory
with the `--cfgdir` option to `pnpm run fetch`, eg:
```
mkdir myconfig
cp /path/to/my/config.json myconfig/
pnpm run fetch --cfgdir myconfig
```
The config dir for the official Element app is in `element.io`. If you use this,
your app will auto-update itself using builds from element.io.
# Profiles
To run multiple instances of the desktop app for different accounts, you can
launch the executable with the `--profile` argument followed by a unique
identifier, e.g `element-desktop --profile Work` for it to run a separate profile and
not interfere with the default one.
Alternatively, a custom location for the profile data can be specified using the
`--profile-dir` flag followed by the desired path.
# User-specified config.json
- `%APPDATA%\$NAME\config.json` on Windows
- `$XDG_CONFIG_HOME/$NAME/config.json` or `~/.config/$NAME/config.json` on Linux
- `~/Library/Application Support/$NAME/config.json` on macOS
In the paths above, `$NAME` is typically `Element`, unless you use `--profile
$PROFILE` in which case it becomes `Element-$PROFILE`, or it is using one of
the above created by a pre-1.7 install, in which case it will be `Riot` or
`Riot-$PROFILE`.
You may also specify a different path entirely for the `config.json` file by
providing the `--config $YOUR_CONFIG_JSON_FILE` to the process, or via the
`ELEMENT_DESKTOP_CONFIG_JSON` environment variable.
# Translations
To add a new translation, head to the [translating doc](https://github.com/vector-im/element-web/blob/develop/docs/translating.md).
For a developer guide, see the [translating dev doc](https://github.com/vector-im/element-web/blob/develop/docs/translating-dev.md).
# Report bugs & give feedback
If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.
To help avoid duplicate issues, please [view existing issues](https://github.com/vector-im/element-web/issues?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc) first (and add a +1) or [create a new issue](https://github.com/vector-im/element-web/issues/new/choose) if you can't find it. Please note that this issue tracker is associated with the [element-web](https://github.com/vector-im/element-web) repo, but is also applied to the code in this repo as well.
## Copyright & License
Copyright (c) 2016-2017 OpenMarket Ltd
Copyright (c) 2017 Vector Creations Ltd
Copyright (c) 2017-2025 New Vector Ltd
This software is multi licensed by New Vector Ltd (Element). It can be used either:
(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
(2) for free under the terms of the GNU General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
(3) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to).
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"],
};
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Entitlements from electron-builder's defaults
(https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/templates/entitlements.mac.plist)
nb. This does *not* include the app sandbox: at the time of adding this file,
we were using electron-builder 21.2.0 which does not have the sandbox entitlement.
Latest electron-builder does, but it appears to be causing issues:
(https://github.com/electron-userland/electron-builder/issues/4390)
-->
<!-- https://github.com/electron/electron-notarize#prerequisites -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Our own additional entitlements (we need to access the camera and
mic for VoIP calls -->
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

+27
View File
@@ -0,0 +1,27 @@
{
"fill": "automatic",
"groups": [
{
"layers": [
{
"glass": false,
"image-name": "element.png",
"name": "element"
}
],
"name": "Figma",
"shadow": {
"kind": "none",
"opacity": 0.5
},
"specular": false,
"translucency": {
"enabled": false,
"value": 0.5
}
}
],
"supported-platforms": {
"squares": ["macOS"]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

+24
View File
@@ -0,0 +1,24 @@
# Docker image to facilitate building Element Desktop's native bits using a glibc version (2.31)
# with broader compatibility, down to Debian bullseye & Ubuntu focal.
FROM rust:bullseye@sha256:16950191527a4cb9e0762d9d48b705a6315158e4035e64f7a93ce8656a1b053c
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -qq update && apt-get -y -qq dist-upgrade && \
apt-get -y -qq install --no-install-recommends \
# tclsh is required for building SQLite as part of SQLCipher
tcl \
# Used by seshat (when not SQLCIPHER_STATIC) \
libsqlcipher-dev && \
apt-get purge -y --auto-remove && rm -rf /var/lib/apt/lists/*
RUN ln -s /usr/bin/python3 /usr/bin/python & ln -s /usr/bin/pip3 /usr/bin/pip
ENV DEBUG_COLORS=true
ENV FORCE_COLOR=true
WORKDIR /project
ARG TARGETOS
ARG TARGETARCH
COPY .node-version dockerbuild/setup.sh /
RUN /setup.sh
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -x
declare -A archMap=(["amd64"]="x64" ["arm64"]="arm64")
ARCH="${archMap["$TARGETARCH"]}"
# The .node-version file generally doesn't have the 'v' (renovate does not put the 'v' and will
# strip it on upgrade if it's there) but the 'v' is also widely supported so we probably ought
# to just work either way.
NODE_VERSION=$(cat /.node-version | sed -e 's/^v//')
curl --proto "=https" -L "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-$TARGETOS-$ARCH.tar.gz" | tar xz -C /usr/local --strip-components=1 && \
unlink /usr/local/CHANGELOG.md && unlink /usr/local/LICENSE && unlink /usr/local/README.md
corepack enable
+19
View File
@@ -0,0 +1,19 @@
# Summary
- [Introduction](../README.md)
# Build/Debug
- [Native Node modules](native-node-modules.md)
- [Windows requirements](windows-requirements.md)
- [Debugging](debugging.md)
- [Using gdb](gdb.md)
# Distribution
- [Updates](updates.md)
- [Packaging](packaging.md)
# Setup
- [Config](config.md)
+15
View File
@@ -0,0 +1,15 @@
# Configuration
All Element Web options documented [here](https://github.com/vector-im/element-web/blob/develop/docs/config.md) can be used as well as the following:
---
The app contains a configuration file specified at build time using [these instructions](https://github.com/element-hq/element-web/blob/develop/apps/desktop/README.md#config).
This config can be overwritten by the end using by creating a `config.json` file at the paths described [here](https://github.com/element-hq/element-web/blob/develop/apps/desktop/README.md#user-specified-configjson).
After changing the config, the app will need to be exited fully (including via the task tray) and re-started.
---
1. `update_base_url`: Specifies the URL of the update server, see [document](https://github.com/element-hq/element-web/blob/develop/apps/desktop/docs/updates.md).
2. `web_base_url`: Specifies the Element Web URL when performing actions such as popout widget. Defaults to `https://app.element.io/`.
+28
View File
@@ -0,0 +1,28 @@
# Debugging Element-Desktop
There are two parts of the desktop app that you might want to debug.
## The renderer process
This is the regular element-web codeand can be debugged by just selecting 'toggle developer tools'
from the menu, even on ppackaged builds. This then works the same as chrome dev tools for element web.
## The main process
This is debugged as a node app, so:
1. Open any chrome dev tools window
1. Start element with the `--inspect-brk` flag
1. Notice that you now have a little green icon in the top left of your chrome devtools window, click it.
You are now debugging the code of the desktop app itself.
## The main process of a package app
When the app is shipped, electron's "fuses" are flipped, editing the electron binary itself to prevent certain features from being usable, one of which is debugging using `--inspect-brk` as above. You can flip the fuse back on Linux as follows:
```
sudo npx @electron/fuses write --app /opt/Element/element-desktop EnableNodeCliInspectArguments=on
```
A similar command will work, in theory, on mac and windows, except that this will break code signing (which is the point of fuses) so you would have to re-sign the app or somesuch.
+45
View File
@@ -0,0 +1,45 @@
# Using gdb against Element-Desktop
Occasionally it is useful to be able to connect to a running Element-Desktop
with [`gdb`](https://sourceware.org/gdb/), or to analayze a coredump. For this,
you will need debug symbols.
1. If you don't already have the right version of Element-Desktop (eg because
you are analyzing someone else's coredump), download and unpack the tarball
from https://packages.element.io/desktop/install/linux/. If it was a
nightly, your best bet may be to download the deb from
https://packages.element.io/debian/pool/main/e/element-nightly/ and unpack
it.
2. Figure out which version of Electron your Element-Desktop is based on. The
best way to do this is to figure out the version of Element-Desktop, then
look at
[`package.json`](https://github.com/element-hq/element-web/blob/develop/apps/desktop/package.json)
for the corresponding version. There will be an entry within `dependencies` of
`electron`: the value will tell you the version of Electron that was used for that version of Element-Desktop.
3. Go to [Electron's releases page](https://github.com/electron/electron/releases/)
and find the version you just identified. Under "Assets", download
`electron-v<version>-linux-x64-debug.zip` (or, the -debug zip corresponding to your
architecture).
4. The debug zip has a structure like:
```
.
├── debug
│   ├── chrome_crashpad_handler.debug
│   ├── electron.debug
│   ├── libEGL.so.debug
│   ├── libffmpeg.so.debug
│   ├── libGLESv2.so.debug
│   └── libvk_swiftshader.so.debug
├── LICENSE
├── LICENSES.chromium.html
└── version
```
Take all the contents of `debug`, and copy them into the Element-Desktop directory,
so that `electron.debug` is alongside the `element-desktop-nightly` executable.
5. You now have a thing you can gdb as normal, either as `gdb --args element-desktop-nightly`, or
`gdb element-desktop-nightly core`.
+180
View File
@@ -0,0 +1,180 @@
# Native Node Modules
For some features, the desktop version of Element can make use of native Node
modules. These allow Element to integrate with the desktop in ways that a browser
cannot.
While native modules enable powerful new features, they must be complied for
each operating system. For official Element releases, we will always build these
modules from source to ensure we can trust the compiled output. In the future,
we may offer a pre-compiled path for those who want to use these features in a
custom build of Element without installing the various build tools required.
The process is automated by [vector-im/element-builder](https://github.com/vector-im/element-builder)
when releasing.
## Use docker
If you are building for Linux, you can build the native modules with:
```
pnpm docker:setup
pnpm docker:install
INDOCKER_SQLCIPHER_BUNDLED=1 pnpm docker:build:native
```
The above will build `matrix-seshat` in
`docker/node_modules/matrix-seshat`. You can then either run `pnpm docker:build`
to build the app inside docker, or:
```
pnpm link docker/node_modules/matrix-seshat
```
... and build the app with `pnpm build` or run it with `pnpm start`.
(See also https://github.com/element-hq/element-web/blob/develop/apps/desktop/README.md#docker.)
## Building
Install the pre-requisites for your system:
- [Windows pre-requisites](https://github.com/element-hq/element-web/blob/develop/apps/desktop/docs/windows-requirements.md)
- Linux: TODO. Using the docker environment as above is recommended.
- OS X: TODO
Then optionally, [add seshat and dependencies to support search in E2E rooms](#adding-seshat-for-search-in-e2e-encrypted-rooms).
Then, to build for an architecture selected automatically based on your system (recommended), run:
```
pnpm run build:native
```
If you need to build for a specific architecture, see [here](#compiling-for-specific-architectures).
## Adding Seshat for search in E2E encrypted rooms
Seshat is a native Node module that adds support for local event indexing and
full text search in E2E encrypted rooms.
Since Seshat is written in Rust, the Rust compiler and related tools need to be
installed before installing Seshat itself. To install Rust please consult the
official Rust [documentation](https://www.rust-lang.org/tools/install).
Seshat also depends on the SQLCipher library to store its data in encrypted form
on disk. You'll need to install it via your OS package manager.
After installing the Rust compiler and SQLCipher, Seshat support can be added
using pnpm at the root of this project:
pnpm add matrix-seshat
You will have to rebuild the native libraries against electron's version
of node rather than your system node, using the `electron-build-env` tool.
This is also needed to when pulling in changes to Seshat using `pnpm link`.
pnpm add electron-build-env
Recompiling Seshat itself can be done like so:
ELECTRON_VERSION=$(electron --version)
pnpm electron-build-env -- --electron ${ELECTRON_VERSION#v} -- neon build matrix-seshat --release
Please make sure to include all the `--` as well as the `--release` command line
switch at the end. Modify your electron version accordingly depending on the
version that is installed on your system.
After this is done the Electron version of Element can be run from the main folder
as usual using:
pnpm start
### Statically linking libsqlcipher
On Windows & macOS we always statically link libsqlcipher for it is not generally available.
On Linux by default we will use a system package, on debian & ubuntu this is `libsqlcipher0`,
but this is problematic for some other packages, and we found that it may crashes for unknown reasons.
By including `SQLCIPHER_BUNDLED=1` in the build environment, the build scripts will fully statically
link sqlcipher, including a static build of OpenSSL.
More info can be found at https://github.com/matrix-org/seshat/issues/102
and https://github.com/vector-im/element-web/issues/20926.
## Compiling for specific architectures
### macOS
On macOS, you can build universal native modules too:
```
pnpm run build:native:universal
```
...or you can build for a specific architecture:
```
pnpm run build:native --target x86_64-apple-darwin
```
or
```
pnpm run build:native --target aarch64-apple-darwin
```
You'll then need to create a built bundle with the same architecture.
To bundle a universal build for macOS, run:
```
pnpm run build:universal
```
### Windows
If you're on Windows, you can choose to build specifically for 32 or 64 bit:
```
pnpm run build:32
```
or
```
pnpm run build:64
```
### Cross compiling
Compiling a module for a particular operating system (Linux/macOS/Windows) needs
to be done on that operating system. Cross-compiling from a host OS for a different
target OS may be possible, but we don't support this flow with Element dependencies
at this time.
### Switching between architectures
The native module build system keeps the different architectures
separate, so you can keep native modules for several architectures at the same
time and switch which are active using a `pnpm run hak copy` command, passing
the appropriate architectures. This will error if you haven't yet built those
architectures. eg:
```
pnpm run build:native --target x86_64-apple-darwin
# We've now built & linked into place native modules for Intel
pnpm run build:native --target aarch64-apple-darwin
# We've now built Apple Silicon modules too, and linked them into place as the active ones
pnpm run hak copy --target x86_64-apple-darwin
# We've now switched back to our Intel modules
pnpm run hak copy --target x86_64-apple-darwin --target aarch64-apple-darwin
# Now our native modules are universal x86_64+aarch64 binaries
```
The current set of native modules are stored in `.hak/hakModules`,
so you can use this to check what architecture is currently in place, eg:
```
$ lipo -info .hak/hakModules/keytar/build/Release/keytar.node
Architectures in the fat file: .hak/hakModules/keytar/build/Release/keytar.node are: x86_64 arm64
```
+46
View File
@@ -0,0 +1,46 @@
## Packaging nightlies
Element Desktop nightly builds are build automatically by the [Github Actions workflow](https://github.com/vector-im/element-web/blob/develop/.github/workflows/build_desktop_and_deploy.yaml).
The schedule is currently set for once a day at 9am UTC. It will deploy to packages.element.io upon completion.
## Triggering a manual nightly build
Simply go to https://github.com/vector-im/element-web/actions/workflows/build_desktop_and_deploy.yaml
1. Click `Run workflow`
1. Feel free to make changes to the checkboxes depending on the circumstances
1. Click the green `Run workflow`
## Packaging releases
The packaging is kicked off automagically for you when a Github Release for Element Web is published.
### More detail on the github actions
We moved to Github Actions for the following reasons:
1. Removing single point of failure
2. Improving reliability
3. Unblocking the packaging on a single individual
4. Improving parallelism
The Windows builds are signed by SSL.com using their Cloud Key Adapter for eSigner.
This allows us to use Microsoft's signtool to interface with eSigner and send them a hash of the exe along with
credentials in exchange for a signed certificate which we attach onto all the relevant files.
The Apple builds are signed using standard code signing means and then notarised to appease GateKeeper.
The Linux builds are distributed via a signed reprepro repository.
The packages.element.io site is a public Cloudflare R2 bucket which is deployed to solely from Github Actions.
The main bucket in R2 is `packages-element-io` which is a direct mapping of packages.element.io,
we have a workflow which generates the index.html files there to imitate a public index which Cloudflare does not currently support.
The reprepro database lives in `packages-element-io-db`.
There is an additional pair of buckets of same name but appended with `-test` which can be used for testing,
these land on https://packages-element-io-test.element.io/.
### Debian/Ubuntu Distributions
We used to add a new distribution to match each Debian and Ubuntu release. As of April 2020, we have created a `default` distribution that everyone can use (since the packages have never differed by distribution anyway).
The distribution configuration lives in https://github.com/vector-im/packages.element.io/blob/master/debian/conf/distributions as a canonical source.
+15
View File
@@ -0,0 +1,15 @@
The Desktop app is capable of self-updating on macOS and Windows.
The update server base url is configurable as `update_base_url` in config.json and can be served by a static file host,
CDN or object storage.
Currently all packaging & deployment is handled by [Github actions](https://github.com/vector-im/element-web/blob/develop/.github/workflows/build_desktop_and_deploy.yaml)
# Windows
On Windows the update mechanism used is [Squirrel.Windows](https://github.com/Squirrel/Squirrel.Windows)
and can be served by any compatible Squirrel server, such as https://github.com/Tiliq/squirrel-server
# macOS
On macOS the update mechanism used is [Squirrel.Mac](https://github.com/Squirrel/Squirrel.Mac)
using the newer JSON format as documented [here](https://github.com/Squirrel/Squirrel.Mac#update-file-json-format).
+45
View File
@@ -0,0 +1,45 @@
# Windows
## Requirements to build native modules
We rely on Github Actions `windows-2022` plus a few extra utilities as per [the workflow](https://github.com/vector-im/element-web/blob/develop/.github/workflows/build_desktop_windows.yaml).
If you want to build native modules, make sure that the following tools are installed on your system.
- [Git for Windows](https://git-scm.com/download/win)
- [Node 16](https://nodejs.org)
- [Python 3](https://www.python.org/downloads/) (if you type 'python' into command prompt it will offer to install it from the windows store)
- [Strawberry Perl](https://strawberryperl.com/)
- [Rustup](https://rustup.rs/)
- [NASM](https://www.nasm.us/)
You can install the above tools using [Chocolatey](https://chocolatey.org/install):
```cmd
choco install --no-progress -y git nodejs-lts pnpm python StrawberryPerl rustup.install nasm magicsplat-tcl-tk
```
- [Build Tools for Visual Studio 2019](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019) with the following configuration:
- On the Workloads tab:
- Desktop & Mobile -> C++ build tools
- On the Individual components tab:
- MSVC VS 2019 C++ build tools
- Windows 10 SDK (latest version available)
- C++ CMake tools for Windows
Once installed make sure all those utilities are accessible in your `PATH`.
If you want to be able to build x86 targets from an x64 host install the right toolchain:
```cmd
rustup toolchain install stable-i686-pc-windows-msvc
rustup target add i686-pc-windows-msvc
```
In order to load all the C++ utilities installed by Visual Studio you can run the following in a terminal window.
```
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" amd64
```
You can replace `amd64` with `x86` depending on your CPU architecture.
+232
View File
@@ -0,0 +1,232 @@
import * as os from "node:os";
import * as fs from "node:fs";
import * as path from "node:path";
import { type Configuration as BaseConfiguration, type Protocol } from "electron-builder";
/**
* This script has different outputs depending on your os platform.
*
* On Windows:
* Passes $ED_SIGNTOOL_THUMBPRINT and $ED_SIGNTOOL_SUBJECT_NAME to
* build.win.signtoolOptions.signingHashAlgorithms and build.win.signtoolOptions.certificateSubjectName respectively if specified.
*
* On Linux:
* Replaces spaces in the product name with dashes as spaces in paths can cause issues
* Removes libsqlcipher0 recommended dependency if env SQLCIPHER_BUNDLED is asserted.
* Passes $ED_DEBIAN_CHANGELOG to build.deb.fpm if specified
*/
/**
* Interface describing relevant fields of the package.json file.
*/
interface Pkg {
version: string;
}
/**
* Base metadata fields, used in both package.json and the variant configuration.
*/
interface Metadata {
name: string;
productName: string;
description: string;
}
/**
* Extra metadata fields that are injected into the build to pass to the app at runtime.
*/
interface ExtraMetadata extends Metadata {
electron_appId: string;
electron_protocol: string;
}
/**
* Interface describing the variant configuration format.
*/
interface Variant extends Metadata {
"appId": string;
"linux.executableName"?: string;
"linux.deb.name"?: string;
"protocols": string[];
}
type Writable<T> = NonNullable<
T extends Function ? T : T extends object ? { -readonly [K in keyof T]: Writable<T[K]> } : T
>;
// Load the default variant as a base configuration
const DEFAULT_VARIANT = path.join("element.io", "release", "build.json");
let variant: Variant = JSON.parse(fs.readFileSync(DEFAULT_VARIANT, "utf8"));
/**
* If a variant is specified, we will use it to override the build-specific values.
* This allows us to have different builds for different purposes (e.g. stable, nightly).
*/
if (process.env.VARIANT_PATH) {
console.log(`Using variant configuration from '${process.env.VARIANT_PATH}':`);
variant = {
...variant,
...JSON.parse(fs.readFileSync(`${process.env.VARIANT_PATH}`, "utf8")),
};
} else {
console.warn(`No VARIANT_PATH specified, using default variant configuration '${DEFAULT_VARIANT}':`);
}
for (const key in variant) {
console.log(`${key}: ${variant[key]}`);
}
interface Configuration extends BaseConfiguration {
extraMetadata: Partial<Pick<Pkg, "version">> & ExtraMetadata;
linux: BaseConfiguration["linux"];
win: BaseConfiguration["win"];
mac: BaseConfiguration["mac"];
deb: {
fpm: string[];
} & BaseConfiguration["deb"];
}
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
*/
const config: Omit<Writable<Configuration>, "electronFuses"> & {
// Make all fuses required to ensure they are all explicitly specified
electronFuses: Required<Configuration["electronFuses"]>;
} = {
appId: variant.appId,
asarUnpack: "**/*.node",
electronFuses: {
enableCookieEncryption: true,
onlyLoadAppFromAsar: true,
grantFileProtocolExtraPrivileges: false,
runAsNode: false,
enableNodeOptionsEnvironmentVariable: false,
enableNodeCliInspectArguments: false,
// We need to reset the signature if we are not signing on darwin otherwise it won't launch
resetAdHocDarwinSignature: !process.env.APPLE_TEAM_ID,
loadBrowserProcessSpecificV8Snapshot: false,
enableEmbeddedAsarIntegrityValidation: true,
},
files: [
"package.json",
{
from: ".hak/hakModules",
to: "node_modules",
},
"lib/**",
],
extraResources: ["build/icon.*", "webapp.asar"],
extraMetadata: {
name: variant.name,
productName: variant.productName,
description: variant.description,
electron_appId: variant.appId,
electron_protocol: variant.protocols[0],
},
linux: {
target: ["tar.gz", "deb"],
category: "Network;InstantMessaging;Chat",
icon: "icon.png",
executableName: variant.name, // element-desktop or element-desktop-nightly
},
deb: {
packageCategory: "net",
depends: [
"libgtk-3-0",
"libnotify4",
"libnss3",
"libxss1",
"libxtst6",
"xdg-utils",
"libatspi2.0-0",
"libuuid1",
"libsecret-1-0",
"libasound2",
"libgbm1",
],
recommends: ["libsqlcipher0", "element-io-archive-keyring"],
fpm: ["--deb-pre-depends", "libc6 (>= 2.31)"],
},
mac: {
target: ["dmg", "zip"],
category: "public.app-category.social-networking",
darkModeSupport: true,
hardenedRuntime: true,
gatekeeperAssess: true,
strictVerify: true,
entitlements: "./build/entitlements.mac.plist",
icon: "build/icon.icon",
mergeASARs: true,
x64ArchFiles: "**/matrix-seshat/*.node", // hak already runs lipo
},
dmg: {
badgeIcon: "build/icon.icon",
},
win: {
target: ["squirrel", "msi"],
signtoolOptions: {
signingHashAlgorithms: ["sha256"],
},
icon: "build/icon.ico",
},
msi: {
perMachine: true,
},
directories: {
output: "dist",
},
protocols: {
name: variant.productName,
schemes: variant.protocols,
},
nativeRebuilder: "sequential",
nodeGypRebuild: false,
npmRebuild: true,
};
/**
* Allow specifying the version via env var.
* If unspecified, it will default to the version in package.json.
* @param {string} process.env.VERSION
*/
if (process.env.VERSION) {
config.extraMetadata.version = process.env.VERSION;
}
if (variant["linux.deb.name"]) {
config.deb.fpm.push("--name", variant["linux.deb.name"]);
}
/**
* Allow specifying windows signing cert via env vars
* @param {string} process.env.ED_SIGNTOOL_SUBJECT_NAME
* @param {string} process.env.ED_SIGNTOOL_THUMBPRINT
*/
if (process.env.ED_SIGNTOOL_SUBJECT_NAME && process.env.ED_SIGNTOOL_THUMBPRINT) {
config.win.signtoolOptions!.certificateSubjectName = process.env.ED_SIGNTOOL_SUBJECT_NAME;
config.win.signtoolOptions!.certificateSha1 = process.env.ED_SIGNTOOL_THUMBPRINT;
}
if (os.platform() === "linux") {
// Electron crashes on debian if there's a space in the path.
// https://github.com/vector-im/element-web/issues/13171
config.extraMetadata.productName = config.extraMetadata.productName.replace(/ /g, "-");
/**
* Allow specifying deb changelog via env var
* @param {string} process.env.ED_DEB_CHANGELOG
*/
if (process.env.ED_DEBIAN_CHANGELOG) {
config.deb.fpm.push(`--deb-changelog=${process.env.ED_DEBIAN_CHANGELOG}`);
}
if (process.env.SQLCIPHER_BUNDLED) {
// Remove sqlcipher dependency when using bundled
config.deb.recommends = config.deb.recommends?.filter((d) => d !== "libsqlcipher0");
}
}
export default config;
@@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF8DCCBNigAwIBAgIRAIZSd8hNVs3w8AhJIsQSoYYwDQYJKoZIhvcNAQELBQAw
gZExCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTcwNQYD
VQQDEy5DT01PRE8gUlNBIEV4dGVuZGVkIFZhbGlkYXRpb24gQ29kZSBTaWduaW5n
IENBMB4XDTIwMDczMDAwMDAwMFoXDTIzMDczMDIzNTk1OVowgfMxETAPBgNVBAUT
CDEwODczNjYxMRMwEQYLKwYBBAGCNzwCAQMTAkdCMR0wGwYDVQQPExRQcml2YXRl
IE9yZ2FuaXphdGlvbjELMAkGA1UEBhMCR0IxDzANBgNVBBEMBlc0IDFRVTEYMBYG
A1UECAwPTG9uZG9uLCBDaXR5IG9mMQ8wDQYDVQQHDAZMb25kb24xJzAlBgNVBAkM
HjE0IFR1cm5oYW0gR3JlZW4sIFRlcnJhY2UgTWV3czEbMBkGA1UECgwSTmV3IFZl
Y3RvciBMaW1pdGVkMRswGQYDVQQDDBJOZXcgVmVjdG9yIExpbWl0ZWQwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKDnGul2M7oCCN+3veGf7yfakSfMjK
Kqeylyo0Nj8dUleNQvsRo0OHxIWJlbHDYglxebT109MqgyASr0SoeiqvXOSSFACD
MYFoyerRGMnXEuomTU0y7+FE3p/hcim8+C5gS+rHr3YaJJuzzXRztjjCBGoY4was
h9V8kYiyMMK0xi2WftOCBa1yYS32CInHIZVmdhzoK4k4YzSYIp57BWvnIUlDyCYk
slNfp0SFbDrOGa7kbmy8HRfWPLjNmW5PPIrsf8LlnVRBfmTeUIJtV31w/FuMjeir
pzYjzooXmpIrj96ecxdc9thP6etCUazvpowjfewu7UNWRUhGPtYn8v8rAgMBAAGj
ggHdMIIB2TAfBgNVHSMEGDAWgBTfj/MgDOnKpgTYW1g3Kj2rRtyDSTAdBgNVHQ4E
FgQUB9su4pCQXE5ZWFhB1eo48992LEcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB
/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEQYJYIZIAYb4QgEBBAQDAgQQMEkG
A1UdIARCMEAwNQYMKwYBBAGyMQECAQYBMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8v
c2VjdGlnby5jb20vQ1BTMAcGBWeBDAEDMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6
Ly9jcmwuY29tb2RvY2EuY29tL0NPTU9ET1JTQUV4dGVuZGVkVmFsaWRhdGlvbkNv
ZGVTaWduaW5nQ0EuY3JsMIGGBggrBgEFBQcBAQR6MHgwUAYIKwYBBQUHMAKGRGh0
dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET1JTQUV4dGVuZGVkVmFsaWRhdGlv
bkNvZGVTaWduaW5nQ0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v
ZG9jYS5jb20wJgYDVR0RBB8wHaAbBggrBgEFBQcIA6APMA0MC0dCLTEwODczNjYx
MA0GCSqGSIb3DQEBCwUAA4IBAQBZ8/YtqW/+VUoV8knKpdhMR2uTn8AEyVmfmoZA
Ly5kdCRoKvFm/z4VK4cqF7gsUDxRIgDuMKKbWTtr1FEXuaZkUkOjicNcdYxc0pDn
nFKBWAv5pN5OmnC9cVqIG7PtvD+8bKVzDdQRjpGMy6PY4rN2PYGfQ7KGgddEDD6m
oM51jI/OTvCeU0Tyl0bixKEmpUJvbeQM9Ul2Y1o5Enx1Q9uda8xATb0HCMKgJ+GC
iFHL2DNC3j1xK4QoZEIYgbTscj9rK4OMEov3PT/e1FwQyB5V9xdJ5i1MDBDD9fAf
OiVgf90SffT7TgWUbA4Z+PtQHq/qNma0+dZWyeq7zYn2IIX3
-----END CERTIFICATE-----
+6
View File
@@ -0,0 +1,6 @@
This directory contains the config file for the official element.io distribution
of Element Desktop.
You probably do not want to build with this config unless you're building the
official element.io distribution, or you'll find your builds will replace
themselves with the element.io build.
@@ -0,0 +1,9 @@
{
"appId": "im.riot.nightly",
"name": "element-desktop-nightly",
"productName": "Element Nightly",
"description": "Element: the future of secure communication (nightly unstable build)",
"protocols": ["io.element.nightly", "element"],
"linux.executableName": "element-desktop-nightly",
"linux.deb.name": "element-nightly"
}
@@ -0,0 +1,65 @@
{
"update_base_url": "https://packages.element.io/nightly/update/",
"default_server_name": "matrix.org",
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix-client.matrix.org"
},
"m.identity_server": {
"base_url": "https://vector.im"
}
},
"brand": "Element Nightly",
"integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api",
"integrations_widgets_urls": [
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
],
"bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"show_labs_settings": true,
"room_directory": {
"servers": ["matrix.org", "gitter.im"]
},
"enable_presence_by_hs_url": {
"https://matrix.org": false,
"https://matrix-client.matrix.org": false
},
"terms_and_conditions_links": [
{
"url": "https://element.io/privacy",
"text": "Privacy Policy"
},
{
"url": "https://element.io/cookie-policy",
"text": "Cookie Policy"
}
],
"sentry": {
"dsn": "https://029a0eb289f942508ae0fb17935bd8c5@sentry.matrix.org/6",
"environment": "nightly"
},
"posthog": {
"project_api_key": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
"api_host": "https://posthog.element.io"
},
"privacy_policy_url": "https://element.io/cookie-policy",
"features": {
"threadsActivityCentre": true,
"feature_spotlight": true,
"feature_group_calls": true,
"feature_video_rooms": true,
"feature_element_call_video_rooms": true
},
"setting_defaults": {
"RustCrypto.staged_rollout_percent": 100
},
"element_call": {
"url": "https://call.element.dev"
},
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
}
@@ -0,0 +1,7 @@
{
"appId": "im.riot.app",
"name": "element-desktop",
"productName": "Element",
"description": "Element: the future of secure communication",
"protocols": ["io.element.desktop", "element"]
}
@@ -0,0 +1,59 @@
{
"update_base_url": "https://packages.element.io/desktop/update/",
"default_server_name": "matrix.org",
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix-client.matrix.org"
},
"m.identity_server": {
"base_url": "https://vector.im"
}
},
"brand": "Element",
"integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api",
"integrations_widgets_urls": [
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
],
"bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"room_directory": {
"servers": ["matrix.org", "gitter.im"]
},
"show_labs_settings": false,
"enable_presence_by_hs_url": {
"https://matrix.org": false,
"https://matrix-client.matrix.org": false
},
"terms_and_conditions_links": [
{
"url": "https://element.io/privacy",
"text": "Privacy Policy"
},
{
"url": "https://element.io/cookie-policy",
"text": "Cookie Policy"
}
],
"posthog": {
"project_api_key": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
"api_host": "https://posthog.element.io"
},
"privacy_policy_url": "https://element.io/cookie-policy",
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx",
"setting_defaults": {
"RustCrypto.staged_rollout_percent": 60
},
"features": {
"feature_video_rooms": true,
"feature_group_calls": true,
"feature_element_call_video_rooms": true
},
"element_call": {
"url": "https://call.element.io"
}
}
+34
View File
@@ -0,0 +1,34 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type HakEnv from "../../scripts/hak/hakEnv.js";
import type { DependencyInfo } from "../../scripts/hak/dep.js";
export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const env = hakEnv.makeGypEnv();
if (!hakEnv.isHost()) {
env.CARGO_BUILD_TARGET = hakEnv.getTargetId();
}
console.log("Running yarn install");
await hakEnv.spawn("yarn", ["install"], {
cwd: moduleInfo.moduleBuildDir,
env,
shell: true,
});
const buildTarget = hakEnv.wantsStaticSqlCipher() ? "build-bundled" : "build";
console.log("Running yarn build");
await hakEnv.spawn("yarn", ["run", buildTarget], {
cwd: moduleInfo.moduleBuildDir,
env,
shell: true,
});
}
+55
View File
@@ -0,0 +1,55 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import childProcess from "node:child_process";
import fsProm from "node:fs/promises";
import type HakEnv from "../../scripts/hak/hakEnv.js";
import type { Tool } from "../../scripts/hak/hakEnv.js";
import type { DependencyInfo } from "../../scripts/hak/dep.js";
export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const tools: Tool[] = [
["rustc", "--version"],
["python", "--version"], // node-gyp uses python for reasons beyond comprehension
];
if (hakEnv.isWin()) {
tools.push(["perl", "--version"]); // for openssl configure
tools.push(["nasm", "-v"]); // for openssl building
tools.push(["patch", "--version"]); // to patch sqlcipher Makefile.msc
tools.push(["nmake", "/?"]);
} else {
tools.push(["make", "--version"]);
}
await hakEnv.checkTools(tools);
// Ensure Rust target exists (nb. we avoid depending on rustup)
await new Promise((resolve, reject) => {
const rustc = childProcess.execFile(
"rustc",
["--target", hakEnv.getTargetId(), "--emit=obj", "-o", "tmp", "-"],
(err, out) => {
if (err) {
reject(
"rustc can't build for target " +
hakEnv.getTargetId() +
": ensure target is installed via `rustup target add " +
hakEnv.getTargetId() +
"` " +
"or your package manager if not using `rustup`",
);
}
fsProm.unlink("tmp").then(resolve);
},
);
rustc.stdin!.write("fn main() {}");
rustc.stdout!.pipe(process.stdout);
rustc.stderr!.pipe(process.stderr);
rustc.stdin!.end();
});
}
+7
View File
@@ -0,0 +1,7 @@
{
"scripts": {
"check": "check.ts",
"build": "build.ts"
},
"copy": "index.node"
}
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"target": "es2022",
"sourceMap": false,
"strict": true,
"lib": ["es2022"],
"types": ["node"]
},
"include": ["../scripts/@types/*.d.ts", "./**/*.ts"]
}
+112
View File
@@ -0,0 +1,112 @@
{
"name": "element-desktop",
"productName": "Element",
"main": "lib/electron-main.js",
"exports": "./lib/electron-main.js",
"version": "1.12.13",
"description": "Element: the future of secure communication",
"author": {
"name": "Element",
"email": "support@element.io"
},
"homepage": "https://element.io",
"repository": {
"type": "git",
"url": "https://github.com/vector-im/element-web"
},
"license": "SEE LICENSE IN README.md",
"type": "module",
"files": [],
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"i18n": "matrix-gen-i18n && pnpm i18n:sort && pnpm i18n:lint",
"i18n:sort": "matrix-sort-i18n src/i18n/strings/en_EN.json",
"i18n:lint": "prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null",
"i18n:diff": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && pnpm i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"mkdirs": "mkdirp packages deploys",
"fetch": "pnpm run mkdirs && tsx scripts/fetch-package.ts",
"asar-webapp": "asar p webapp webapp.asar",
"start": "pnpm run build:ts && pnpm run build:res && electron .",
"lint": "pnpm lint:types && pnpm lint:js",
"lint:js": "eslint --max-warnings 0 src hak playwright scripts",
"lint:js-fix": "eslint --fix --max-warnings 0 src hak playwright scripts && prettier --log-level=warn --write .",
"lint:types": "pnpm lint:types:src && pnpm lint:types:test && pnpm lint:types:scripts && pnpm lint:types:hak",
"lint:types:src": "tsc --noEmit",
"lint:types:test": "tsc --noEmit -p playwright/tsconfig.json",
"lint:types:scripts": "tsc --noEmit -p scripts/tsconfig.json",
"lint:types:hak": "tsc --noEmit -p hak/tsconfig.json",
"build:native": "pnpm run hak",
"build:native:universal": "pnpm run hak --target x86_64-apple-darwin fetchandbuild && pnpm run hak --target aarch64-apple-darwin fetchandbuild && pnpm run hak --target x86_64-apple-darwin --target aarch64-apple-darwin copyandlink",
"build:32": "pnpm run build:ts && pnpm run build:res && electron-builder --ia32",
"build:64": "pnpm run build:ts && pnpm run build:res && electron-builder --x64",
"build:universal": "pnpm run build:ts && pnpm run build:res && electron-builder --universal",
"build": "pnpm run build:ts && pnpm run build:res && electron-builder",
"build:ts": "tsc",
"build:res": "tsx scripts/copy-res.ts",
"docker:setup": "docker build --platform linux/amd64 -t element-desktop-dockerbuild -f dockerbuild/Dockerfile .",
"docker:build:native": "scripts/in-docker.sh pnpm run hak",
"docker:build": "scripts/in-docker.sh pnpm run build",
"docker:install": "scripts/in-docker.sh pnpm install",
"clean": "rimraf webapp.asar dist packages deploys lib",
"hak": "tsx scripts/hak/index.ts",
"test": "playwright test",
"test:open": "pnpm test --ui",
"test:screenshots:build": "docker build playwright -t element-desktop-playwright --platform linux/amd64",
"test:screenshots:run": "docker run --rm --network host -v $(pwd):/work/element-desktop -v element-desktop-playwright:/work/element-desktop/node_modules -v /var/run/docker.sock:/var/run/docker.sock --platform linux/amd64 -it element-desktop-playwright",
"sane-postinstall": "electron-builder install-app-deps"
},
"dependencies": {
"@sentry/electron": "^7.0.0",
"auto-launch": "^5.0.5",
"counterpart": "^0.18.6",
"electron-store": "^11.0.0",
"electron-window-state": "^5.0.3",
"minimist": "^1.2.6",
"png-to-ico": "^3.0.0",
"uuid": "^13.0.0"
},
"devDependencies": {
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/preset-typescript": "^7.18.6",
"@electron/asar": "4.1.0",
"@playwright/test": "catalog:",
"@stylistic/eslint-plugin": "^5.0.0",
"@types/auto-launch": "^5.0.1",
"@types/counterpart": "^0.18.1",
"@types/minimist": "^1.2.1",
"@types/node": "18.19.130",
"@types/pacote": "^11.1.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"app-builder-lib": "26.8.2",
"chokidar": "^5.0.0",
"detect-libc": "^2.0.0",
"electron": "41.0.2",
"electron-builder": "26.8.2",
"electron-builder-squirrel-windows": "26.8.2",
"electron-devtools-installer": "^4.0.0",
"eslint": "^8.26.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-n": "^17.12.0",
"eslint-plugin-unicorn": "^56.0.0",
"glob": "^13.0.0",
"matrix-web-i18n": "catalog:",
"mkdirp": "^3.0.0",
"pacote": "^21.0.0",
"prettier": "^3.0.0",
"rimraf": "^6.0.0",
"tar": "^7.5.8",
"tsx": "^4.19.2",
"typescript": "5.9.3"
},
"hakDependencies": {
"matrix-seshat": "^4.0.1"
},
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
}
+43
View File
@@ -0,0 +1,43 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { defineConfig } from "@playwright/test";
const projects = [
"macos",
"win-x64",
"win-ia32",
"win-arm64",
"linux-amd64-sqlcipher-system",
"linux-amd64-sqlcipher-static",
"linux-arm64-sqlcipher-system",
"linux-arm64-sqlcipher-static",
];
export default defineConfig({
// Allows the GitHub action to specify a project name (OS + arch) for the combined report to make sense
// workaround for https://github.com/microsoft/playwright/issues/33521
projects: process.env.CI
? projects.map((name) => ({
name,
}))
: undefined,
use: {
viewport: { width: 1280, height: 720 },
video: "retain-on-failure",
trace: "on-first-retry",
},
testDir: "playwright/e2e",
outputDir: "playwright/test-results",
workers: 1,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? [["blob"], ["github"]] : [["html", { outputFolder: "playwright/html-report" }]],
snapshotDir: "playwright/snapshots",
snapshotPathTemplate: "{snapshotDir}/{testFilePath}/{arg}-{platform}{ext}",
timeout: 30 * 1000,
});
+5
View File
@@ -0,0 +1,5 @@
/test-results/
/html-report/
# Only commit snapshots from Linux
/snapshots/**/*.png
!/snapshots/**/*-linux.png
+13
View File
@@ -0,0 +1,13 @@
FROM mcr.microsoft.com/playwright:v1.58.2-jammy@sha256:4698a73749c5848d3f5fcd42a2174d172fcad2b2283e087843b115424303a565
WORKDIR /work/element-desktop
RUN apt-get update && apt-get -y install xvfb dbus-x11 && apt-get purge -y --auto-remove && rm -rf /var/lib/apt/lists/*
# Create node_modules & dist dirs so that the volumes have the correct permissions
RUN mkdir node_modules dist && chown 1000:1000 node_modules dist
USER 1000:1000
COPY docker-entrypoint.sh /opt/docker-entrypoint.sh
ENTRYPOINT ["bash", "/opt/docker-entrypoint.sh"]
@@ -0,0 +1,18 @@
#!/bin/bash
set -e
echo "Starting Xvfb"
Xvfb :99 -ac &
sleep 2
export DISPLAY=:99
pnpm install --frozen-lockfile
pnpm build -l --dir
PLAYWRIGHT_HTML_OPEN=never ELEMENT_DESKTOP_EXECUTABLE="./dist/linux-unpacked/element-desktop" \
npx playwright test --update-snapshots --reporter line,html "$1"
# Clean up
rm -R core qemu_* || exit 0
@@ -0,0 +1,44 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { test, expect } from "../../element-desktop-test.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
test.describe("App config options", () => {
test.describe("Should load custom config via env", () => {
test.slow();
test.use({
extraEnv: {
ELEMENT_DESKTOP_CONFIG_JSON: resolve(__dirname, "../..", "fixtures/custom-config.json"),
},
});
test("should launch and use configured homeserver", async ({ page }) => {
await page.locator("#matrixchat").waitFor();
await page.locator(".mx_Welcome").waitFor();
await expect(page).toHaveURL("vector://vector/webapp/#/welcome");
await page.getByText("Sign in").click();
await page.getByText("matrix.example.org", { exact: true }).waitFor();
});
});
test.describe("Should load custom config via argument", () => {
test.slow();
test.use({
extraArgs: ["--config", resolve(__dirname, "../..", "fixtures/custom-config.json")],
});
test("should launch and use configured homeserver", async ({ page }) => {
await page.locator("#matrixchat").waitFor();
await page.locator(".mx_Welcome").waitFor();
await expect(page).toHaveURL("vector://vector/webapp/#/welcome");
await page.getByText("Sign in").click();
await page.getByText("matrix.example.org", { exact: true }).waitFor();
});
});
});
@@ -0,0 +1,75 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-desktop-test.js";
declare global {
interface ElectronPlatform {
getEventIndexingManager():
| {
supportsEventIndexing(): Promise<boolean>;
}
| undefined;
getPickleKey(userId: string, deviceId: string): Promise<string | null>;
createPickleKey(userId: string, deviceId: string): Promise<string | null>;
}
interface Window {
mxPlatformPeg: {
get(): ElectronPlatform;
};
}
}
test.describe("App launch", () => {
test.slow();
test.beforeEach(async ({ page }) => {
await page.locator("#matrixchat").waitFor();
await page.locator(".mx_Welcome").waitFor();
});
test("should launch and render the welcome view successfully", async ({ page }) => {
await expect(page).toHaveURL("vector://vector/webapp/#/welcome");
await expect(page).toHaveScreenshot();
});
test("should launch and render the welcome view successfully and support seshat", async ({ page }) => {
await expect(
page.evaluate<boolean>(async () => {
return window.mxPlatformPeg.get().getEventIndexingManager()?.supportsEventIndexing();
}),
).resolves.toBeTruthy();
});
test.describe("safeStorage", () => {
const userId = "@user:server";
const deviceId = "ABCDEF";
test("should be supported", async ({ page }) => {
await expect(
page.evaluate(
([userId, deviceId]) => window.mxPlatformPeg.get().createPickleKey(userId, deviceId),
[userId, deviceId],
),
).resolves.not.toBeNull();
});
});
test.describe("--no-update", () => {
test.use({
extraArgs: ["--no-update"],
});
// XXX: this test works fine locally but in CI the app start races with the test plumbing up the stdout/stderr pipes
// which means the logs are missed, disabling for now.
test.skip("should respect option", async ({ page, stdout }) => {
expect(stdout.data.toString()).toContain("Auto update disabled via command line flag");
});
});
});
@@ -0,0 +1,36 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-desktop-test.js";
declare global {
interface ElectronPlatform {
getOidcCallbackUrl(): URL;
}
interface Window {
mxPlatformPeg: {
get(): ElectronPlatform;
};
}
}
test.describe("OIDC Native", () => {
test.slow();
test.beforeEach(async ({ page }) => {
await page.locator(".mx_Welcome").waitFor();
});
test("should use OIDC callback URL without authority component", async ({ page }) => {
await expect(
page.evaluate<string>(() => {
return window.mxPlatformPeg.get().getOidcCallbackUrl().toString();
}),
).resolves.toMatch(/io\.element\.(desktop|nightly):\/vector\/webapp\//);
});
});
@@ -0,0 +1,120 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { _electron as electron, test as base, expect as baseExpect, type ElectronApplication } from "@playwright/test";
import fs from "node:fs/promises";
import path, { dirname } from "node:path";
import os from "node:os";
import { fileURLToPath } from "node:url";
import { PassThrough } from "node:stream";
/**
* A PassThrough stream that captures all data written to it.
*/
class CapturedPassThrough extends PassThrough {
private _chunks = [];
public constructor() {
super();
super.on("data", this.onData);
}
private onData = (chunk): void => {
this._chunks.push(chunk);
};
public get data(): Buffer {
return Buffer.concat(this._chunks);
}
}
interface Fixtures {
app: ElectronApplication;
tmpDir: string;
extraEnv: Record<string, string>;
extraArgs: string[];
// Utilities to capture stdout and stderr for tests to make assertions against
stdout: CapturedPassThrough;
stderr: CapturedPassThrough;
}
const __dirname = dirname(fileURLToPath(import.meta.url));
export const test = base.extend<Fixtures>({
extraEnv: {},
extraArgs: [],
// eslint-disable-next-line no-empty-pattern
stdout: async ({}, use) => {
await use(new CapturedPassThrough());
},
// eslint-disable-next-line no-empty-pattern
stderr: async ({}, use) => {
await use(new CapturedPassThrough());
},
// eslint-disable-next-line no-empty-pattern
tmpDir: async ({}, use) => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "element-desktop-tests-"));
await use(tmpDir);
await fs.rm(tmpDir, { recursive: true });
},
app: async ({ tmpDir, extraEnv, extraArgs, stdout, stderr }, use) => {
const args = ["--profile-dir", tmpDir, ...extraArgs];
if (process.env.GITHUB_ACTIONS) {
if (process.platform === "linux") {
// GitHub Actions hosted runner lacks dbus and a compatible keyring, so we need to force plaintext storage
args.push("--storage-mode", "force-plaintext");
} else if (process.platform === "darwin") {
// GitHub Actions hosted runner has no working default keychain, so allow plaintext storage
args.push("--storage-mode", "allow-plaintext");
}
}
const executablePath = process.env["ELEMENT_DESKTOP_EXECUTABLE"];
if (!executablePath) {
// Unpackaged mode testing
args.unshift(path.join(__dirname, "..", "lib", "electron-main.js"));
}
console.log(`Launching '${executablePath || "electron"}' with args ${args.join(" ")}`);
const app = await electron.launch({
env: {
...process.env,
...extraEnv,
},
executablePath,
args,
});
app.process().stdout.pipe(stdout).pipe(process.stdout);
app.process().stderr.pipe(stderr).pipe(process.stderr);
await app.firstWindow();
// Block matrix.org access to ensure consistent tests
const context = app.context();
await context.route("https://matrix.org/**", async (route) => {
await route.abort();
});
await use(app);
},
page: async ({ app }, use) => {
const window = await app.firstWindow();
await use(window);
await app.close().catch((e) => {
console.error(e);
});
},
});
export const expect = baseExpect;
@@ -0,0 +1,10 @@
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.org"
},
"m.identity_server": {
"base_url": "https://identity.example.org"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"moduleResolution": "node",
"esModuleInterop": true,
"target": "es2022",
"module": "es2022",
"lib": ["es2022", "dom"],
"types": ["node"]
},
"include": ["**/*.ts"]
}
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# Script for downloading a branch of element-web matching the branch a PR is contributed from
set -x
deforg="element-hq"
defrepo="element-web"
# The PR_NUMBER variable must be set explicitly.
default_org_repo=${GITHUB_REPOSITORY:-"$deforg/$defrepo"}
PR_ORG=${PR_ORG:-${default_org_repo%%/*}}
PR_REPO=${PR_REPO:-${default_org_repo##*/}}
# A function that clones a branch of a repo based on the org, repo and branch
clone() {
org=$1
repo=$2
branch=$3
if [ -n "$branch" ]
then
echo "Trying to use $org/$repo#$branch"
# Disable auth prompts: https://serverfault.com/a/665959
GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0
fi
}
echo "Getting info about a PR with number $PR_NUMBER"
apiEndpoint="https://api.github.com/repos/$PR_ORG/$PR_REPO/pulls/$PR_NUMBER"
head=$(curl "$apiEndpoint" | jq -r '.head.label')
# for forks, $head will be in the format "fork:branch", so we split it by ":"
# into an array. On non-forks, this has the effect of splitting into a single
# element array given ":" shouldn't appear in the head - it'll just be the
# branch name. Based on the results, we clone.
BRANCH_ARRAY=(${head//:/ })
TRY_ORG=$deforg
TRY_BRANCH=${BRANCH_ARRAY[0]}
if [[ "$head" == *":"* ]]; then
# ... but only match that fork if it's a real fork
if [ "${BRANCH_ARRAY[0]}" != "$PR_ORG" ]; then
TRY_ORG=${BRANCH_ARRAY[0]}
fi
TRY_BRANCH=${BRANCH_ARRAY[1]}
fi
clone "$TRY_ORG" "$defrepo" "$TRY_BRANCH"
exit 1
+7
View File
@@ -0,0 +1,7 @@
REM Batch file to aid in cross-compiling sqlcipher for Windows ARM64
REM Full path should be passed to Makefile.msc as NCC env var
setlocal
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" %VSCMD_ARG_HOST_ARCH%
cl.exe %*
endlocal
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env -S npx tsx
// copies resources into the lib directory.
import parseArgs from "minimist";
import * as chokidar from "chokidar";
import * as path from "node:path";
import * as fs from "node:fs";
const argv = parseArgs(process.argv.slice(2), {});
const watch = argv.w;
const verbose = argv.v;
function errCheck(err: unknown): void {
if (err) {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
}
}
const I18N_BASE_PATH = "src/i18n/strings/";
const INCLUDE_LANGS = fs.readdirSync(I18N_BASE_PATH).filter((fn) => fn.endsWith(".json"));
// Ensure lib, lib/i18n and lib/i18n/strings all exist
fs.mkdirSync("lib/i18n/strings", { recursive: true });
type Translations = Record<string, Record<string, string> | string>;
function genLangFile(file: string, dest: string): void {
const translations: Translations = {};
[file].forEach(function (f) {
if (fs.existsSync(f)) {
try {
Object.assign(translations, JSON.parse(fs.readFileSync(f).toString()));
} catch (e) {
console.error("Failed: " + f, e);
throw e;
}
}
});
const json = JSON.stringify(translations, null, 4);
const filename = path.basename(file);
fs.writeFileSync(dest + filename, json);
if (verbose) {
console.log("Generated language file: " + filename);
}
}
/*
watch the input files for a given language,
regenerate the file, and regenerating languages.json with the new filename
*/
function watchLanguage(file: string, dest: string): void {
// XXX: Use a debounce because for some reason if we read the language
// file immediately after the FS event is received, the file contents
// appears empty. Possibly https://github.com/nodejs/node/issues/6112
let makeLangDebouncer: NodeJS.Timeout | undefined;
const makeLang = (): void => {
if (makeLangDebouncer) {
clearTimeout(makeLangDebouncer);
}
makeLangDebouncer = setTimeout(() => {
genLangFile(file, dest);
}, 500);
};
chokidar.watch(file).on("add", makeLang).on("change", makeLang).on("error", errCheck);
}
// language resources
const I18N_DEST = "lib/i18n/strings/";
INCLUDE_LANGS.forEach((file): void => {
genLangFile(I18N_BASE_PATH + file, I18N_DEST);
}, {});
if (watch) {
INCLUDE_LANGS.forEach((file) => watchLanguage(I18N_BASE_PATH + file, I18N_DEST));
}
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env -S npx tsx --resolveJsonModule
import * as path from "node:path";
import { createWriteStream, promises as fs } from "node:fs";
import * as childProcess from "node:child_process";
import * as tar from "tar";
import * as asar from "@electron/asar";
import { promises as stream } from "node:stream";
import riotDesktopPackageJson from "../package.json";
import { setPackageVersion } from "./set-version.js";
const PUB_KEY_URL = "https://packages.riot.im/element-release-key.asc";
const PACKAGE_URL_PREFIX = "https://github.com/element-hq/element-web/releases/download/";
const DEVELOP_TGZ_URL = "https://develop.element.io/develop.tar.gz";
const ASAR_PATH = "webapp.asar";
async function downloadToFile(url: string, filename: string): Promise<void> {
console.log("Downloading " + url + "...");
try {
const resp = await fetch(url);
if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`);
if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`);
await stream.pipeline(resp.body, createWriteStream(filename));
} catch (e) {
console.error(e);
try {
await fs.unlink(filename);
} catch {}
throw e;
}
}
async function verifyFile(filename: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
childProcess.execFile("gpg", ["--verify", filename + ".asc", filename], (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
async function main(): Promise<number | undefined> {
let verify = true;
let importkey = false;
let pkgDir = "packages";
let deployDir = "deploys";
let cfgDir: string | undefined;
let targetVersion: string | undefined;
let filename: string | undefined;
let url: string | undefined;
let setVersion = false;
while (process.argv.length > 2) {
switch (process.argv[2]) {
case "--noverify":
verify = false;
break;
case "--importkey":
importkey = true;
break;
case "--packages":
process.argv.shift();
pkgDir = process.argv[2];
break;
case "--deploys":
process.argv.shift();
deployDir = process.argv[2];
break;
case "--cfgdir":
case "-d":
process.argv.shift();
cfgDir = process.argv[2];
break;
default:
targetVersion = process.argv[2];
}
process.argv.shift();
}
if (targetVersion === undefined) {
targetVersion = "v" + riotDesktopPackageJson.version;
} else if (targetVersion !== "develop") {
setVersion = true; // version was specified
}
if (targetVersion === "develop") {
filename = "develop.tar.gz";
url = DEVELOP_TGZ_URL;
verify = false; // develop builds aren't signed
} else if (targetVersion.includes("://")) {
filename = targetVersion.substring(targetVersion.lastIndexOf("/") + 1);
url = targetVersion;
verify = false; // manually verified
} else {
filename = `element-${targetVersion}.tar.gz`;
url = PACKAGE_URL_PREFIX + targetVersion + "/" + filename;
}
const haveGpg = await new Promise<boolean>((resolve) => {
childProcess.execFile("gpg", ["--version"], (error) => {
resolve(!error);
});
});
if (importkey) {
if (!haveGpg) {
console.log("Can't import key without working GPG binary: install GPG and try again");
return 1;
}
await new Promise<boolean>((resolve, reject) => {
const gpgProc = childProcess.execFile("gpg", ["--import"], (error) => {
if (error) {
console.log("Failed to import key", error);
} else {
console.log("Key imported!");
}
resolve(!error);
});
fetch(PUB_KEY_URL)
.then((resp) => {
if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`);
if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`);
stream.pipeline(resp.body, gpgProc.stdin!).catch(reject);
})
.catch(reject);
});
return 0;
}
if (cfgDir === undefined) {
console.log("No config directory set");
console.log("Specify a config directory with --cfgdir or -d");
console.log("To build with no config (and no auto-update), pass the empty string (-d '')");
return 1;
}
if (verify && !haveGpg) {
console.log("No working GPG binary: install GPG or pass --noverify to skip verification");
return 1;
}
let haveDeploy = false;
let expectedDeployDir = path.join(deployDir, path.basename(filename).replace(/\.tar\.gz/, ""));
try {
await fs.opendir(expectedDeployDir);
console.log(expectedDeployDir + "already exists");
haveDeploy = true;
} catch {}
if (!haveDeploy) {
const outPath = path.join(pkgDir, filename);
try {
await fs.stat(outPath);
console.log("Already have " + filename + ": not redownloading");
} catch {
try {
await downloadToFile(url, outPath);
} catch (e) {
console.log("Failed to download " + url, e);
return 1;
}
}
if (verify) {
try {
await fs.stat(outPath + ".asc");
console.log("Already have " + filename + ".asc: not redownloading");
} catch {
try {
await downloadToFile(url + ".asc", outPath + ".asc");
} catch (e) {
console.log("Failed to download " + url, e);
return 1;
}
}
try {
await verifyFile(outPath);
console.log(outPath + " downloaded and verified");
} catch (e) {
console.log("Signature verification failed!", e);
return 1;
}
} else {
console.log(outPath + " downloaded but NOT verified");
}
await tar.x({
file: outPath,
cwd: deployDir,
onentry: (entry) => {
// Find the appropriate extraction path, only needed for `develop` where the dir name is unknown
if (entry.type === "Directory" && !path.join(deployDir, entry.path).startsWith(expectedDeployDir)) {
expectedDeployDir = path.join(deployDir, entry.path);
}
},
});
}
try {
await fs.stat(ASAR_PATH);
console.log(ASAR_PATH + " already present: removing");
await fs.unlink(ASAR_PATH);
} catch {}
if (cfgDir.length) {
const configJsonSource = path.join(cfgDir, "config.json");
const configJsonDest = path.join(expectedDeployDir, "config.json");
console.log(configJsonSource + " -> " + configJsonDest);
await fs.copyFile(configJsonSource, configJsonDest);
} else {
console.log("Skipping config file");
}
console.log("Pack " + expectedDeployDir + " -> " + ASAR_PATH);
await asar.createPackage(expectedDeployDir, ASAR_PATH);
if (setVersion) {
const semVer = (await fs.readFile(path.join(expectedDeployDir, "version"), "utf-8")).trim();
console.log("Updating version to " + semVer);
await setPackageVersion(semVer);
}
console.log("Done!");
}
main()
.then((ret) => {
process.exit(ret);
})
.catch((e) => {
console.error(e);
process.exit(1);
});
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env -S npx tsx
/**
* Script to generate incremental Nightly build versions, based on the latest Nightly build version of that kind.
* The version format is YYYYMMDDNN where NN is in case we need to do multiple versions in a day.
*
* NB. on windows, squirrel will try to parse the version number parts, including this string, into 32-bit integers,
* which is fine as long as we only add two digits to the end...
*/
import parseArgs from "minimist";
const argv = parseArgs<{
latest?: string;
}>(process.argv.slice(2), {
string: ["latest"],
});
function parseVersion(version: string): [Date, number] {
const year = parseInt(version.slice(0, 4), 10);
const month = parseInt(version.slice(4, 6), 10);
const day = parseInt(version.slice(6, 8), 10);
const num = parseInt(version.slice(8, 10), 10);
return [new Date(year, month - 1, day), num];
}
const [latestDate, latestNum] = argv.latest ? parseVersion(argv.latest) : [];
const now = new Date();
const month = (now.getMonth() + 1).toString().padStart(2, "0");
const date = now.getDate().toString().padStart(2, "0");
let buildNum = 1;
if (latestDate && new Date(latestDate).getDate().toString().padStart(2, "0") === date) {
buildNum = latestNum! + 1;
}
if (buildNum > 99) {
throw new Error("Maximum number of Nightlies exceeded on this day.");
}
console.log(now.getFullYear() + month + date + buildNum.toString().padStart(2, "0"));
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env -S npx tsx
/*
* Checks for the presence of a webapp, inspects its version and prints it
*/
import url from "node:url";
import { versionFromAsar } from "./set-version.js";
async function main(): Promise<number> {
const version = await versionFromAsar();
console.log(version);
return 0;
}
if (import.meta.url.startsWith("file:")) {
const modulePath = url.fileURLToPath(import.meta.url);
if (process.argv[1] === modulePath) {
main()
.then((ret) => {
process.exit(ret);
})
.catch((e) => {
console.error(e);
process.exit(1);
});
}
}
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Source https://gist.github.com/vladimyr/9a03481154cd3048a486bdf71e5e1535/57e57a6ace6fb2c8bba948bce726df7a96c3f99f
# This scripts lets you check which minimum GLIBC version an executable requires.
# Simply run './glibc-check.sh path/to/your/binary'
MAX_GLIBC="${MAX_GLIBC:-2.28}"
BINARY="$1"
# Version comparison function in bash
vercomp() {
if [[ $1 == "$2" ]]; then
return 0
fi
local i ver1 ver2
IFS="." read -ra ver1 <<<"$1"
IFS="." read -ra ver2 <<<"$2"
# fill empty fields in ver1 with zeros
for ((i = ${#ver1[@]}; i < ${#ver2[@]}; i++)); do
ver1[i]=0
done
for ((i = 0; i < ${#ver1[@]}; i++)); do
if [[ -z ${ver2[i]} ]]; then
# fill empty fields in ver2 with zeros
ver2[i]=0
fi
if ((10#${ver1[i]} > 10#${ver2[i]})); then
return 1
fi
if ((10#${ver1[i]} < 10#${ver2[i]})); then
return 2
fi
done
return 0
}
IFS="
"
VERS=$(objdump -T "$BINARY" | grep GLIBC_ | sed 's/.*GLIBC_\([.0-9]*\).*/\1/g' | sort -u)
for VER in $VERS; do
vercomp "$VER" "$MAX_GLIBC"
COMP=$?
if [[ $COMP -eq 1 ]]; then
echo "Error! ${BINARY} requests GLIBC ${VER}, which is higher than target ${MAX_GLIBC}"
echo "Affected symbols:"
objdump -T "$BINARY" | grep -F "GLIBC_${VER}"
echo "Looking for symbols in libraries..."
for LIBRARY in $(ldd "$BINARY" | cut -d ' ' -f 3); do
echo "$LIBRARY"
objdump -T "$LIBRARY" | grep -F "GLIBC_${VER}"
done
exit 27
else
echo "Found version ${VER}"
fi
done
+78
View File
@@ -0,0 +1,78 @@
# hak
This tool builds native dependencies for element-desktop. Here follows some very minimal
documentation for it.
Goals:
- Must build compiled native node modules in a shippable state
(ie. only dynamically linked against libraries that will be on the
target system, all unnecessary files removed).
- Must be able to build any native module, no matter what build system
it uses (electron-rebuild is supposed to do this job but only works
for modules that use gyp).
It's also loosely designed to be a general tool and agnostic to what it's
actually building. It's used here to build modules for the electron app
but should work equally well for building modules for normal node.
# Running
Hak is invoked with a command and a dependency, eg. `pnpm run hak fetch matrix-seshat`.
If no dependencies are given, hak runs the command on all dependencies.
# Files
There are a lot of files involved:
- scripts/hak/... - The tool itself
- hak/[dependency] - Files provided by the app that tell hak how to build each of its native dependencies.
Contains a hak.json file and also some script files, each of which must be referenced in hak.json.
- .hak/ - Files generated by hak in the course of doing its job. Includes the dependency module itself and
any of the native dependency's native dependencies.
- .hak/[dependency]/build - An extracted copy of the dependency's node module used to build it.
- .hak/[dependency]/out - Another extracted copy of the dependency, this one contains only what will be shipped.
# Workings
Hak works around native node modules that try to fetch or build their native component in
the npm 'install' phase - modules that do this will typically end up with native components
targeted to the build platform and the node that npm/pnpm is using, which is no good for an
electron app.
It does this by installing it with `--ignore-scripts` and then using `pnpm link` to keep the
dependency module separate so pnpm doesn't try to run its install / postinstall script
at other points (eg. whenever you `pnpm add` a random other dependency).
This also means that the dependencies cannot be listed in `dependencies` or
`devDependencies` in the project, since this would cause pnpm to install them and
try to fetch their native parts. Instead, they are listed in `hakDependencies` which
hak reads to install them for you.
Hak will _not_ install dependencies for the copy of the module it links into your
project, so if your native module has javascript dependencies that are actually needed at
runtime (and not just to fetch / build the native parts), it won't work.
# Lifecycle
Hak is divided into lifecycle stages, in order:
- fetch - Download and extract the source of the dependency
- link - Link the copy of the dependency into your node_modules directory
- build - The Good Stuff. Configure and build any native dependencies, then the module itself.
- copy - Copy the built artifact from the module build directory to the module output directory.
# hak.json
The scripts section contains scripts used for lifecycle stages that need them (fetch, build).
It also contains 'prune' and 'copy' which are globs of files to delete from the output module directory
and copy over from the module build directory to the output module directory, respectively.
# Shortcomings
Hak doesn't know about dependencies between lifecycle stages, ie. it doesn't know that you need to
'fetch' before you can 'build', etc. You get to run each individually, and remember
the right order.
There is also a _lot_ of duplication in the command execution: we should abstract away
some of the boilerplate required to run commands & so forth.
+14
View File
@@ -0,0 +1,14 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { DependencyInfo } from "./dep.js";
import type HakEnv from "./hakEnv.js";
export default async function build(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
await moduleInfo.scripts.build(hakEnv, moduleInfo);
}
+14
View File
@@ -0,0 +1,14 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { DependencyInfo } from "./dep.js";
import type HakEnv from "./hakEnv.js";
export default async function check(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
await moduleInfo.scripts.check?.(hakEnv, moduleInfo);
}
+19
View File
@@ -0,0 +1,19 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import path from "node:path";
import { rimraf } from "rimraf";
import type { DependencyInfo } from "./dep.js";
import type HakEnv from "./hakEnv.js";
export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
await rimraf(moduleInfo.moduleDotHakDir);
await rimraf(path.join(hakEnv.dotHakDir, "links", moduleInfo.name));
await rimraf(path.join(hakEnv.projectRoot, "node_modules", moduleInfo.name));
}
+62
View File
@@ -0,0 +1,62 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import path from "node:path";
import fsProm from "node:fs/promises";
import childProcess from "node:child_process";
import { glob } from "glob";
import { mkdirp } from "mkdirp";
import type HakEnv from "./hakEnv.js";
import type { DependencyInfo } from "./dep.js";
export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (moduleInfo.cfg.copy) {
// If there are multiple moduleBuildDirs, singular moduleBuildDir
// is the same as moduleBuildDirs[0], so we're just listing the contents
// of the first one.
const files = await glob(moduleInfo.cfg.copy, {
cwd: moduleInfo.moduleBuildDir,
});
if (moduleInfo.moduleBuildDirs.length > 1) {
if (!hakEnv.isMac()) {
console.error(
"You asked me to copy multiple targets but I've only been taught " + "how to do that on macOS.",
);
throw new Error("Can't copy multiple targets on this platform");
}
for (const f of files) {
const components = moduleInfo.moduleBuildDirs.map((dir) => path.join(dir, f));
const dst = path.join(moduleInfo.moduleOutDir, f);
await mkdirp(path.dirname(dst));
await new Promise<void>((resolve, reject) => {
childProcess.execFile("lipo", ["-create", "-output", dst, ...components], (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
} else {
console.log("Copying files from " + moduleInfo.moduleBuildDir + " to " + moduleInfo.moduleOutDir);
for (const f of files) {
console.log("\t" + f);
const src = path.join(moduleInfo.moduleBuildDir, f);
const dst = path.join(moduleInfo.moduleOutDir, f);
await mkdirp(path.dirname(dst));
await fsProm.copyFile(src, dst);
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type HakEnv from "./hakEnv.js";
export interface DependencyInfo {
name: string;
version: string;
cfg: Record<string, any>;
moduleHakDir: string;
moduleDotHakDir: string;
moduleTargetDotHakDir: string;
moduleBuildDir: string;
moduleBuildDirs: string[];
moduleOutDir: string;
nodeModuleBinDir: string;
depPrefix: string;
scripts: Record<string, (hakEnv: HakEnv, moduleInfo: DependencyInfo) => Promise<void>>;
}
+59
View File
@@ -0,0 +1,59 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import fsProm from "node:fs/promises";
import pacote from "pacote";
import path from "node:path";
import type HakEnv from "./hakEnv.js";
import type { DependencyInfo } from "./dep.js";
export default async function fetch(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
let haveModuleBuildDir;
try {
const stats = await fsProm.stat(moduleInfo.moduleBuildDir);
haveModuleBuildDir = stats.isDirectory();
} catch {
haveModuleBuildDir = false;
}
if (haveModuleBuildDir) return;
console.log("Fetching " + moduleInfo.name + "@" + moduleInfo.version);
const packumentCache = new Map();
await pacote.extract(`${moduleInfo.name}@${moduleInfo.version}`, moduleInfo.moduleBuildDir, {
packumentCache,
});
// Workaround for us switching to pnpm but matrix-seshat still using yarn classic
const packageJsonPath = path.join(moduleInfo.moduleBuildDir, "package.json");
const packageJson = await fsProm.readFile(packageJsonPath, "utf-8");
const packageJsonData = JSON.parse(packageJson);
packageJsonData["packageManager"] = "yarn@1.22.22";
await fsProm.writeFile(packageJsonPath, JSON.stringify(packageJsonData, null, 2), "utf-8");
console.log("Running yarn install in " + moduleInfo.moduleBuildDir);
await hakEnv.spawn("yarn", ["install", "--ignore-scripts"], {
cwd: moduleInfo.moduleBuildDir,
});
// also extract another copy to the output directory at this point
// nb. we do not yarn install in the output copy: we could install in
// production mode to get only runtime dependencies and not devDependencies,
// but usually native modules come with dependencies that are needed for
// building/fetching the native modules (eg. node-pre-gyp) rather than
// actually used at runtime: we do not want to bundle these into our app.
// We therefore just install no dependencies at all, and accept that any
// actual runtime dependencies will have to be added to the main app's
// dependencies. We can't tell what dependencies are real runtime deps
// and which are just used for native module building.
await pacote.extract(`${moduleInfo.name}@${moduleInfo.version}`, moduleInfo.moduleOutDir, {
packumentCache,
});
}
+130
View File
@@ -0,0 +1,130 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import path from "node:path";
import os from "node:os";
import { getElectronVersionFromInstalled } from "app-builder-lib/out/electron/electronVersion.js";
import childProcess, { type SpawnOptions } from "node:child_process";
import { type Arch, type Target, TARGETS, getHost, isHostId, type TargetId } from "./target.js";
async function getRuntimeVersion(projectRoot: string): Promise<string> {
const electronVersion = await getElectronVersionFromInstalled(path.join(projectRoot, "..", ".."));
if (!electronVersion) {
throw new Error("Can't determine Electron version");
}
return electronVersion;
}
export type Tool = [cmd: string, ...args: string[]];
export default class HakEnv {
public readonly target: Target;
public runtime: string = "electron";
public runtimeVersion?: string;
public dotHakDir: string;
public constructor(
public readonly projectRoot: string,
targetId: TargetId | null,
) {
const target = targetId ? TARGETS[targetId] : getHost();
if (!target) {
throw new Error(`Unknown target ${targetId}!`);
}
this.target = target;
this.dotHakDir = path.join(this.projectRoot, ".hak");
}
public async init(): Promise<void> {
this.runtimeVersion = await getRuntimeVersion(this.projectRoot);
}
public getTargetId(): TargetId {
return this.target.id;
}
public isWin(): boolean {
return this.target.platform === "win32";
}
public isMac(): boolean {
return this.target.platform === "darwin";
}
public isLinux(): boolean {
return this.target.platform === "linux";
}
public isFreeBSD(): boolean {
return this.target.platform === "freebsd";
}
public getTargetArch(): Arch {
return this.target.arch;
}
public isHost(): boolean {
return isHostId(this.target.id);
}
public makeGypEnv(): Record<string, string | undefined> {
return {
...process.env,
npm_config_arch: this.target.arch,
npm_config_target_arch: this.target.arch,
npm_config_disturl: "https://electronjs.org/headers",
npm_config_runtime: this.runtime,
npm_config_target: this.runtimeVersion,
npm_config_build_from_source: "true",
npm_config_devdir: path.join(os.homedir(), ".electron-gyp"),
};
}
public wantsStaticSqlCipher(): boolean {
return !(this.isLinux() || this.isFreeBSD()) || process.env.SQLCIPHER_BUNDLED == "1";
}
public spawn(
cmd: string,
args: string[],
{ ignoreWinCmdlet, ...options }: SpawnOptions & { ignoreWinCmdlet?: boolean } = {},
): Promise<void> {
return new Promise((resolve, reject) => {
const proc = childProcess.spawn(cmd + (!ignoreWinCmdlet && this.isWin() ? ".cmd" : ""), args, {
stdio: "inherit",
// We need shell mode on Windows to be able to launch `.cmd` executables
// See https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
shell: this.isWin(),
...options,
});
proc.on("exit", (code) => {
if (code) {
reject(code);
} else {
resolve();
}
});
});
}
public async checkTools(tools: Tool[]): Promise<void> {
for (const [tool, ...args] of tools) {
try {
await this.spawn(tool, args, {
ignoreWinCmdlet: true,
stdio: ["ignore"],
shell: false,
});
} catch {
throw new Error(`Can't find ${tool}`);
}
}
}
}
+147
View File
@@ -0,0 +1,147 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import HakEnv from "./hakEnv.js";
import type { TargetId } from "./target.js";
import type { DependencyInfo } from "./dep.js";
import { loadJsonFile } from "../../src/utils.js";
import packageJson from "../../package.json";
// These can only be run on specific modules
const MODULECOMMANDS = ["check", "fetch", "link", "build", "copy", "clean"];
// Shortcuts for multiple commands at once (useful for building universal binaries
// because you can run the fetch/build for each arch and then copy/link once)
const METACOMMANDS: Record<string, string[]> = {
fetchandbuild: ["check", "fetch", "build"],
copyandlink: ["copy", "link"],
};
// Scripts valid in a hak.json 'scripts' section
const HAKSCRIPTS = ["check", "fetch", "build"];
const __dirname = dirname(fileURLToPath(import.meta.url));
async function main(): Promise<void> {
const prefix = path.join(__dirname, "..", "..");
const targetIds: TargetId[] = [];
// Apply `--target <target>` option if specified
// Can be specified multiple times for the copy command to bundle
// multiple arches into a single universal output module)
for (;;) {
// eslint-disable-line no-constant-condition
const targetIndex = process.argv.indexOf("--target");
if (targetIndex === -1) break;
if (targetIndex + 1 >= process.argv.length) {
console.error("--target option specified without a target");
process.exit(1);
}
// Extract target ID and remove from args
targetIds.push(process.argv.splice(targetIndex, 2)[1] as TargetId);
}
const hakEnvs = targetIds.map((tid) => new HakEnv(prefix, tid));
if (hakEnvs.length == 0) hakEnvs.push(new HakEnv(prefix, null));
for (const h of hakEnvs) {
await h.init();
}
const hakEnv = hakEnvs[0];
const deps: Record<string, DependencyInfo> = {};
const hakDepsCfg = packageJson.hakDependencies || {};
for (const dep in hakDepsCfg) {
const hakJsonPath = path.join(prefix, "hak", dep, "hak.json");
let hakJson: Record<string, any>;
try {
hakJson = loadJsonFile(hakJsonPath);
} catch {
console.error("No hak.json found for " + dep + ".");
console.log("Expecting " + hakJsonPath);
process.exit(1);
}
deps[dep] = {
name: dep,
version: hakDepsCfg[dep as keyof typeof hakDepsCfg],
cfg: hakJson,
moduleHakDir: path.join(prefix, "hak", dep),
moduleDotHakDir: path.join(hakEnv.dotHakDir, dep),
moduleTargetDotHakDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId()),
moduleBuildDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), "build"),
moduleBuildDirs: hakEnvs.map((h) => path.join(h.dotHakDir, dep, h.getTargetId(), "build")),
moduleOutDir: path.join(hakEnv.dotHakDir, "hakModules", dep),
nodeModuleBinDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), "build", "node_modules", ".bin"),
depPrefix: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), "opt"),
scripts: {},
};
for (const s of HAKSCRIPTS) {
if (hakJson.scripts?.[s]) {
// Shockingly, using path.join and backslashes here doesn't work on Windows
const scriptModule = await import(`../../hak/${dep}/${hakJson.scripts[s]}`);
if (scriptModule.default) {
deps[dep].scripts[s] = scriptModule.default;
} else {
deps[dep].scripts[s] = scriptModule;
}
}
}
}
let cmds: string[];
if (process.argv.length < 3) {
cmds = ["check", "fetch", "build", "copy", "link"];
} else if (METACOMMANDS[process.argv[2]]) {
cmds = METACOMMANDS[process.argv[2]];
} else {
cmds = [process.argv[2]];
}
if (hakEnvs.length > 1 && cmds.some((c) => !["copy", "link"].includes(c))) {
// We allow link here too for convenience because it's completely arch independent
console.error("Multiple targets only supported with the copy command");
return;
}
let modules = process.argv.slice(3);
if (modules.length === 0) modules = Object.keys(deps);
for (const cmd of cmds) {
if (!MODULECOMMANDS.includes(cmd)) {
console.error("Unknown command: " + cmd);
console.log("Commands I know about:");
for (const cmd of MODULECOMMANDS) {
console.log("\t" + cmd);
}
process.exit(1);
}
const cmdFunc = (await import("./" + cmd)).default;
for (const mod of modules) {
const depInfo = deps[mod];
if (depInfo === undefined) {
console.log("Module " + mod + " not found - is it in hakDependencies " + "in your package.json?");
process.exit(1);
}
console.log("hak " + cmd + ": " + mod);
await cmdFunc(hakEnv, depInfo);
}
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
+16
View File
@@ -0,0 +1,16 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type HakEnv from "./hakEnv.js";
import { type DependencyInfo } from "./dep.js";
export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
await hakEnv.spawn("pnpm", ["link", moduleInfo.moduleOutDir], {
cwd: hakEnv.projectRoot,
});
}
+217
View File
@@ -0,0 +1,217 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { GLIBC, MUSL, familySync as processLibC } from "detect-libc";
// We borrow Rust's target naming scheme as a way of expressing all target
// details in a single string.
// See https://doc.rust-lang.org/rustc/platform-support.html.
export type TargetId =
| "aarch64-apple-darwin"
| "x86_64-apple-darwin"
| "universal-apple-darwin"
| "i686-pc-windows-msvc"
| "x86_64-pc-windows-msvc"
| "aarch64-pc-windows-msvc"
| "i686-unknown-freebsd"
| "x86_64-unknown-freebsd"
| "aarch64-unknown-freebsd"
| "i686-unknown-linux-musl"
| "i686-unknown-linux-gnu"
| "x86_64-unknown-linux-musl"
| "x86_64-unknown-linux-gnu"
| "aarch64-unknown-linux-musl"
| "aarch64-unknown-linux-gnu"
| "powerpc64le-unknown-linux-musl"
| "powerpc64le-unknown-linux-gnu";
// Values are expected to match those used in `process.platform`.
export type Platform = "darwin" | "freebsd" | "linux" | "win32";
// Values are expected to match those used in `process.arch`.
export type Arch = "arm64" | "ia32" | "x64" | "ppc64" | "universal";
// Values are expected to match those used by Visual Studio's `vcvarsall.bat`.
// See https://docs.microsoft.com/cpp/build/building-on-the-command-line?view=msvc-160#vcvarsall-syntax
export type VcVarsArch = "amd64" | "arm64" | "x86";
export type Target = {
id: TargetId;
platform: Platform;
arch: Arch;
};
export type WindowsTarget = Target & {
platform: "win32";
vcVarsArch: VcVarsArch;
};
export type LinuxTarget = Target & {
platform: "linux";
libC: typeof GLIBC | typeof MUSL;
};
export type UniversalTarget = Target & {
arch: "universal";
subtargets: Target[];
};
const aarch64AppleDarwin: Target = {
id: "aarch64-apple-darwin",
platform: "darwin",
arch: "arm64",
};
const x8664AppleDarwin: Target = {
id: "x86_64-apple-darwin",
platform: "darwin",
arch: "x64",
};
const universalAppleDarwin: UniversalTarget = {
id: "universal-apple-darwin",
platform: "darwin",
arch: "universal",
subtargets: [aarch64AppleDarwin, x8664AppleDarwin],
};
const i686PcWindowsMsvc: WindowsTarget = {
id: "i686-pc-windows-msvc",
platform: "win32",
arch: "ia32",
vcVarsArch: "x86",
};
const x8664PcWindowsMsvc: WindowsTarget = {
id: "x86_64-pc-windows-msvc",
platform: "win32",
arch: "x64",
vcVarsArch: "amd64",
};
const aarch64WindowsMsvc: WindowsTarget = {
id: "aarch64-pc-windows-msvc",
platform: "win32",
arch: "arm64",
vcVarsArch: "arm64",
};
const i686UnknownFreebsd: Target = {
id: "i686-unknown-freebsd",
platform: "freebsd",
arch: "ia32",
};
const x8664UnknownFreebsd: Target = {
id: "x86_64-unknown-freebsd",
platform: "freebsd",
arch: "x64",
};
const aarch64UnknownFreebsd: Target = {
id: "aarch64-unknown-freebsd",
platform: "freebsd",
arch: "arm64",
};
const x8664UnknownLinuxGnu: LinuxTarget = {
id: "x86_64-unknown-linux-gnu",
platform: "linux",
arch: "x64",
libC: GLIBC,
};
const x8664UnknownLinuxMusl: LinuxTarget = {
id: "x86_64-unknown-linux-musl",
platform: "linux",
arch: "x64",
libC: MUSL,
};
const i686UnknownLinuxGnu: LinuxTarget = {
id: "i686-unknown-linux-gnu",
platform: "linux",
arch: "ia32",
libC: GLIBC,
};
const i686UnknownLinuxMusl: LinuxTarget = {
id: "i686-unknown-linux-musl",
platform: "linux",
arch: "ia32",
libC: MUSL,
};
const aarch64UnknownLinuxGnu: LinuxTarget = {
id: "aarch64-unknown-linux-gnu",
platform: "linux",
arch: "arm64",
libC: GLIBC,
};
const aarch64UnknownLinuxMusl: LinuxTarget = {
id: "aarch64-unknown-linux-musl",
platform: "linux",
arch: "arm64",
libC: MUSL,
};
const powerpc64leUnknownLinuxGnu: LinuxTarget = {
id: "powerpc64le-unknown-linux-gnu",
platform: "linux",
arch: "ppc64",
libC: GLIBC,
};
const powerpc64leUnknownLinuxMusl: LinuxTarget = {
id: "powerpc64le-unknown-linux-musl",
platform: "linux",
arch: "ppc64",
libC: MUSL,
};
export const TARGETS: Record<TargetId, Target> = {
// macOS
"aarch64-apple-darwin": aarch64AppleDarwin,
"x86_64-apple-darwin": x8664AppleDarwin,
"universal-apple-darwin": universalAppleDarwin,
// Windows
"i686-pc-windows-msvc": i686PcWindowsMsvc,
"x86_64-pc-windows-msvc": x8664PcWindowsMsvc,
"aarch64-pc-windows-msvc": aarch64WindowsMsvc,
// FreeBSD
"i686-unknown-freebsd": i686UnknownFreebsd,
"x86_64-unknown-freebsd": x8664UnknownFreebsd,
"aarch64-unknown-freebsd": aarch64UnknownFreebsd,
// Linux
"i686-unknown-linux-musl": i686UnknownLinuxMusl,
"i686-unknown-linux-gnu": i686UnknownLinuxGnu,
"x86_64-unknown-linux-musl": x8664UnknownLinuxMusl,
"x86_64-unknown-linux-gnu": x8664UnknownLinuxGnu,
"aarch64-unknown-linux-musl": aarch64UnknownLinuxMusl,
"aarch64-unknown-linux-gnu": aarch64UnknownLinuxGnu,
"powerpc64le-unknown-linux-musl": powerpc64leUnknownLinuxMusl,
"powerpc64le-unknown-linux-gnu": powerpc64leUnknownLinuxGnu,
};
export function getHost(): Target | undefined {
return Object.values(TARGETS).find(
(target) =>
target.platform === process.platform &&
target.arch === process.arch &&
(process.platform !== "linux" || (target as LinuxTarget).libC === processLibC()),
);
}
export function isHostId(id: TargetId): boolean {
return getHost()?.id === id;
}
export function isHost(target: Target): boolean {
return getHost()?.id === target.id;
}
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
set -e
cd $(dirname "$0")/..
IMAGE=${DOCKER_IMAGE_NAME:-"element-desktop-dockerbuild"}
docker inspect "$IMAGE" 2> /dev/null > /dev/null
if [ $? != 0 ]; then
echo "Docker image $IMAGE not found. Have you run pnpm run docker:setup?"
exit 1
fi
mkdir -p docker/node_modules docker/.hak docker/.gnupg
# Taken from https://www.electron.build/multi-platform-build#docker
# Pass through any vars prefixed with INDOCKER_, removing the prefix
docker run --rm -ti \
--platform linux/amd64 \
--env-file <(env | grep -E '^INDOCKER_' | sed -e 's/^INDOCKER_//') \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \
-v ${PWD}/docker/node_modules:/project/node_modules \
-v ${PWD}/docker/.hak:/project/.hak \
-v ${PWD}/docker/.gnupg:/root/.gnupg \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
"$IMAGE" "$@"
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env -S npx tsx
/*
* Checks for the presence of a webapp, inspects its version and sets the
* version metadata of the package to match.
*/
import { promises as fs } from "node:fs";
import * as asar from "@electron/asar";
import * as childProcess from "node:child_process";
import * as url from "node:url";
export async function versionFromAsar(): Promise<string> {
try {
await fs.stat("webapp.asar");
} catch {
throw new Error("No 'webapp.asar' found. Run 'pnpm run fetch'");
}
return asar.extractFile("webapp.asar", "version").toString().trim();
}
export async function setPackageVersion(ver: string): Promise<void> {
// set version in package.json: electron-builder will use this to populate
// all the various version fields
await new Promise<void>((resolve, reject) => {
childProcess.execFile(
process.platform === "win32" ? "pnpm.cmd" : "pnpm",
[
"version",
"-s",
"--no-git-tag-version", // This also means "don't commit to git" as it turns out
"--new-version",
ver,
],
{
// We need shell mode on Windows to be able to launch `.cmd` executables
// See https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
shell: process.platform === "win32",
},
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
},
);
});
}
async function main(args: string[]): Promise<number> {
let version = args[0];
if (version === undefined) version = await versionFromAsar();
await setPackageVersion(version);
return 0;
}
if (import.meta.url.startsWith("file:")) {
const modulePath = url.fileURLToPath(import.meta.url);
if (process.argv[1] === modulePath) {
main(process.argv.slice(2))
.then((ret) => {
process.exit(ret);
})
.catch((e) => {
console.error(e);
process.exit(1);
});
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"moduleResolution": "node16",
"skipLibCheck": true,
"esModuleInterop": true,
"target": "es2022",
"module": "node16",
"sourceMap": false,
"strict": true,
"lib": ["es2022"],
"types": ["node"]
},
"include": ["../src/@types", "./**/*.ts"]
}
+27
View File
@@ -0,0 +1,27 @@
/*
Copyright 2021-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type BrowserWindow } from "electron";
import { type AppLocalization } from "../language-helper.js";
// global type extensions need to use var for whatever reason
/* eslint-disable no-var */
declare global {
type IConfigOptions = Record<string, any>;
var mainWindow: BrowserWindow | null;
var appQuitting: boolean;
var appLocalization: AppLocalization;
var vectorConfig: IConfigOptions;
var trayConfig: {
// eslint-disable-next-line camelcase
icon_path: string;
brand: string;
};
}
/* eslint-enable no-var */
+136
View File
@@ -0,0 +1,136 @@
/*
Copyright 2022-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
declare module "matrix-seshat" {
interface IConfig {
language?: string;
passphrase?: string;
}
/* eslint-disable camelcase */
interface IMatrixEvent {
event_id: string;
sender: string;
room_id: string;
origin_server_ts: number;
content: Record<string, any>;
}
interface IMatrixProfile {
displayname?: string;
avatar_url?: string;
}
interface ISearchArgs {
searchTerm: number;
limit: number;
before_limit: number;
after_limit: number;
order_by_recency: boolean;
next_batch?: string;
}
interface ISearchContext {
events_before: IMatrixEvent[];
events_after: IMatrixEvent[];
profile_info: { [userId: string]: IMatrixProfile };
}
interface ISearchResult {
next_batch: string;
count: number;
results: Array<{
rank: number;
result: IMatrixEvent;
context: ISearchContext;
}>;
}
/* eslint-enable camelcase */
interface ICheckpoint {
roomId: string;
token: string;
fullCrawl: boolean;
direction: "b" | "f";
}
interface IDatabaseStats {
size: number;
eventCount: number;
roomCount: number;
}
interface ILoadArgs {
roomId: string;
limit: number;
fromEvent: string;
direction: "b" | "f";
}
interface ILoadResult {
event: IMatrixEvent;
matrixProfile: IMatrixProfile;
}
export class Seshat {
public constructor(path: string, config?: IConfig);
public addEvent(matrixEvent: IMatrixEvent, profile?: IMatrixProfile): void;
public deleteEvent(eventId: string): Promise<boolean>;
public commit(force?: boolean): Promise<number>;
public commitSync(wait?: boolean, force?: boolean): number;
public reload(): void;
public search(args: ISearchArgs): Promise<ISearchResult>;
public searchSync(
term: string,
limit?: number,
beforeLimit?: number,
afterLimit?: number,
orderByRecency?: boolean,
): ISearchResult;
public addHistoricEventsSync(
events: IMatrixEvent[],
newCheckpoint?: ICheckpoint,
oldCheckpoint?: ICheckpoint,
): boolean;
public addHistoricEvents(
events: IMatrixEvent[],
newCheckpoint?: ICheckpoint,
oldCheckpoint?: ICheckpoint,
): Promise<boolean>;
public addCrawlerCheckpoint(checkpoint: ICheckpoint): Promise<void>;
public removeCrawlerCheckpoint(checkpoint: ICheckpoint): Promise<void>;
public loadCheckpoints(): Promise<ICheckpoint[]>;
public getSize(): Promise<number>;
public getStats(): Promise<IDatabaseStats>;
public delete(): Promise<void>;
public shutdown(): Promise<void>;
public changePassphrase(newPassphrase: string): Promise<void>;
public isEmpty(): Promise<boolean>;
public isRoomIndexed(roomId: string): Promise<boolean>;
public getUserVersion(): Promise<number>;
public setUserVersion(version: number): Promise<void>;
public loadFileEvents(args: ILoadArgs): Promise<ILoadResult[]>;
}
interface IRecoveryInfo {
totalEvents: number;
reindexedEvents: number;
done: number;
}
export class SeshatRecovery {
public constructor(path: string, config?: IConfig);
public info(): IRecoveryInfo;
public getUserVersion(): Promise<number>;
public shutdown(): Promise<void>;
public reindex(): Promise<void>;
}
export class ReindexError extends Error {
public constructor(message?: string);
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import BaseAutoLaunch from "auto-launch";
import Store from "./store.js";
export type AutoLaunchState = "enabled" | "minimised" | "disabled";
// Wrapper around auto-launch to get/set the `isHidden` option
export class AutoLaunch extends BaseAutoLaunch {
private static internalInstance?: AutoLaunch;
public static get instance(): AutoLaunch {
if (!AutoLaunch.internalInstance) {
if (!Store.instance) throw new Error("Store not initialized");
AutoLaunch.internalInstance = new AutoLaunch({
name: global.vectorConfig.brand || "Element",
isHidden: Store.instance.get("openAtLoginMinimised"),
mac: {
useLaunchAgent: true,
},
});
}
return AutoLaunch.internalInstance;
}
public async getState(): Promise<AutoLaunchState> {
if (!(await this.isEnabled())) {
return "disabled";
}
return this.opts.isHiddenOnLaunch ? "minimised" : "enabled";
}
public async setState(state: AutoLaunchState): Promise<void> {
const openAtLoginMinimised = state === "minimised";
Store.instance?.set("openAtLoginMinimised", openAtLoginMinimised);
this.opts.isHiddenOnLaunch = openAtLoginMinimised;
if (state !== "disabled") {
return this.enable();
} else {
return this.disable();
}
}
}
+49
View File
@@ -0,0 +1,49 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { app, ipcMain, type IpcMainEvent, nativeImage } from "electron";
import { _t } from "./language-helper.js";
// Handles calculating the correct "badge" for the window, for notifications and error states.
// Tray icon updates are handled in tray.ts
if (process.platform === "win32") {
// We only use setOverlayIcon on Windows as it's only supported on that platform, but has good support
// from all the Windows variants we support.
// https://www.electronjs.org/docs/latest/api/browser-window#winsetoverlayiconoverlay-description-windows
ipcMain.on(
"setBadgeCount",
function (_ev: IpcMainEvent, count: number, imageBuffer?: Buffer, isError?: boolean): void {
if (count === 0) {
// Flash frame is set to true in ipc.ts "loudNotification"
global.mainWindow?.flashFrame(false);
}
if (imageBuffer) {
global.mainWindow?.setOverlayIcon(
nativeImage.createFromBuffer(Buffer.from(imageBuffer)),
isError
? _t("icon_overlay|description_error")
: _t("icon_overlay|description_notifications", { count }),
);
} else {
global.mainWindow?.setOverlayIcon(null, "");
}
},
);
} else {
// only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron
// has some Windows support too, and in some Windows environments this leads to two badges rendering atop
// each other. See https://github.com/vector-im/element-web/issues/16942
ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void {
if (count === 0) {
// Flash frame is set to true in ipc.ts "loudNotification"
global.mainWindow?.flashFrame(false);
}
app.badgeCount = count;
});
}
+26
View File
@@ -0,0 +1,26 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { type JsonObject, loadJsonFile } from "./utils.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
interface BuildConfig {
appId: string;
protocol: string;
}
export function readBuildConfig(): BuildConfig {
const packageJson = loadJsonFile(path.join(__dirname, "..", "package.json")) as JsonObject;
return {
appId: (packageJson["electron_appId"] as string) || "im.riot.app",
protocol: (packageJson["electron_protocol"] as string) || "io.element.desktop",
};
}
+20
View File
@@ -0,0 +1,20 @@
/*
Copyright 2023, 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { Streams } from "electron";
type DisplayMediaCallback = (streams: Streams) => void;
let displayMediaCallback: DisplayMediaCallback | null;
export const getDisplayMediaCallback = (): DisplayMediaCallback | null => {
return displayMediaCallback;
};
export const setDisplayMediaCallback = (callback: DisplayMediaCallback | null): void => {
displayMediaCallback = callback;
};
+634
View File
@@ -0,0 +1,634 @@
/*
Copyright 2018-2025 New Vector Ltd.
Copyright 2017-2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
// Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
import "./squirrelhooks.js";
import {
app,
BrowserWindow,
Menu,
autoUpdater,
dialog,
type Input,
type Event,
session,
protocol,
desktopCapturer,
} from "electron";
// eslint-disable-next-line n/file-extension-in-import
import * as Sentry from "@sentry/electron/main";
import path, { dirname } from "node:path";
import windowStateKeeper from "electron-window-state";
import fs, { promises as afs } from "node:fs";
import { URL, fileURLToPath } from "node:url";
import minimist from "minimist";
import "./ipc.js";
import "./seshat.js";
import "./settings.js";
import "./badge.js";
import * as tray from "./tray.js";
import Store from "./store.js";
import { buildMenuTemplate } from "./vectormenu.js";
import webContentsHandler from "./webcontents-handler.js";
import * as updater from "./updater.js";
import ProtocolHandler from "./protocol.js";
import { _t, AppLocalization } from "./language-helper.js";
import { setDisplayMediaCallback } from "./displayMediaCallback.js";
import { setupMacosTitleBar } from "./macos-titlebar.js";
import { type Json, loadJsonFile } from "./utils.js";
import { setupMediaAuth } from "./media-auth.js";
import { readBuildConfig } from "./build-config.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const argv = minimist(process.argv, {
alias: { help: "h" },
});
if (argv["help"]) {
console.log("Options:");
console.log(" --profile-dir {path}: Path to where to store the profile.");
console.log(
` --profile {name}: Name of alternate profile to use, allows for running multiple accounts.\n` +
` Ignored if --profile-dir is specified.\n` +
` The ELEMENT_PROFILE_DIR environment variable may be used to change the default profile path.\n` +
` It is overridden by --profile-dir, but can be combined with --profile.`,
);
console.log(" --devtools: Install and use react-devtools and react-perf.");
console.log(
` --config: Path to the config.json file. May also be specified via the ELEMENT_DESKTOP_CONFIG_JSON environment variable.\n` +
` Otherwise use the default user location '${app.getPath("userData")}'`,
);
console.log(" --no-update: Disable automatic updating.");
console.log(" --hidden: Start the application hidden in the system tray.");
console.log(" --help: Displays this help message.");
console.log("And more such as --proxy, see: https://electronjs.org/docs/api/command-line-switches");
app.exit();
}
const LocalConfigLocation = process.env.ELEMENT_DESKTOP_CONFIG_JSON ?? argv["config"];
const LocalConfigFilename = "config.json";
// Electron creates the user data directory (with just an empty 'Dictionaries' directory...)
// as soon as the app path is set, so pick a random path in it that must exist if it's a
// real user data directory.
function isRealUserDataDir(d: string): boolean {
return fs.existsSync(path.join(d, "IndexedDB"));
}
const buildConfig = readBuildConfig();
const protocolHandler = new ProtocolHandler(buildConfig.protocol);
// check if we are passed a profile in the SSO callback url
let userDataPath: string;
const userDataPathInProtocol = protocolHandler.getProfileFromDeeplink(argv["_"]);
if (userDataPathInProtocol) {
userDataPath = userDataPathInProtocol;
} else if (argv["profile-dir"]) {
userDataPath = argv["profile-dir"];
} else {
let newUserDataPath = process.env.ELEMENT_PROFILE_DIR ?? app.getPath("userData");
if (argv["profile"]) {
newUserDataPath += "-" + argv["profile"];
}
const newUserDataPathExists = isRealUserDataDir(newUserDataPath);
let oldUserDataPath = path.join(app.getPath("appData"), app.getName().replace("Element", "Riot"));
if (argv["profile"]) {
oldUserDataPath += "-" + argv["profile"];
}
const oldUserDataPathExists = isRealUserDataDir(oldUserDataPath);
console.log(newUserDataPath + " exists: " + (newUserDataPathExists ? "yes" : "no"));
console.log(oldUserDataPath + " exists: " + (oldUserDataPathExists ? "yes" : "no"));
if (!newUserDataPathExists && oldUserDataPathExists) {
console.log("Using legacy user data path: " + oldUserDataPath);
userDataPath = oldUserDataPath;
} else {
userDataPath = newUserDataPath;
}
}
app.setPath("userData", userDataPath);
async function tryPaths(name: string, root: string, rawPaths: string[]): Promise<string> {
// Make everything relative to root
const paths = rawPaths.map((p) => path.join(root, p));
for (const p of paths) {
try {
await afs.stat(p);
return p + "/";
} catch {}
}
console.log(`Couldn't find ${name} files in any of: `);
for (const p of paths) {
console.log("\t" + path.resolve(p));
}
throw new Error(`Failed to find ${name} files`);
}
const homeserverProps = ["default_is_url", "default_hs_url", "default_server_name", "default_server_config"] as const;
let asarPathPromise: Promise<string> | undefined;
// Get the webapp resource file path, memoizes result
function getAsarPath(): Promise<string> {
if (!asarPathPromise) {
asarPathPromise = tryPaths("webapp", __dirname, [
// If run from the source checkout, this will be in the directory above
"../webapp.asar",
// but if run from a packaged application, electron-main.js will be in
// a different asar file, so it will be two levels above
"../../webapp.asar",
// also try without the 'asar' suffix to allow symlinking in a directory
"../webapp",
// from a packaged application
"../../webapp",
]);
}
return asarPathPromise;
}
function loadLocalConfigFile(): Json {
if (LocalConfigLocation) {
console.log("Loading local config: " + LocalConfigLocation);
return loadJsonFile(LocalConfigLocation);
} else {
const configDir = app.getPath("userData");
console.log(`Loading local config: ${path.join(configDir, LocalConfigFilename)}`);
return loadJsonFile(configDir, LocalConfigFilename);
}
}
let loadConfigPromise: Promise<void> | undefined;
// Loads the config from asar, and applies a config.json from userData atop if one exists
// Writes config to `global.vectorConfig`. Idempotent, returns the same promise on subsequent calls.
function loadConfig(): Promise<void> {
if (loadConfigPromise) return loadConfigPromise;
async function actuallyLoadConfig(): Promise<void> {
const asarPath = await getAsarPath();
try {
console.log(`Loading app config: ${path.join(asarPath, LocalConfigFilename)}`);
global.vectorConfig = loadJsonFile(asarPath, LocalConfigFilename);
} catch {
// it would be nice to check the error code here and bail if the config
// is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
// file or invalid json, so node is just very unhelpful.
// Continue with the defaults (ie. an empty config)
global.vectorConfig = {};
}
try {
// Load local config and use it to override values from the one baked with the build
const localConfig = loadLocalConfigFile();
// If the local config has a homeserver defined, don't use the homeserver from the build
// config. This is to avoid a problem where Riot thinks there are multiple homeservers
// defined, and panics as a result.
if (Object.keys(localConfig).find((k) => homeserverProps.includes(<any>k))) {
// Rip out all the homeserver options from the vector config
global.vectorConfig = Object.keys(global.vectorConfig)
.filter((k) => !homeserverProps.includes(<any>k))
.reduce(
(obj, key) => {
obj[key] = global.vectorConfig[key];
return obj;
},
{} as Omit<Partial<(typeof global)["vectorConfig"]>, keyof typeof homeserverProps>,
);
}
global.vectorConfig = Object.assign(global.vectorConfig, localConfig);
} catch (e) {
if (e instanceof SyntaxError) {
await app.whenReady();
void dialog.showMessageBox({
type: "error",
title: `Your ${global.vectorConfig.brand || "Element"} is misconfigured`,
message:
`Your custom ${global.vectorConfig.brand || "Element"} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${global.vectorConfig.brand || "Element"}.`,
detail: e.message || "",
});
}
// Could not load local config, this is expected in most cases.
}
// Tweak modules paths as they assume the root is at the same level as webapp, but for `vector://vector/webapp` it is not.
if (Array.isArray(global.vectorConfig.modules)) {
global.vectorConfig.modules = global.vectorConfig.modules.map((m) => {
if (m.startsWith("/")) {
return "/webapp" + m;
}
return m;
});
}
}
loadConfigPromise = actuallyLoadConfig();
return loadConfigPromise;
}
// Configure Electron Sentry and crashReporter using sentry.dsn in config.json if one is present.
async function configureSentry(): Promise<void> {
await loadConfig();
const { dsn, environment } = global.vectorConfig.sentry || {};
if (dsn) {
console.log(`Enabling Sentry with dsn=${dsn} environment=${environment}`);
Sentry.init({
dsn,
environment,
// We don't actually use this IPC, but we do not want Sentry injecting preloads
ipcMode: Sentry.IPCMode.Classic,
});
}
}
// Set up globals for Tray
async function setupGlobals(): Promise<void> {
const asarPath = await getAsarPath();
await loadConfig();
// Figure out the tray icon path & brand name
const iconFile = `icon.${process.platform === "win32" ? "ico" : "png"}`;
global.trayConfig = {
icon_path: path.join(path.dirname(asarPath), "build", iconFile),
brand: global.vectorConfig.brand || "Element",
};
}
global.appQuitting = false;
const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
(input, platform): boolean => platform !== "darwin" && input.alt && input.key.toUpperCase() === "F4",
(input, platform): boolean => platform !== "darwin" && input.control && input.key.toUpperCase() === "Q",
(input, platform): boolean =>
platform === "darwin" && input.meta && !input.control && input.key.toUpperCase() === "Q",
];
void configureSentry();
// handle uncaught errors otherwise it displays
// stack traces in popup dialogs, which is terrible (which
// it will do any time the auto update poke fails, and there's
// no other way to catch this error).
// Assuming we generally run from the console when developing,
// this is far preferable.
process.on("uncaughtException", function (error: Error): void {
console.log("Unhandled exception", error);
});
app.commandLine.appendSwitch("--enable-usermedia-screen-capturing");
if (!app.commandLine.hasSwitch("enable-features")) {
app.commandLine.appendSwitch("enable-features", "WebRTCPipeWireCapturer");
}
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
console.log("Other instance detected: exiting");
app.exit();
}
// do this after we know we are the primary instance of the app
protocolHandler.initialise(userDataPath);
// Register the scheme the app is served from as 'standard'
// which allows things like relative URLs and IndexedDB to
// work.
// Also mark it as secure (ie. accessing resources from this
// protocol and HTTPS won't trigger mixed content warnings).
protocol.registerSchemesAsPrivileged([
{
scheme: "vector",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
},
]);
// Turn the sandbox on for *all* windows we might generate. Doing this means we don't
// have to specify a `sandbox: true` to each BrowserWindow.
//
// This also fixes an issue with window.open where if we only specified the sandbox
// on the main window we'd run into cryptic "ipc_renderer be broke" errors. Turns out
// it's trying to jump the sandbox and make some calls into electron, which it can't
// do when half of it is sandboxed. By turning on the sandbox for everything, the new
// window (no matter how temporary it may be) is also sandboxed, allowing for a clean
// transition into the user's browser.
app.enableSandbox();
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
const store = Store.initialize(argv["storage-mode"]); // must be called before any async actions
// Disable hardware acceleration if the setting has been set.
if (store.get("disableHardwareAcceleration")) {
console.log("Disabling hardware acceleration.");
app.disableHardwareAcceleration();
}
app.on("ready", async () => {
console.debug("Reached Electron ready state");
let asarPath: string;
try {
asarPath = await getAsarPath();
await setupGlobals();
} catch (e) {
console.log("App setup failed: exiting", e);
process.exit(1);
// process.exit doesn't cause node to stop running code immediately,
// so return (we could let the exception propagate but then we end up
// with node printing all sorts of stuff about unhandled exceptions
// when we want the actual error to be as obvious as possible).
return;
}
if (argv["devtools"]) {
try {
const { installExtension, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer");
installExtension(REACT_DEVELOPER_TOOLS)
.then((ext) => console.log(`Added Extension: ${ext.name}`))
.catch((err: unknown) => console.log("An error occurred: ", err));
} catch (e) {
console.log(e);
}
}
protocol.registerFileProtocol("vector", (request, callback) => {
if (request.method !== "GET") {
callback({ error: -322 }); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
return null;
}
const parsedUrl = new URL(request.url);
if (parsedUrl.protocol !== "vector:") {
callback({ error: -302 }); // UNKNOWN_URL_SCHEME
return;
}
if (parsedUrl.host !== "vector") {
callback({ error: -105 }); // NAME_NOT_RESOLVED
return;
}
const target = parsedUrl.pathname.split("/");
// path starts with a '/'
if (target[0] !== "") {
callback({ error: -6 }); // FILE_NOT_FOUND
return;
}
if (target[target.length - 1] == "") {
target[target.length - 1] = "index.html";
}
let baseDir: string;
if (target[1] === "webapp") {
baseDir = asarPath;
} else {
callback({ error: -6 }); // FILE_NOT_FOUND
return;
}
// Normalise the base dir and the target path separately, then make sure
// the target path isn't trying to back out beyond its root
baseDir = path.normalize(baseDir);
const relTarget = path.normalize(path.join(...target.slice(2)));
if (relTarget.startsWith("..")) {
callback({ error: -6 }); // FILE_NOT_FOUND
return;
}
const absTarget = path.join(baseDir, relTarget);
callback({
path: absTarget,
});
});
// Minimist parses `--no-`-prefixed arguments as booleans with value `false` rather than verbatim.
if (argv["update"] === false) {
console.log("Auto update disabled via command line flag");
} else if (global.vectorConfig["update_base_url"]) {
void updater.start(global.vectorConfig["update_base_url"]);
} else {
console.log("No update_base_url is defined: auto update is disabled");
}
// Set up i18n before loading storage as we need translations for dialogs
global.appLocalization = new AppLocalization({
components: [(): void => tray.initApplicationMenu(), (): void => Menu.setApplicationMenu(buildMenuTemplate())],
store,
});
// Load the previous window state with fallback to defaults
const mainWindowState = windowStateKeeper({
defaultWidth: 1024,
defaultHeight: 768,
});
console.debug("Opening main window");
const preloadScript = path.normalize(`${__dirname}/preload.cjs`);
global.mainWindow = new BrowserWindow({
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
backgroundColor: "#fff",
titleBarStyle: process.platform === "darwin" ? "hidden" : "default",
trafficLightPosition: { x: 9, y: 8 },
icon: global.trayConfig.icon_path,
show: false,
autoHideMenuBar: store.get("autoHideMenuBar"),
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
webPreferences: {
preload: preloadScript,
nodeIntegration: false,
//sandbox: true, // We enable sandboxing from app.enableSandbox() above
contextIsolation: true,
webgl: true,
},
});
global.mainWindow.setContentProtection(store.get("enableContentProtection"));
try {
console.debug("Ensuring storage is ready");
if (!(await store.prepareSafeStorage(global.mainWindow.webContents.session))) return;
} catch (e) {
console.error(e);
app.exit(1);
}
void global.mainWindow.loadURL("vector://vector/webapp/");
if (process.platform === "darwin") {
setupMacosTitleBar(global.mainWindow);
}
// Handle spellchecker
// For some reason spellCheckerEnabled isn't persisted, so we have to use the store here
global.mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true));
// Create trayIcon icon
if (store.get("minimizeToTray")) tray.create(global.trayConfig);
global.mainWindow.once("ready-to-show", () => {
if (!global.mainWindow) return;
mainWindowState.manage(global.mainWindow);
if (!argv["hidden"]) {
global.mainWindow.show();
} else {
// hide here explicitly because window manage above sometimes shows it
global.mainWindow.hide();
}
});
global.mainWindow.webContents.on("before-input-event", (event: Event, input: Input): void => {
const exitShortcutPressed =
input.type === "keyDown" && exitShortcuts.some((shortcutFn) => shortcutFn(input, process.platform));
// We only care about the exit shortcuts here
if (!exitShortcutPressed || !global.mainWindow) return;
// Prevent the default behaviour
event.preventDefault();
// Let's ask the user if they really want to exit the app
const shouldWarnBeforeExit = store.get("warnBeforeExit", true);
if (shouldWarnBeforeExit) {
const shouldCancelCloseRequest =
dialog.showMessageBoxSync(global.mainWindow, {
type: "question",
buttons: [
_t("action|cancel"),
_t("action|close_brand", {
brand: global.vectorConfig.brand || "Element",
}),
],
message: _t("confirm_quit"),
defaultId: 1,
cancelId: 0,
}) === 0;
if (shouldCancelCloseRequest) return;
}
// Exit the app
app.exit();
});
global.mainWindow.on("closed", () => {
global.mainWindow = null;
});
global.mainWindow.on("close", async (e) => {
// If we are not quitting and have a tray icon then minimize to tray
if (!global.appQuitting && (tray.hasTray() || process.platform === "darwin")) {
// On Mac, closing the window just hides it
// (this is generally how single-window Mac apps
// behave, eg. Mail.app)
e.preventDefault();
if (global.mainWindow?.isFullScreen()) {
global.mainWindow.once("leave-full-screen", () => global.mainWindow?.hide());
global.mainWindow.setFullScreen(false);
} else {
global.mainWindow?.hide();
}
return false;
}
});
if (process.platform === "win32") {
// Handle forward/backward mouse buttons in Windows
global.mainWindow.on("app-command", (e, cmd) => {
if (cmd === "browser-backward" && global.mainWindow?.webContents.canGoBack()) {
global.mainWindow.webContents.goBack();
} else if (cmd === "browser-forward" && global.mainWindow?.webContents.canGoForward()) {
global.mainWindow.webContents.goForward();
}
});
}
webContentsHandler(global.mainWindow.webContents);
session.defaultSession.setDisplayMediaRequestHandler(
(_, callback) => {
if (process.env.XDG_SESSION_TYPE === "wayland") {
// On Wayland, calling getSources() opens the xdg-desktop-portal picker.
// The user can only select a single source there, so Electron will return an array with exactly one entry.
desktopCapturer
.getSources({ types: ["screen", "window"] })
.then((sources) => {
callback({ video: sources[0] });
})
.catch((err) => {
// If the user cancels the dialog an error occurs "Failed to get sources"
console.error("Wayland: failed to get user-selected source:", err);
callback({ video: { id: "", name: "" } }); // The promise does not return if no dummy is passed here as source
});
} else {
global.mainWindow?.webContents.send("openDesktopCapturerSourcePicker");
}
setDisplayMediaCallback(callback);
},
{ useSystemPicker: true },
); // Use Mac OS 15+ native picker
setupMediaAuth(global.mainWindow);
});
app.on("window-all-closed", () => {
app.quit();
});
app.on("activate", () => {
global.mainWindow?.show();
});
function beforeQuit(): void {
global.appQuitting = true;
global.mainWindow?.webContents.send("before-quit");
}
app.on("before-quit", beforeQuit);
autoUpdater.on("before-quit-for-update", beforeQuit);
app.on("second-instance", (ev, commandLine, workingDirectory) => {
// If other instance launched with --hidden then skip showing window
if (commandLine.includes("--hidden")) return;
// Someone tried to run a second instance, we should focus our window.
if (global.mainWindow) {
if (!global.mainWindow.isVisible()) global.mainWindow.show();
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
global.mainWindow.focus();
}
});
// This is required to make notification handlers work
// on Windows 8.1/10/11 (and is a noop on other platforms);
// It must also match the ID found in 'electron-builder'
// in order to get the title and icon to show up correctly.
// Ref: https://stackoverflow.com/a/77314604/3525780
app.setAppUserModelId(buildConfig.appId);
+93
View File
@@ -0,0 +1,93 @@
{
"action": {
"cancel": "Storno",
"close": "Zavřít",
"close_brand": "Zavřít %(brand)s",
"copy": "Zkopírovat",
"cut": "Vyjmout",
"delete": "Smazat",
"edit": "Upravit",
"minimise": "Minimalizovat",
"paste": "Vložit",
"paste_match_style": "Vložit a přizpůsobit styl",
"quit": "Ukončit",
"redo": "Znovu",
"select_all": "Vybrat vše",
"show_hide": "Zobrazit/Skrýt",
"undo": "Zpět",
"zoom_in": "Přiblížit",
"zoom_out": "Oddálit"
},
"common": {
"about": "O",
"brand_help": "%(brand)s nápověda",
"help": "Nápověda",
"no": "Ne",
"preferences": "Předvolby",
"yes": "Ano"
},
"confirm_quit": "Opravdu chcete ukončit aplikaci?",
"edit_menu": {
"speech": "Řeč",
"speech_start_speaking": "Spustit nahrávání hlasu",
"speech_stop_speaking": "Zastavit nahrávání hlasu"
},
"eol": {
"no_more_updates": "Používáte nepodporovanou verzi systému macOS. Prosím upgradujte %(brand)s pro získání aktualizací.",
"title": "Systém není podporován",
"warning": "Používáte nepodporovanou verzi systému macOS. Proveďte prosím upgrade %(brand)s, aby byl stále funkční."
},
"file_menu": {
"label": "Soubor"
},
"icon_overlay": {
"description_error": "Chyba",
"description_notifications": {
"one": "Máte %(count)s nepřečtené oznámení.",
"few": "Máte %(count)s nepřečtená oznámení.",
"other": "Máte %(count)s nepřečtených oznámení."
}
},
"menu": {
"hide": "Skrýt",
"hide_others": "Skrýt ostatní",
"services": "Služby",
"unhide": "Zrušit skrytí"
},
"right_click_menu": {
"add_to_dictionary": "Přidat do slovníku",
"copy_email": "Kopírovat e-mailovou adresu",
"copy_image": "Kopírovat obrázek",
"copy_image_url": "Kopírovat adresu obrázku",
"copy_link_url": "Kopírovat adresu odkazu",
"save_image_as": "Uložit obrázek jako...",
"save_image_as_error_description": "Obrázek se nepodařilo uložit",
"save_image_as_error_title": "Chyba při ukládání obrázku"
},
"store": {
"error": {
"backend_changed": "Vymazat data a znovu načíst?",
"backend_changed_detail": "Nelze získat přístup k tajnému klíči ze systémové klíčenky, zdá se, že se změnil.",
"backend_changed_title": "Nepodařilo se načíst databázi",
"backend_no_encryption": "Váš systém má podporovanou klíčenku, ale šifrování není k dispozici.",
"backend_no_encryption_detail": "Electron zjistil, že pro vaši klíčenku %(backend)s není k dispozici šifrování. Ujistěte se, že máte nainstalovanou klíčenku. Pokud ji máte, restartujte počítač a zkuste to znovu. Volitelně můžete povolit %(brand)s použít slabší formu šifrování.",
"backend_no_encryption_title": "Bez podpory šifrování",
"unsupported_keyring": "Váš systém má nepodporovanou klíčenku, což znamená, že databázi nelze otevřít.",
"unsupported_keyring_detail": "Detekce klíčenky Electronu nenalezla podporovaný backend. Můžete se pokusit ručně nakonfigurovat backend spuštěním %(brand)s s argumentem příkazového řádku, jednorázovou operací. Viz %(link)s.",
"unsupported_keyring_title": "Systém není podporován",
"unsupported_keyring_use_basic_text": "Používat slabší šifrování",
"unsupported_keyring_use_plaintext": "Nepoužívat žádné šifrování"
}
},
"view_menu": {
"actual_size": "Aktuální velikost",
"toggle_developer_tools": "Přepnout zobrazení nástrojů pro vývojáře",
"toggle_full_screen": "Přepnout zobrazení celé obrazovky",
"view": "Zobrazit"
},
"window_menu": {
"bring_all_to_front": "Přenést vše do popředí",
"label": "Okno",
"zoom": "Lupa"
}
}
+93
View File
@@ -0,0 +1,93 @@
{
"action": {
"cancel": "Diddymu",
"close": "Cau",
"close_brand": "Cau %(brand)s",
"copy": "Copïo",
"cut": "Torri",
"delete": "Dileu",
"edit": "Golygu",
"minimise": "Lleihau",
"paste": "Gludo",
"paste_match_style": "Arddull Gludo a Chyfateb",
"quit": "Gadael",
"redo": "Ail-wneud",
"select_all": "Dewis y Cyfan",
"show_hide": "Dangos/Cuddio",
"undo": "Dadwneud",
"zoom_in": "Chwyddo i Mewn",
"zoom_out": "Chwyddo Allan"
},
"common": {
"about": "Ynghylch",
"brand_help": "Cymorth %(brand)s",
"help": "Cymorth",
"no": "Na",
"preferences": "Dewisiadau",
"yes": "Iawn"
},
"confirm_quit": "Ydych chi'n siŵr eich bod am roi'r gorau iddi?",
"edit_menu": {
"speech": "Lleferydd",
"speech_start_speaking": "Cychwyn Llefaru",
"speech_stop_speaking": "Peidio Llefaru"
},
"eol": {
"no_more_updates": "Rydych chi'n rhedeg fersiwn o macOS nad yw'n cael ei chefnogi. Uwchraddiwch i dderbyn diweddariadau %(brand)s.",
"title": "System heb ei chefnogi",
"warning": "Rydych chi'n rhedeg fersiwn o macOS nad yw'n cael ei chefnogi. Uwchraddiwch i sicrhau bod %(brand)s yn parhau i weithio."
},
"file_menu": {
"label": "Ffeil"
},
"icon_overlay": {
"description_error": "Gwall",
"description_notifications": {
"Mae gennych chi %(count)s hysbysiadau heb eu darllen.": "zero",
"Mae gennych chi %(count)s hysbysiad heb ei ddarllen.": "one",
"Mae gennych chi %(count)s hysbysiad heb eu darllen.": "other"
}
},
"menu": {
"hide": "Cuddio",
"hide_others": "Cuddio'r Gweddill",
"services": "Gwasanaethau",
"unhide": "Datguddio"
},
"right_click_menu": {
"add_to_dictionary": "Ychwanegu at y geiriadur",
"copy_email": "Copïo cyfeiriad e-bost",
"copy_image": "Copïo delwedd",
"copy_image_url": "Copïo cyfeiriad delwedd",
"copy_link_url": "Copïo cyfeiriad y ddolen",
"save_image_as": "Cadw delwedd fel...",
"save_image_as_error_description": "Methodd cadw'r ddelwedd",
"save_image_as_error_title": "Methodd cadw'r ddelwedd"
},
"store": {
"error": {
"backend_changed": "Clirio data ac ail-lwytho?",
"backend_changed_detail": "Methu cael mynediad at y gyfrinach o allweddi'r system, mae'n ymddangos ei fod wedi newid.",
"backend_changed_title": "Methwyd llwytho'r gronfa ddata",
"backend_no_encryption": "Mae gan eich system cylch allwedd sy'n cael ei gefnogi ond nid yw amgryptio ar gael.",
"backend_no_encryption_detail": "Mae Electron wedi canfod nad yw amgryptio ar gael ar eich cylch allweddi %(backend)s. Gwnewch yn siŵr bod y cylch allweddi wedi'i osod. Os oes y cylch allweddi wedi'i osod, ail gychwynnwch a cheisiwch eto. Yn ddewisol, gallwch ganiatáu i %(brand)s ddefnyddio ffurf wannach o amgryptio.",
"backend_no_encryption_title": "Dim cefnogaeth amgryptio",
"unsupported_keyring": "Mae gan eich system allweddell nad yw'n cael ei chefnogi sy'n golygu nad oes modd agor y gronfa ddata.",
"unsupported_keyring_detail": "Heb ganfod allweddell Electron gefn. Gallwch geisio ffurfweddu'r gefn â llaw trwy gychwyn %(brand)s gyda dadl llinell orchymyn, gweithrediad untro. Gweler %(link)s.",
"unsupported_keyring_title": "System heb ei chefnogi",
"unsupported_keyring_use_basic_text": "Defnyddiwch amgryptio gwannach",
"unsupported_keyring_use_plaintext": "Peidiwch â defnyddio amgryptio"
}
},
"view_menu": {
"actual_size": "Maint Gwirioneddol",
"toggle_developer_tools": "Toglo Offer Datblygwyr",
"toggle_full_screen": "Toglo Sgrin Lawn",
"view": "Golwg"
},
"window_menu": {
"bring_all_to_front": "Popeth i'r Blaen",
"label": "Ffenestr",
"zoom": "Chwyddo"
}
}
+74
View File
@@ -0,0 +1,74 @@
{
"action": {
"cancel": "Afbryd",
"close": "Luk",
"close_brand": "Luk %(brand)s",
"copy": "Kopiér",
"cut": "Klip",
"delete": "Slet",
"edit": "Rediger",
"minimise": "Minimér",
"paste": "Indsæt",
"paste_match_style": "Indsæt og match stil",
"quit": "Luk",
"redo": "Omgør",
"select_all": "Vælg alle",
"show_hide": "Vis/skjul",
"undo": "Fortryd",
"zoom_in": "Zoom ind",
"zoom_out": "Zoom ud"
},
"common": {
"about": "Om",
"brand_help": "%(brand)sHjælp",
"help": "Hjælp",
"no": "Nej",
"preferences": "Indstillinger",
"yes": "Ja"
},
"confirm_quit": "Er du sikker på, du vil afslutte?",
"edit_menu": {
"speech": "Tale",
"speech_start_speaking": "Begynd at tale",
"speech_stop_speaking": "Stop med at tale"
},
"file_menu": {
"label": "Fil"
},
"icon_overlay": {
"description_error": "Fejl"
},
"menu": {
"hide": "Skjul",
"hide_others": "Skjul andre",
"services": "Tjenester",
"unhide": "Vis"
},
"right_click_menu": {
"add_to_dictionary": "Tilføj til ordbog",
"copy_email": "Kopier e-mailadresse",
"copy_image": "Kopier billede",
"copy_image_url": "Kopier billed-adresse",
"copy_link_url": "Kopier linkadresse",
"save_image_as": "Gem billede som...",
"save_image_as_error_description": "Billedet kunne ikke gemmes",
"save_image_as_error_title": "Kunne ikke gemme billedet"
},
"store": {
"error": {
"backend_no_encryption": "Dit system har en understøttet nøglering, men kryptering er ikke tilgængelig.",
"unsupported_keyring": "Dit system har en ikke-understøttet nøglering, hvilket betyder at databasen ikke kan åbnes."
}
},
"view_menu": {
"actual_size": "Faktisk størrelse",
"toggle_developer_tools": "Skift udviklerværktøjer",
"toggle_full_screen": "Skift fuld skærm",
"view": "Vis"
},
"window_menu": {
"bring_all_to_front": "Bring alt i front",
"label": "Vindue",
"zoom": "Zoom"
}
}
+92
View File
@@ -0,0 +1,92 @@
{
"action": {
"cancel": "Abbrechen",
"close": "Schließen",
"close_brand": "%(brand)s schließen",
"copy": "Kopieren",
"cut": "Ausschneiden",
"delete": "Löschen",
"edit": "Bearbeiten",
"minimise": "Minimieren",
"paste": "Einfügen",
"paste_match_style": "Einfügen und Formatierung beibehalten",
"quit": "Beenden",
"redo": "Wiederherstellen",
"select_all": "Alles auswählen",
"show_hide": "Anzeigen/Ausblenden",
"undo": "Rückgängig",
"zoom_in": "Vergrößern",
"zoom_out": "Verkleinern"
},
"common": {
"about": "Über",
"brand_help": "%(brand)s Hilfe",
"help": "Hilfe",
"no": "Nein",
"preferences": "Präferenzen",
"yes": "Ja"
},
"confirm_quit": "Wirklich beenden?",
"edit_menu": {
"speech": "Sprache",
"speech_start_speaking": "Aufnahme starten",
"speech_stop_speaking": "Aufnahme beenden"
},
"eol": {
"no_more_updates": "Du benutzt eine nicht unterstützte Version von macOS. Bitte aktualisiere, um Updates für %(brand)s zu erhalten.",
"title": "System nicht unterstützt",
"warning": "Du benutzt eine nicht unterstützte Version von macOS. Bitte aktualisiere, damit %(brand)s weiter funktioniert."
},
"file_menu": {
"label": "Datei"
},
"icon_overlay": {
"description_error": "Fehler",
"description_notifications": {
"one": "Du hast %(count)s ungelesene Benachrichtigung.",
"other": "Du hast %(count)s ungelesene Benachrichtigungen."
}
},
"menu": {
"hide": "Verstecken",
"hide_others": "Andere verstecken",
"services": "Dienste",
"unhide": "Wieder anzeigen"
},
"right_click_menu": {
"add_to_dictionary": "Wörterbuch hinzufügen",
"copy_email": "Email-Adresse kopieren",
"copy_image": "Bild kopieren",
"copy_image_url": "Bild-Adresse kopieren",
"copy_link_url": "Link-Adresse kopieren",
"save_image_as": "Bild speichern unter...",
"save_image_as_error_description": "Das Bild konnte nicht gespeichert werden",
"save_image_as_error_title": "Bild kann nicht gespeichert werden"
},
"store": {
"error": {
"backend_changed": "Daten löschen und neu laden?",
"backend_changed_detail": "Zugriff auf Schlüssel im Systemschlüsselbund nicht möglich, er scheint sich geändert zu haben.",
"backend_changed_title": "Datenbank konnte nicht geladen werden",
"backend_no_encryption": "Ihr System verfügt über einen unterstützten Keyring, aber die Verschlüsselung ist nicht verfügbar.",
"backend_no_encryption_detail": "Electron hat festgestellt, dass der Keyring von %(backend)s keine Verschlüsselung bietet. Wenn du den Keyring installiert hast, starte den Rechner bitte neu und versuche es erneut. Optional kann %(brand)s auch eine abgeschwächte Verschlüsselung nutzen.",
"backend_no_encryption_title": "Keine Verschlüsselungsunterstützung",
"unsupported_keyring": "Der Keyring des Systems wird nicht unterstützt. Daher kann die Datenbank nicht geöffnet werden kann.",
"unsupported_keyring_detail": "Die Keyring-Erkennung von Electron hat kein unterstütztes Backend gefunden. Du kannst einmalig versuchen, eine manuelle Konfiguration von %(brand)s über die Kommandozeile vorzunehmen. Infos unter %(link)s.",
"unsupported_keyring_title": "System nicht unterstützt",
"unsupported_keyring_use_basic_text": "Schwächere Verschlüsselung verwenden",
"unsupported_keyring_use_plaintext": "Verwende keine Verschlüsselung"
}
},
"view_menu": {
"actual_size": "Tatsächliche Größe",
"toggle_developer_tools": "Developer-Tools an/aus",
"toggle_full_screen": "Vollbildschirm an/aus",
"view": "Ansicht"
},
"window_menu": {
"bring_all_to_front": "Alles in den Vordergrund",
"label": "Fenster",
"zoom": "Zoomen"
}
}
+63
View File
@@ -0,0 +1,63 @@
{
"action": {
"cancel": "Ακύρωση",
"close": "Κλείσιμο",
"close_brand": "Κλείσιμο %(brand)s",
"copy": "Αντιγραφή",
"cut": "Αποκοπή",
"delete": "Διαγραφή",
"edit": "Επεξεργασία",
"minimise": "Ελαχιστοποίηση",
"paste": "Επικόλληση",
"paste_match_style": "Επικόλληση και Ταίριασμα Στυλ",
"quit": "Κλείσιμο",
"redo": "Επανάληψη",
"select_all": "Επιλογή Όλων",
"show_hide": "Eμφάνιση/Απόκρυψη",
"undo": "Αναίρεση",
"zoom_in": "Μεγέθυνση",
"zoom_out": "Σμίκρυνση"
},
"common": {
"about": "Σχετικά με",
"brand_help": "%(brand)s Υποστήριξη",
"help": "Βοήθεια",
"preferences": "Προτιμήσεις"
},
"confirm_quit": "Είστε βέβαιος ότι θέλετε να εγκαταλείψετε;",
"edit_menu": {
"speech": "Ομιλία",
"speech_start_speaking": "Ξεκινήστε να μιλάτε",
"speech_stop_speaking": "Τερματίστε να μιλάτε"
},
"file_menu": {
"label": "Αρχείο"
},
"menu": {
"hide": "Απόκρυψη",
"hide_others": "Απόκρυψη Άλλων",
"services": "Υπηρεσίες",
"unhide": "Εμφάνιση"
},
"right_click_menu": {
"add_to_dictionary": "Προσθήκη στο λεξικό",
"copy_email": "Αντιγραφή διεύθυνσης email",
"copy_image": "Αντιγραφή εικόνας",
"copy_image_url": "Αντιγραφή διεύθυνσης εικόνας",
"copy_link_url": "Αντιγραφή διεύθυνσης συνδέσμου",
"save_image_as": "Αποθήκευση εικόνας ως...",
"save_image_as_error_description": "Η αποθήκευση της εικόνας απέτυχε",
"save_image_as_error_title": "Αποτυχία αποθήκευσης εικόνας"
},
"view_menu": {
"actual_size": "Πραγματικό Μέγεθος",
"toggle_developer_tools": "Άνοιγμα Εργαλείων Προγραμματιστή",
"toggle_full_screen": "Εναλλαγή σε Πλήρη Οθόνη",
"view": "Προβολή"
},
"window_menu": {
"bring_all_to_front": "Μεταφορά Όλων στο Προσκήνιο",
"label": "Παράθυρο",
"zoom": "Ζουμ"
}
}
+92
View File
@@ -0,0 +1,92 @@
{
"action": {
"cancel": "Cancel",
"close": "Close",
"close_brand": "Close %(brand)s",
"copy": "Copy",
"cut": "Cut",
"delete": "Delete",
"edit": "Edit",
"minimise": "Minimize",
"paste": "Paste",
"paste_match_style": "Paste and Match Style",
"quit": "Quit",
"redo": "Redo",
"select_all": "Select All",
"show_hide": "Show/Hide",
"undo": "Undo",
"zoom_in": "Zoom In",
"zoom_out": "Zoom Out"
},
"common": {
"about": "About",
"brand_help": "%(brand)s Help",
"help": "Help",
"no": "No",
"preferences": "Preferences",
"yes": "Yes"
},
"confirm_quit": "Are you sure you want to quit?",
"edit_menu": {
"speech": "Speech",
"speech_start_speaking": "Start Speaking",
"speech_stop_speaking": "Stop Speaking"
},
"eol": {
"no_more_updates": "You are running an unsupported version of macOS. Please upgrade to receive %(brand)s updates.",
"title": "System unsupported",
"warning": "You are running an unsupported version of macOS. Please upgrade to ensure %(brand)s keeps working."
},
"file_menu": {
"label": "File"
},
"icon_overlay": {
"description_error": "Error",
"description_notifications": {
"one": "You have %(count)s unread notification.",
"other": "You have %(count)s unread notifications."
}
},
"menu": {
"hide": "Hide",
"hide_others": "Hide Others",
"services": "Services",
"unhide": "Unhide"
},
"right_click_menu": {
"add_to_dictionary": "Add to dictionary",
"copy_email": "Copy email address",
"copy_image": "Copy image",
"copy_image_url": "Copy image address",
"copy_link_url": "Copy link address",
"save_image_as": "Save image as...",
"save_image_as_error_description": "The image failed to save",
"save_image_as_error_title": "Failed to save image"
},
"store": {
"error": {
"backend_changed": "Clear data and reload?",
"backend_changed_detail": "Unable to access secret from system keyring, it appears to have changed.",
"backend_changed_title": "Failed to load database",
"backend_no_encryption": "Your system has a supported keyring but encryption is not available.",
"backend_no_encryption_detail": "Electron has detected that encryption is not available on your keyring %(backend)s. Please ensure that you have the keyring installed. If you do have the keyring installed, please reboot and try again. Optionally, you can allow %(brand)s to use a weaker form of encryption.",
"backend_no_encryption_title": "No encryption support",
"unsupported_keyring": "Your system has an unsupported keyring meaning the database cannot be opened.",
"unsupported_keyring_detail": "Electron's keyring detection did not find a supported backend. You can attempt to manually configure the backend by starting %(brand)s with a command-line argument, a one-time operation. See %(link)s.",
"unsupported_keyring_title": "System unsupported",
"unsupported_keyring_use_basic_text": "Use weaker encryption",
"unsupported_keyring_use_plaintext": "Use no encryption"
}
},
"view_menu": {
"actual_size": "Actual Size",
"toggle_developer_tools": "Toggle Developer Tools",
"toggle_full_screen": "Toggle Full Screen",
"view": "View"
},
"window_menu": {
"bring_all_to_front": "Bring All to Front",
"label": "Window",
"zoom": "Zoom"
}
}
+50
View File
@@ -0,0 +1,50 @@
{
"action": {
"cancel": "Nuligi",
"close": "Fermi",
"close_brand": "Fermu %(brand)s",
"copy": "Kopii",
"cut": "Tranĉi",
"delete": "Forigi",
"edit": "Redakti",
"minimise": "Minimumigi",
"paste": "Enmeti",
"redo": "Refari",
"select_all": "Elekti Ĉiujn",
"show_hide": "Montri/Kaŝi",
"undo": "Malfari"
},
"common": {
"about": "Prio",
"help": "Helpo",
"preferences": "Agordoj"
},
"confirm_quit": "Ĉu vi certas, ke vi volas ĉesi?",
"edit_menu": {
"speech_start_speaking": "Ekparoli",
"speech_stop_speaking": "Ĉesi Paroli"
},
"file_menu": {
"label": "Dosiero"
},
"menu": {
"hide": "Kaŝi",
"hide_others": "Kaŝi Aliajn",
"unhide": "Malkaŝi"
},
"right_click_menu": {
"copy_email": "Kopiu retadreson",
"copy_image": "Kopiu bildon",
"copy_image_url": "Kopiu adreson de la bildo",
"copy_link_url": "Kopiu ligilon de la bildo",
"save_image_as_error_description": "La bildo malsukcesis elŝutiĝi",
"save_image_as_error_title": "Malsukcesis elŝuti bildon"
},
"view_menu": {
"toggle_developer_tools": "Baskuligi Programistajn Ilojn",
"view": "Vidi"
},
"window_menu": {
"label": "Fenestro"
}
}

Some files were not shown because too many files have changed in this diff Show More