Compare commits

..

1 Commits

Author SHA1 Message Date
Kyle Harding
2b63fbed03 Remove repo config from flowzone.yml
This functionality is being deprecated in Flowzone.

See: https://github.com/product-os/flowzone/pull/833

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2023-12-19 18:15:38 -05:00
69 changed files with 11165 additions and 16866 deletions

View File

@@ -1,10 +0,0 @@
module.exports = {
extends: ["./node_modules/@balena/lint/config/.eslintrc.js"],
root: true,
ignorePatterns: ["node_modules/"],
rules: {
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-comment": "off",
},
};

454
.eslintrc.yml Normal file
View File

@@ -0,0 +1,454 @@
env:
browser: true
commonjs: true
es6: true
node: true
mocha: true
plugins:
- lodash
- jsdoc
- node
- react
extends: 'standard'
parserOptions:
sourceType: 'script'
ecmaFeatures:
jsx: true
settings:
jsdoc:
additionalTagNames:
customTags:
- fulfil
rules:
# Possible Errors
no-console:
- off
no-empty:
- error
no-extra-semi:
- error
no-negated-in-lhs:
- error
no-prototype-builtins:
- error
valid-jsdoc:
- error
- requireReturn: false
requireReturnDescription: false
requireReturnType: true
requireParamDescription: true
preferType:
boolean: "Boolean"
number: "Number"
object: "Object"
string: "String"
array: "Array"
prefer:
arg: "param"
return: "returns"
# Best Practices
array-callback-return:
- error
block-scoped-var:
- error
class-methods-use-this:
- error
complexity:
- off
consistent-return:
- error
curly:
- error
default-case:
- error
dot-notation:
- error
guard-for-in:
- error
no-alert:
- error
no-case-declarations:
- error
no-div-regex:
- error
no-else-return:
- error
no-empty-function:
- error
no-eq-null:
- error
no-extra-label:
- error
no-implicit-coercion:
- error
no-implicit-globals:
- error
no-loop-func:
- error
no-magic-numbers:
- error
no-native-reassign:
- error
no-param-reassign:
- error
no-restricted-properties:
- error
- property: __proto__
no-return-await:
- error
no-script-url:
- error
no-unused-expressions:
- error
no-unused-labels:
- error
no-useless-concat:
- error
no-void:
- error
no-warning-comments:
- off
radix:
- error
vars-on-top:
- off
# Strict mode
strict:
- error
- global
# Variables
init-declarations:
- error
- always
no-catch-shadow:
- error
no-restricted-globals:
- error
- event
no-shadow:
- error
no-undefined:
- error
no-unused-vars:
- error
no-use-before-define:
- error
# NodeJS and CommonJS
callback-return:
- error
global-require:
- off
no-mixed-requires:
- error
no-process-env:
- off
no-process-exit:
- off
no-sync:
- off
# Stylistic Issues
array-bracket-spacing:
- error
- always
capitalized-comments:
- error
- always
- ignoreConsecutiveComments: true
comma-spacing:
- error
- before: false
after: true
computed-property-spacing:
- error
- never
consistent-this:
- error
- self
func-name-matching:
- error
- always
func-names:
- error
- never
func-style:
- error
- expression
id-blacklist:
- error
id-length:
- error
- min: 2
exceptions:
- "_"
id-match:
- error
- "^[_0-9A-Za-z\\$]+$"
line-comment-position:
- error
- position: above
linebreak-style:
- error
- unix
lines-around-comment:
- error
- beforeBlockComment: true
afterBlockComment: false
beforeLineComment: true
afterLineComment: false
allowBlockStart: true
allowBlockEnd: false
allowObjectStart: true
allowObjectEnd: false
allowArrayStart: true
allowArrayEnd: false
lines-around-directive:
- error
- always
max-len:
- error
- code: 130
comments: 150
ignoreComments: false
ignoreTrailingComments: false
ignoreUrls: true
max-params:
- off
max-statements-per-line:
- error
- max: 1
multiline-ternary:
- off
newline-per-chained-call:
- off
no-bitwise:
- error
no-continue:
- error
no-inline-comments:
- error
no-lonely-if:
- error
no-mixed-operators:
- error
no-multi-assign:
- error
no-negated-condition:
- error
no-nested-ternary:
- error
no-plusplus:
- error
no-restricted-syntax:
- error
- WithStatement
- ForInStatement
no-spaced-func:
- error
no-underscore-dangle:
- error
- allowAfterThis: false
object-curly-newline:
- error
- minProperties: 3
consistent: true
object-curly-spacing:
- error
- always
one-var-declaration-per-line:
- error
- always
operator-assignment:
- error
- always
quotes:
- error
- single
quote-props:
- error
- as-needed
require-jsdoc:
- error
- require:
FunctionDeclaration: true
ClassDeclaration: true
MethodDefinition: true
ArrowFunctionExpression: true
space-before-function-paren:
- error
- anonymous: always
named: always
asyncArrow: always
template-tag-spacing:
- error
- always
unicode-bom:
- error
# ECMAScript 6
arrow-parens:
- error
- always
arrow-spacing:
- error
- before: true
after: true
generator-star-spacing:
- error
- before: true
after: false
no-confusing-arrow:
- error
no-var:
- error
object-shorthand:
- error
- always
prefer-const:
- error
prefer-spread:
- error
prefer-numeric-literals:
- error
prefer-rest-params:
- error
prefer-template:
- error
prefer-arrow-callback:
- error
- allowNamedFunctions: false
require-yield:
- error
symbol-description:
- error
# Lodash
lodash/chain-style:
- error
- explicit
lodash/identity-shorthand:
- error
- always
lodash/import-scope:
- error
- full
lodash/matches-prop-shorthand:
- error
- always
lodash/matches-shorthand:
- error
- always
lodash/no-commit:
- error
lodash/path-style:
- error
- array
lodash/prefer-compact:
- error
lodash/prefer-filter:
- error
- 5
lodash/prefer-flat-map:
- error
lodash/prefer-invoke-map:
- error
lodash/prefer-map:
- error
lodash/prefer-reject:
- error
lodash/prefer-thru:
- error
lodash/prefer-wrapper-method:
- error
lodash/prop-shorthand:
- error
- always
lodash/prefer-constant:
- error
- true
- true
lodash/prefer-get:
- error
- 2
lodash/prefer-includes:
- error
- includeNative: true
lodash/prefer-is-nil:
- error
lodash/prefer-lodash-chain:
- error
lodash/prefer-lodash-method:
- error
lodash/prefer-lodash-typecheck:
- error
lodash/prefer-matches:
- error
- 3
lodash/prefer-noop:
- error
lodash/prefer-over-quantifier:
- error
lodash/prefer-startswith:
- error
lodash/prefer-times:
- error
# JSDoc
jsdoc/check-param-names:
- error
jsdoc/check-tag-names:
- error
jsdoc/newline-after-description:
- error
jsdoc/require-example:
- error
jsdoc/require-hyphen-before-param-description:
- error
jsdoc/require-param:
- error
jsdoc/require-param-description:
- error
jsdoc/require-param-type:
- error
jsdoc/require-returns-type:
- error
# Node
node/no-deprecated-api:
- error
node/no-missing-import:
- error
node/no-missing-require:
- error
node/process-exit-as-throw:
- error
node/no-extraneous-require:
- error
node/no-extraneous-import:
- error
# React
react/jsx-uses-vars:
- error
overrides:
files: ['*.jsx']
rules:
require-jsdoc:
- off

View File

@@ -10,11 +10,11 @@ inputs:
required: true required: true
# --- custom environment # --- custom environment
XCODE_APP_LOADER_EMAIL:
type: string
default: "accounts+apple@balena.io"
NODE_VERSION: NODE_VERSION:
type: string type: string
# Beware that native modules will be built for this version,
# which might not be compatible with the one used by pkg (see forge.sidecar.ts)
# https://github.com/vercel/pkg-fetch/releases
default: "18.x" default: "18.x"
VERBOSE: VERBOSE:
type: string type: string
@@ -25,14 +25,14 @@ runs:
using: "composite" using: "composite"
steps: steps:
- name: Download custom source artifact - name: Download custom source artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }} name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
path: ${{ runner.temp }} path: ${{ runner.temp }}
- name: Extract custom source artifact - name: Extract custom source artifact
if: runner.os != 'Windows' if: runner.os != 'Windows'
shell: bash shell: pwsh
working-directory: . working-directory: .
run: tar -xf ${{ runner.temp }}/custom.tgz run: tar -xf ${{ runner.temp }}/custom.tgz
@@ -48,54 +48,22 @@ runs:
node-version: ${{ inputs.NODE_VERSION }} node-version: ${{ inputs.NODE_VERSION }}
cache: npm cache: npm
- name: Install host dependencies - name: Install yq
if: runner.os == 'Linux' shell: bash --noprofile --norc -eo pipefail -x {0}
shell: bash run: choco install yq
run: sudo apt-get install -y --no-install-recommends fakeroot dpkg rpm if: runner.os == 'Windows'
- name: Install host dependencies
if: runner.os == 'macOS'
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
# This is a temporary workaround to make the job use Python 3.11 until
# we update to npm 10+.
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4
with:
python-version: '3.11'
# https://www.electron.build/code-signing.html # https://www.electron.build/code-signing.html
# https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof # https://github.com/Apple-Actions/import-codesign-certs
- name: Import Apple code signing certificate - name: Import Apple code signing certificate
if: runner.os == 'macOS' if: runner.os == 'macOS'
shell: bash uses: apple-actions/import-codesign-certs@v1
run: | with:
KEY_CHAIN=build.keychain p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
CERTIFICATE_P12=certificate.p12 p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
# Recreate the certificate from the secure environment variable
echo $CERTIFICATE_P12_B64 | base64 --decode > $CERTIFICATE_P12
# Create a keychain
security create-keychain -p actions $KEY_CHAIN
# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p actions $KEY_CHAIN
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
# remove certs
rm -fr *.p12
env:
CERTIFICATE_P12_B64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
- name: Import Windows code signing certificate - name: Import Windows code signing certificate
if: runner.os == 'Windows' if: runner.os == 'Windows'
id: import_win_signing_cert
shell: powershell shell: powershell
run: | run: |
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
@@ -107,80 +75,95 @@ runs:
-CertStoreLocation Cert:\CurrentUser\My ` -CertStoreLocation Cert:\CurrentUser\My `
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText) -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
echo "certFilePath=${{ runner.temp }}/certificate.pfx" >> $GITHUB_OUTPUT Remove-Item -path ${{ runner.temp }} -include certificate.pfx
env: env:
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }} WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }} WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
# ... or refactor (e.g.) https://github.com/samuelmeuli/action-electron-builder
# https://github.com/product-os/scripts/tree/master/electron
# https://github.com/product-os/scripts/tree/master/shared
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
- name: Package release - name: Package release
shell: bash id: package_release
# IMPORTANT: before making changes to this step please consult @engineering in balena's chat. shell: bash --noprofile --norc -eo pipefail -x {0}
run: | run: |
## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled set -ea
# if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then
# export DEBUG='electron-forge:*,sidecar'
# fi
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
ELECTRON_BUILDER_ARCHITECTURE="${runner_arch}"
APPLICATION_VERSION="$(jq -r '.version' package.json)" APPLICATION_VERSION="$(jq -r '.version' package.json)"
HOST_ARCH="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')" ARCHITECTURE_FLAGS="--${ELECTRON_BUILDER_ARCHITECTURE}"
if [[ "${RUNNER_OS}" == Linux ]]; then if [[ $runner_os =~ linux ]]; then
PLATFORM=Linux ELECTRON_BUILDER_OS='--linux'
SHA256SUM_BIN=sha256sum TARGETS="$(yq e .linux.target[] electron-builder.yml)"
elif [[ "${RUNNER_OS}" == macOS ]]; then elif [[ $runner_os =~ darwin|macos|osx ]]; then
PLATFORM=Darwin CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
SHA256SUM_BIN='shasum -a 256' CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
ELECTRON_BUILDER_OS='--mac'
TARGETS="$(yq e .mac.target[] electron-builder.yml)"
elif [[ "${RUNNER_OS}" == Windows ]]; then elif [[ $runner_os =~ windows|win ]]; then
PLATFORM=Windows ARCHITECTURE_FLAGS="--ia32 ${ARCHITECTURE_FLAGS}"
SHA256SUM_BIN=sha256sum CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
CSC_LINK=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
ELECTRON_BUILDER_OS='--win'
TARGETS="$(yq e .win.target[] electron-builder.yml)"
else else
echo "ERROR: unexpected runner OS: ${RUNNER_OS}"
exit 1 exit 1
fi fi
# Currently, we can only build for the host architecture. npm link electron-builder
npx electron-forge make
for target in ${TARGETS}; do
electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \
--c.extraMetadata.analytics.sentry.token='https://739bbcfc0ba4481481138d3fc831136d@o95242.ingest.sentry.io/4504451487301632' \
--c.extraMetadata.analytics.amplitude.token='balena-etcher' \
--c.extraMetadata.packageType="${target}"
find dist -type f -maxdepth 1
done
echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT
# collect all artifacts from subdirectories under a common top-level directory
mkdir -p dist
find ./out/make -type f \( \
-iname "*.zip" -o \
-iname "*.dmg" -o \
-iname "*.rpm" -o \
-iname "*.deb" -o \
-iname "*.AppImage" -o \
-iname "*Setup.exe" \
\) -ls -exec cp '{}' dist/ \;
if [[ -n "${SHA256SUM_BIN}" ]]; then
# Compute and save digests.
cd dist/
${SHA256SUM_BIN} *.* >"SHA256SUMS.${PLATFORM}.${HOST_ARCH}.txt"
fi
env: env:
# ensure we sign the artifacts # Apple notarization (afterSignHook.js)
NODE_ENV: production XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
# analytics tokens
SENTRY_TOKEN: https://739bbcfc0ba4481481138d3fc831136d@o95242.ingest.sentry.io/4504451487301632
AMPLITUDE_TOKEN: 'balena-etcher'
# Apple notarization
XCODE_APP_LOADER_EMAIL: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }} XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
XCODE_APP_LOADER_TEAM_ID: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_TEAM_ID }} # https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
# Windows signing # https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
WINDOWS_SIGNING_CERT_PATH: ${{ steps.import_win_signing_cert.outputs.certFilePath }} CSC_FOR_PULL_REQUEST: true
WINDOWS_SIGNING_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
# https://www.electron.build/auto-update.html#staged-rollouts
- name: Configure staged rollout(s)
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
percentage="$(cat < repo.yml | yq e .triggerNotification.stagingPercentage)"
find dist -type f -maxdepth 1 \
-name "latest*.yml" \
-exec yq -i e .version=\"${{ steps.package_release.outputs.version }}\" {} \;
find dist -type f -maxdepth 1 \
-name "latest*.yml" \
-exec yq -i e .stagingPercentage=\"$percentage\" {} \;
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }} name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
path: dist path: dist
retention-days: 1 retention-days: 1
if-no-files-found: error

View File

@@ -12,7 +12,7 @@ inputs:
# --- custom environment # --- custom environment
NODE_VERSION: NODE_VERSION:
type: string type: string
default: "18.18" default: "16.x"
VERBOSE: VERBOSE:
type: string type: string
default: "true" default: "true"
@@ -28,44 +28,27 @@ runs:
node-version: ${{ inputs.NODE_VERSION }} node-version: ${{ inputs.NODE_VERSION }}
cache: npm cache: npm
- name: Install host dependencies
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get install -y --no-install-recommends xvfb libudev-dev
cat < package.json | jq -r '.hostDependencies[][]' - | \
xargs -L1 echo | sed 's/|//g' | xargs -L1 \
sudo apt-get --ignore-missing install || true
- name: Install host dependencies
if: runner.os == 'macOS'
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
# This is a temporary workaround to make the job use Python 3.11 until
# we update to npm 10+.
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4
with:
python-version: '3.11'
- name: Test release - name: Test release
shell: bash shell: bash --noprofile --norc -eo pipefail -x {0}
run: | run: |
## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled set -ea
# if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then
# export DEBUG='electron-forge:*,sidecar'
# fi
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
npm run flowzone-preinstall-${runner_os}
npm ci npm ci
npm run lint npm run build
npm run package npm run test-${runner_os}
npm run test
env: env:
# https://www.electronjs.org/docs/latest/api/environment-variables # https://www.electronjs.org/docs/latest/api/environment-variables
ELECTRON_NO_ATTACH_CONSOLE: 'true' ELECTRON_NO_ATTACH_CONSOLE: true
- name: Compress custom source - name: Compress custom source
if: runner.os != 'Windows' if: runner.os != 'Windows'
shell: bash shell: pwsh
run: tar -acf ${{ runner.temp }}/custom.tgz . run: tar -acf ${{ runner.temp }}/custom.tgz .
- name: Compress custom source - name: Compress custom source
@@ -74,8 +57,8 @@ runs:
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -acf ${{ runner.temp }}\custom.tgz . run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -acf ${{ runner.temp }}\custom.tgz .
- name: Upload custom artifact - name: Upload custom artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }} name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
path: ${{ runner.temp }}/custom.tgz path: ${{ runner.temp }}/custom.tgz
retention-days: 1 retention-days: 1

View File

@@ -18,7 +18,7 @@ jobs:
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target') (github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
secrets: inherit secrets: inherit
with: with:
tests_run_on: '["ubuntu-20.04","windows-2019","macos-12","macos-latest-xlarge"]' tests_run_on: '["ubuntu-20.04","macos-latest","windows-2019"]'
restrict_custom_actions: false restrict_custom_actions: false
github_prerelease: true github_prerelease: true
cloudflare_website: "etcher" cloudflare_website: "etcher"

View File

@@ -6,9 +6,8 @@ jobs:
publish: publish:
runs-on: windows-latest # action can only be run on windows runs-on: windows-latest # action can only be run on windows
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@v2 - uses: vedantmgoyal2009/winget-releaser@v1
with: with:
identifier: Balena.Etcher identifier: Balena.Etcher
# matches something like "balenaEtcher-1.19.0.Setup.exe" installers-regex: 'balenaEtcher-Setup.*.exe$'
installers-regex: 'balenaEtcher-[\d.-]+\.Setup.exe$'
token: ${{ secrets.WINGET_PAT }} token: ${{ secrets.WINGET_PAT }}

113
.gitignore vendored
View File

@@ -1,103 +1,41 @@
# -- ADD NEW ENTRIES AT THE END OF THE FILE ---
# Logs # Logs
logs /logs
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data # Runtime data
pids pids
*.pid *.pid
*.seed *.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov /lib-cov
# Image stream output directory
/tests/image-stream/output
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage /coverage
*.lcov
# nyc test coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.nyc_output .grunt
# node-waf configuration # node-waf configuration
.lock-wscript .lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html) # Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release /build
# Dependency directories # Generated files
node_modules/ /generated
jspm_packages/ /binaries
# TypeScript v1 declaration files # Dependency directory
typings/ # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
# TypeScript cache # Compiled Etcher releases
*.tsbuildinfo /dist
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Vite
.vite/
# Electron-Forge
out/
# ---- Do not modify entries above this line ----
# Build artifacts
dist/
# Certificates # Certificates
*.spc *.spc
@@ -107,17 +45,16 @@ dist/
*.crt *.crt
*.pem *.pem
# Secrets # OSX files
.DS_Store
# VSCode files
.vscode
.gitsecret/keys/random_seed .gitsecret/keys/random_seed
!*.secret !*.secret
secrets/APPLE_SIGNING_PASSWORD.txt secrets/APPLE_SIGNING_PASSWORD.txt
secrets/WINDOWS_SIGNING_PASSWORD.txt secrets/WINDOWS_SIGNING_PASSWORD.txt
secrets/XCODE_APP_LOADER_PASSWORD.txt secrets/XCODE_APP_LOADER_PASSWORD.txt
secrets/WINDOWS_SIGNING.pfx secrets/WINDOWS_SIGNING.pfx
# Image stream output directory
/tests/image-stream/output
#local development
.yalc
yalc.lock

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "scripts/resin"]
path = scripts/resin
url = https://github.com/balena-io/scripts.git
branch = master

2
.nvmrc
View File

@@ -1 +1 @@
18 16

View File

@@ -1,186 +1,3 @@
- commits:
- subject: "patch: remove screensaver error when not on etcher-pro"
hash: 196fd8ae24de2a23ebaeae736c6ca41007162fa1
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: fix typo in IPC server id"
hash: 5d436992423961258ad861c01e3b9b30f3317aab
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.4
title: ""
date: 2024-01-26T17:29:27.301Z
- commits:
- subject: Update dependencies
hash: 0f2b4dbc106c55fe104f0b10e62c35c16bcfe9b3
body: >
- upgrade pretty_bytes to 6.1.1
- upgrade electron-remote to 2.1.0
- upgrade semver to 7.5.4 + @types/semver to 7.5.6
- upgrade chai to 4.3.11 + @types/chai to 4.3.10
- upgrade mocha to 10.2.0 + @types/mocha to 10.0.6
- upgrade sinon to 17.0.1 + @types/sinon to 17.0.2
- remove useless @types
- upgrade @svgr/webpack to 8.1.0
- upgrade @sentry/electron to 4.15.1
- upgrade tslib to 2.6.2
- upgrade immutable to 4.3.4
- upgrade redux to 4.2.1
- upgrade ts-node to 10.9.2 & ts-loader to 9.5.1
- remove mini-css-extract-plugin
- upgrade husky to 8.0.3
- upgrade uuid to 9.0.1
- upgrade lint-staged to 15.2.1
- upgrade @types/node to 18.11.9
- upgrade @fortawesome/fontawesome-free to 6.5.1
- upgrade i18next to 23.7.8 & react-i18next to 11.18.6
- bump react, react-dom + related @types to 17.0.2 and rendition to 35.1.0
- fix getuid for ts
- fix @types/react being in wrong deps
- upgrade @types/tmp to 0.2.6
- upgrade typescript to 5.3.3
- upgrade @types/mime-types to 2.1.4
- remove d3 from deps
- upgrade electron-updater to 6.1.7
- upgrade rendition to 35.1.2
- upgrade node-ipc to 9.2.3
- upgrade @types/node-ipc to 9.2.3
- upgrade electron to 27.1.3
- upgrade @electron-forge/* to 7.2.0
- upgrade @reforged/marker-appimage to 3.3.2
- upgrade style-loader to 3.3.3
- upgrade balena-lint to 7.2.4
- run CI with node 18.19
- add xxhash-addon to sidecar assets
footer:
Change-type: patch
change-type: patch
author: Edwin Joassart
nested: []
version: 1.19.3
title: ""
date: 2023-12-22T16:13:00.924Z
- commits:
- subject: "fix: typos"
hash: aaac1336702b7ac4a07992f41db4f0bcdb931c70
body: ""
footer:
Change-type: patch
change-type: patch
author: Rotzbua
nested: []
version: 1.19.2
title: ""
date: 2023-12-22T12:57:35.441Z
- commits:
- subject: "patch: update winget-releaser v2"
hash: ea184eb6352b7988c6ab1f439d30c297610cd84e
body: ""
footer: {}
author: Vedant
nested: []
version: 1.19.1
title: ""
date: 2023-12-22T08:12:34.451Z
- commits:
- subject: Use native ARM runner for Apple Silicon builds
hash: 01a96bb6de1ff00d20f7784469dd05286069e014
body: ""
footer:
Change-type: minor
change-type: minor
author: Akis Kesoglou
nested: []
- subject: Calculate and upload build artifact sha256 checksums
hash: 2e3a75e685258961bc8efdb95dde12727b93a04a
body: ""
footer:
Change-type: minor
change-type: minor
author: Akis Kesoglou
nested: []
- subject: Migrate build pipeline to Electron Forge
hash: bd33c5b092cb5224c8dfc4d5a2caf4684cee161d
body: ""
footer:
Change-type: minor
change-type: minor
author: Akis Kesoglou
nested: []
version: 1.19.0
title: ""
date: 2023-12-21T16:41:57.426Z
- commits:
- subject: Remove repo config from flowzone.yml
hash: ecb24dad251fbb9b3f92e5b404b66aedd155a584
body: |
This functionality is being deprecated in Flowzone.
See: https://github.com/product-os/flowzone/pull/833
footer:
Change-type: patch
change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
signed-off-by: Kyle Harding <kyle@balena.io>
author: Kyle Harding
nested: []
- subject: Update actions/upload-artifact to v4
hash: a970f55b555f69c5fcb40374eb50ad7b98cc8f96
body: |
Also ensure we are generating unique artifact names on upload.
footer:
Change-type: patch
change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
signed-off-by: Kyle Harding <kyle@balena.io>
See: https://github.com/product-os/flowzone/pull/827
see: https://github.com/product-os/flowzone/pull/827
author: Kyle Harding
nested: []
version: 1.18.14
title: ""
date: 2023-12-20T16:23:00.875Z
- commits: - commits:
- subject: "patch: upgrade to electron 25" - subject: "patch: upgrade to electron 25"
hash: f38bca290fe26121bed58d1131265e1aa350ddb5 hash: f38bca290fe26121bed58d1131265e1aa350ddb5

View File

@@ -3,40 +3,6 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
# v1.19.4
## (2024-01-26)
* patch: remove screensaver error when not on etcher-pro [Edwin Joassart]
* patch: fix typo in IPC server id [Edwin Joassart]
# v1.19.3
## (2023-12-22)
* Update dependencies [Edwin Joassart]
# v1.19.2
## (2023-12-22)
* fix: typos [Rotzbua]
# v1.19.1
## (2023-12-22)
* patch: update winget-releaser v2 [Vedant]
# v1.19.0
## (2023-12-21)
* Use native ARM runner for Apple Silicon builds [Akis Kesoglou]
* Calculate and upload build artifact sha256 checksums [Akis Kesoglou]
* Migrate build pipeline to Electron Forge [Akis Kesoglou]
# v1.18.14
## (2023-12-20)
* Remove repo config from flowzone.yml [Kyle Harding]
* Update actions/upload-artifact to v4 [Kyle Harding]
# v1.18.13 # v1.18.13
## (2023-10-16) ## (2023-10-16)

152
Makefile Normal file
View File

@@ -0,0 +1,152 @@
# ---------------------------------------------------------------------
# Build configuration
# ---------------------------------------------------------------------
RESIN_SCRIPTS ?= ./scripts/resin
export NPM_VERSION ?= 6.14.8
S3_BUCKET = artifacts.ci.balena-cloud.com
# This directory will be completely deleted by the `clean` rule
BUILD_DIRECTORY ?= dist
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
$(BUILD_DIRECTORY):
mkdir $@
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
mkdir $@
SHELL := /bin/bash
# ---------------------------------------------------------------------
# Operating system and architecture detection
# ---------------------------------------------------------------------
# http://stackoverflow.com/a/12099167
ifeq ($(OS),Windows_NT)
PLATFORM = win32
ifeq ($(PROCESSOR_ARCHITEW6432),AMD64)
HOST_ARCH = x64
else
ifeq ($(PROCESSOR_ARCHITECTURE),AMD64)
HOST_ARCH = x64
endif
ifeq ($(PROCESSOR_ARCHITECTURE),x86)
HOST_ARCH = x86
endif
endif
else
ifeq ($(shell uname -s),Linux)
PLATFORM = linux
ifeq ($(shell uname -m),x86_64)
HOST_ARCH = x64
endif
ifneq ($(filter %86,$(shell uname -m)),)
HOST_ARCH = x86
endif
ifeq ($(shell uname -m),armv7l)
HOST_ARCH = armv7hf
endif
ifeq ($(shell uname -m),aarch64)
HOST_ARCH = aarch64
endif
ifeq ($(shell uname -m),armv8)
HOST_ARCH = aarch64
endif
ifeq ($(shell uname -m),arm64)
HOST_ARCH = aarch64
endif
endif
ifeq ($(shell uname -s),Darwin)
PLATFORM = darwin
ifeq ($(shell uname -m),x86_64)
HOST_ARCH = x64
endif
ifeq ($(shell uname -m),arm64)
HOST_ARCH = aarch64
endif
endif
endif
ifndef PLATFORM
$(error We could not detect your host platform)
endif
ifndef HOST_ARCH
$(error We could not detect your host architecture)
endif
# Default to host architecture. You can override by doing:
#
# make <target> TARGET_ARCH=<arch>
#
TARGET_ARCH ?= $(HOST_ARCH)
# ---------------------------------------------------------------------
# Electron
# ---------------------------------------------------------------------
electron-develop:
git submodule update --init && \
npm ci && \
npm run webpack
electron-test:
$(RESIN_SCRIPTS)/electron/test.sh \
-b $(shell pwd) \
-s $(PLATFORM)
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
tiffutil -cathidpicheck $^ -out $@
electron-build: assets/dmg/background.tiff | $(BUILD_TEMPORARY_DIRECTORY)
$(RESIN_SCRIPTS)/electron/build.sh \
-b $(shell pwd) \
-r $(TARGET_ARCH) \
-s $(PLATFORM) \
-v production \
-n $(BUILD_TEMPORARY_DIRECTORY)/npm
# ---------------------------------------------------------------------
# Phony targets
# ---------------------------------------------------------------------
TARGETS = \
help \
info \
lint \
test \
clean \
distclean \
electron-develop \
electron-test \
electron-build
.PHONY: $(TARGETS)
lint:
npm run lint
test:
npm run test
help:
@echo "Available targets: $(TARGETS)"
info:
@echo "Platform : $(PLATFORM)"
@echo "Host arch : $(HOST_ARCH)"
@echo "Target arch : $(TARGET_ARCH)"
clean:
rm -rf $(BUILD_DIRECTORY)
distclean: clean
rm -rf node_modules
rm -rf dist
rm -rf generated
rm -rf $(BUILD_TEMPORARY_DIRECTORY)
.DEFAULT_GOAL = help

View File

@@ -17,9 +17,13 @@ was written correctly, and much more. It can also directly flash Raspberry Pi de
## Supported Operating Systems ## Supported Operating Systems
- Linux; most distros; Intel 64-bit. - Linux (most distros)
- Windows 10 and later; Intel 64-bit. - macOS 10.10 (Yosemite) and later
- macOS 10.13 (High Sierra) and later; both Intel and Apple Silicon. - Microsoft Windows 7 and later
**Note**: Etcher will run on any platform officially supported by
[Electron][electron]. Read more in their
[documentation][electron-supported-platforms].
## Installers ## Installers

31
afterPack.js Normal file
View File

@@ -0,0 +1,31 @@
'use strict'
const cp = require('child_process')
const fs = require('fs')
const outdent = require('outdent')
const path = require('path')
exports.default = function(context) {
if (context.packager.platform.name !== 'linux') {
return
}
const scriptPath = path.join(context.appOutDir, context.packager.executableName)
const binPath = scriptPath + '.bin'
cp.execFileSync('mv', [scriptPath, binPath])
fs.writeFileSync(
scriptPath,
outdent({trimTrailingNewline: false})`
#!/bin/bash
# Resolve symlinks. Warning, readlink -f doesn't work on MacOS/BSD
script_dir="$(dirname "$(readlink -f "\${BASH_SOURCE[0]}")")"
if [[ $EUID -ne 0 ]] || [[ $ELECTRON_RUN_AS_NODE ]]; then
"\${script_dir}"/${context.packager.executableName}.bin "$@"
else
"\${script_dir}"/${context.packager.executableName}.bin "$@" --no-sandbox
fi
`
)
cp.execFileSync('chmod', ['+x', scriptPath])
}

25
afterSignHook.js Normal file
View File

@@ -0,0 +1,25 @@
'use strict'
const { notarize } = require('electron-notarize')
const { ELECTRON_SKIP_NOTARIZATION } = process.env
async function main(context) {
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin' || ELECTRON_SKIP_NOTARIZATION === 'true') {
return
}
const appName = context.packager.appInfo.productFilename
const appleId = process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io'
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD
// https://github.com/electron/notarize/blob/main/README.md
await notarize({
appBundleId: 'io.balena.etcher',
appPath: `${appOutDir}/${appName}.app`,
appleId,
appleIdPassword
})
}
exports.default = main

4
dev-app-update.yml Normal file
View File

@@ -0,0 +1,4 @@
owner: balena-io
repo: etcher
provider: github
updaterCacheDirName: balena-etcher-updater

9
dictionary.txt Normal file
View File

@@ -0,0 +1,9 @@
boolen->boolean
aknowledge->acknowledge
seleted->selected
reming->remind
locl->local
subsribe->subscribe
unsubsribe->unsubscribe
calcluate->calculate
dictionaty->dictionary

View File

@@ -75,7 +75,9 @@ cd etcher
#### GUI #### GUI
```sh ```sh
# Build and start application # Build the GUI
npm run webpack #or npm run build
# Start Electron
npm start npm start
``` ```
@@ -102,6 +104,7 @@ systems as they can before sending a pull request.
*The test suite is run automatically by CI servers when you send a pull *The test suite is run automatically by CI servers when you send a pull
request.* request.*
We make use of [EditorConfig] to communicate indentation, line endings and We make use of [EditorConfig] to communicate indentation, line endings and
other text editing default. We encourage you to install the relevant plugin in other text editing default. We encourage you to install the relevant plugin in
your text editor of choice to avoid having to fix any issues during the review your text editor of choice to avoid having to fix any issues during the review
@@ -110,8 +113,7 @@ process.
Updating a dependency Updating a dependency
--------------------- ---------------------
- Install new version of dependency using npm - Commit *both* `package.json` and `package-lock.json`.
- Commit *both* `package.json` and `npm-shrinkwrap.json`.
Diffing Binaries Diffing Binaries
---------------- ----------------

View File

@@ -58,18 +58,25 @@ export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
##### Clean dist folder ##### Clean dist folder
Delete `.webpack` and `out/`. **NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`)
##### Generating artifacts ##### Generating artifacts
The artifacts are generated by the CI and published as draft-release or pre-release. The artifacts are generated by the CI and published as draft-release or pre-release.
Etcher is built with electron-forge. Run: `electron-builder` is used to create the packaged application.
``` #### Mac OS
npm run make
```
Our CI will appropriately sign artifacts for macOS and some Windows targets. **ATTENTION:** For production releases you'll need the code-signing key,
and set `CSC_NAME` to generate signed binaries on Mac OS.
#### Windows
**ATTENTION:** For production releases you'll need the code-signing key,
and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows.
**NOTE:**
- Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`.
### Uploading packages to Cloudfront ### Uploading packages to Cloudfront

View File

@@ -36,17 +36,14 @@ employee by asking for it from the relevant people.
Packaging Packaging
--------- ---------
Run the following command on each platform: The resulting installers will be saved to `dist/out`.
Run the following commands on all platforms with the right arguments:
```sh ```sh
npm run make ./node_modules/electron-builder build <...>
``` ```
This will produce all targets (eg. zip, dmg) specified in forge.config.ts for the
host platform and architecture.
The resulting artifacts can be found in `out/make`.
Publishing to Cloudfront Publishing to Cloudfront
--------------------- ---------------------

110
electron-builder.yml Normal file
View File

@@ -0,0 +1,110 @@
# https://www.electron.build/configuration/configuration
appId: io.balena.etcher
copyright: Copyright 2016-2023 Balena Ltd
productName: balenaEtcher
afterPack: ./afterPack.js
afterSign: ./afterSignHook.js
asar: false
files:
- generated
- lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
- lib/shared/catalina-sudo/sudo-askpass.osascript-en.js
mac:
icon: assets/icon.icns
category: public.app-category.developer-tools
hardenedRuntime: true
entitlements: "entitlements.mac.plist"
entitlementsInherit: "entitlements.mac.plist"
artifactName: "${productName}-${version}.${ext}"
target:
- dmg
dmg:
background: assets/dmg/background.tiff
icon: assets/icon.icns
iconSize: 110
contents:
- x: 140
y: 225
- x: 415
y: 225
type: link
path: /Applications
window:
width: 540
height: 405
win:
icon: assets/icon.ico
target:
- zip
- nsis
- portable
nsis:
oneClick: true
runAfterFinish: true
installerIcon: assets/icon.ico
uninstallerIcon: assets/icon.ico
deleteAppDataOnUninstall: true
license: LICENSE
artifactName: "${productName}-Setup-${version}.${ext}"
portable:
artifactName: "${productName}-Portable-${version}.${ext}"
requestExecutionLevel: user
linux:
icon: assets/iconset
target:
- AppImage
- rpm
- deb
category: Utility
packageCategory: utils
executableName: balena-etcher
synopsis: balenaEtcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.
appImage:
artifactName: ${productName}-${version}-${env.ELECTRON_BUILDER_ARCHITECTURE}.${ext}
deb:
priority: optional
compression: bzip2
depends:
- gconf-service
- gconf2
- libasound2
- libatk1.0-0
- libc6
- libcairo2
- libcups2
- libdbus-1-3
- libexpat1
- libfontconfig1
- libfreetype6
- libgbm1
- libgcc1
- libgconf-2-4
- libgdk-pixbuf2.0-0
- libglib2.0-0
- libgtk-3-0
- liblzma5
- libnotify4
- libnspr4
- libnss3
- libpango1.0-0 | libpango-1.0-0
- libstdc++6
- libx11-6
- libxcomposite1
- libxcursor1
- libxdamage1
- libxext6
- libxfixes3
- libxi6
- libxrandr2
- libxrender1
- libxss1
- libxtst6
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
afterInstall: "./after-install.tpl"
rpm:
depends:
- util-linux
protocols:
name: etcher
schemes:
- etcher

View File

@@ -1,158 +0,0 @@
import type { ForgeConfig } from '@electron-forge/shared-types';
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerZIP } from '@electron-forge/maker-zip';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerRpm } from '@electron-forge/maker-rpm';
import { MakerDMG } from '@electron-forge/maker-dmg';
import { MakerAppImage } from '@reforged/maker-appimage';
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
import { mainConfig, rendererConfig } from './webpack.config';
import * as sidecar from './forge.sidecar';
import { hostDependencies, productDescription } from './package.json';
const osxSigningConfig: any = {};
let winSigningConfig: any = {};
if (process.env.NODE_ENV === 'production') {
osxSigningConfig.osxNotarize = {
tool: 'notarytool',
appleId: process.env.XCODE_APP_LOADER_EMAIL,
appleIdPassword: process.env.XCODE_APP_LOADER_PASSWORD,
teamId: process.env.XCODE_APP_LOADER_TEAM_ID,
};
winSigningConfig = {
certificateFile: process.env.WINDOWS_SIGNING_CERT_PATH,
certificatePassword: process.env.WINDOWS_SIGNING_PASSWORD,
};
}
const config: ForgeConfig = {
packagerConfig: {
asar: true,
icon: './assets/icon',
executableName:
process.platform === 'linux' ? 'balena-etcher' : 'balenaEtcher',
appBundleId: 'io.balena.etcher',
appCategoryType: 'public.app-category.developer-tools',
appCopyright: 'Copyright 2016-2023 Balena Ltd',
darwinDarkModeSupport: true,
protocols: [{ name: 'etcher', schemes: ['etcher'] }],
extraResource: [
'lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js',
'lib/shared/catalina-sudo/sudo-askpass.osascript-en.js',
],
osxSign: {
optionsForFile: () => ({
entitlements: './entitlements.mac.plist',
hardenedRuntime: true,
}),
},
...osxSigningConfig,
},
rebuildConfig: {},
makers: [
new MakerZIP(),
new MakerSquirrel({
setupIcon: 'assets/icon.ico',
...winSigningConfig,
}),
new MakerDMG({
background: './assets/dmg/background.tiff',
icon: './assets/icon.icns',
iconSize: 110,
contents: ((opts: { appPath: string }) => {
return [
{ x: 140, y: 250, type: 'file', path: opts.appPath },
{ x: 415, y: 250, type: 'link', path: '/Applications' },
];
}) as any, // type of MakerDMGConfig omits `appPath`
additionalDMGOptions: {
window: {
size: {
width: 540,
height: 425,
},
position: {
x: 400,
y: 500,
},
},
},
}),
new MakerAppImage({
options: {
icon: './assets/icon.png',
categories: ['Utility'],
},
}),
new MakerRpm({
options: {
icon: './assets/icon.png',
categories: ['Utility'],
productDescription,
requires: ['util-linux'],
},
}),
new MakerDeb({
options: {
icon: './assets/icon.png',
categories: ['Utility'],
section: 'utils',
priority: 'optional',
productDescription,
scripts: {
postinst: './after-install.tpl',
},
depends: hostDependencies['debian'],
},
}),
],
plugins: [
new AutoUnpackNativesPlugin({}),
new WebpackPlugin({
mainConfig,
renderer: {
config: rendererConfig,
nodeIntegration: true,
entryPoints: [
{
html: './lib/gui/app/index.html',
js: './lib/gui/app/renderer.ts',
name: 'main_window',
preload: {
js: './lib/gui/app/preload.ts',
},
},
],
},
}),
new sidecar.SidecarPlugin(),
],
hooks: {
readPackageJson: async (_config, packageJson) => {
packageJson.analytics = {};
if (process.env.SENTRY_TOKEN) {
packageJson.analytics.sentry = {
token: process.env.SENTRY_TOKEN,
};
}
if (process.env.AMPLITUDE_TOKEN) {
packageJson.analytics.amplitude = {
token: 'balena-etcher',
};
}
// packageJson.packageType = 'dmg' | 'AppImage' | 'rpm' | 'deb' | 'zip' | 'nsis' | 'portable'
return packageJson;
},
},
};
export default config;

View File

@@ -1,168 +0,0 @@
import { PluginBase } from '@electron-forge/plugin-base';
import {
ForgeHookMap,
ResolvedForgeConfig,
} from '@electron-forge/shared-types';
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
import { DefinePlugin } from 'webpack';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as d from 'debug';
const debug = d('sidecar');
function isStartScrpt(): boolean {
return process.env.npm_lifecycle_event === 'start';
}
function addWebpackDefine(
config: ResolvedForgeConfig,
defineName: string,
binDir: string,
binName: string,
): ResolvedForgeConfig {
config.plugins.forEach((plugin) => {
if (plugin.name !== 'webpack' || !(plugin instanceof WebpackPlugin)) {
return;
}
const { mainConfig } = plugin.config as any;
if (mainConfig.plugins == null) {
mainConfig.plugins = [];
}
const value = isStartScrpt()
? // on `npm start`, point directly to the binary
path.resolve(binDir, binName)
: // otherwise point relative to the resources folder of the bundled app
binName;
debug(`define '${defineName}'='${value}'`);
mainConfig.plugins.push(
new DefinePlugin({
// expose path to helper via this webpack define
[defineName]: JSON.stringify(value),
}),
);
});
return config;
}
function build(
sourcesDir: string,
buildForArchs: string,
binDir: string,
binName: string,
) {
const commands: Array<[string, string[], object?]> = [
['tsc', ['--project', 'tsconfig.sidecar.json', '--outDir', sourcesDir]],
];
buildForArchs.split(',').forEach((arch) => {
const binPath = isStartScrpt()
? // on `npm start`, we don't know the arch we're building for at the time we're
// adding the webpack define, so we just build under binDir
path.resolve(binDir, binName)
: // otherwise build in arch-specific directory within binDir
path.resolve(binDir, arch, binName);
// FIXME: rebuilding mountutils shouldn't be necessary, but it is.
// It's coming from etcher-sdk, a fix has been upstreamed but to use
// the latest etcher-sdk we need to upgrade axios at the same time.
commands.push(['npm', ['rebuild', 'mountutils', `--arch=${arch}`]]);
commands.push([
'pkg',
[
path.join(sourcesDir, 'util', 'api.js'),
'-c',
'pkg-sidecar.json',
// `--no-bytecode` so that we can cross-compile for arm64 on x64
'--no-bytecode',
'--public',
'--public-packages',
'"*"',
// always build for host platform and node version
// https://github.com/vercel/pkg-fetch/releases
'--target',
`node18-${arch}`,
'--output',
binPath,
],
]);
});
commands.forEach(([cmd, args, opt]) => {
debug('running command:', cmd, args.join(' '));
execFileSync(cmd, args, { shell: true, stdio: 'inherit', ...opt });
});
}
function copyArtifact(
buildPath: string,
arch: string,
binDir: string,
binName: string,
) {
const binPath = isStartScrpt()
? // on `npm start`, we don't know the arch we're building for at the time we're
// adding the webpack define, so look for the binary directly under binDir
path.resolve(binDir, binName)
: // otherwise look into arch-specific directory within binDir
path.resolve(binDir, arch, binName);
// buildPath points to appPath, which is inside resources dir which is the one we actually want
const resourcesPath = path.dirname(buildPath);
const dest = path.resolve(resourcesPath, path.basename(binPath));
debug(`copying '${binPath}' to '${dest}'`);
fs.copyFileSync(binPath, dest);
}
export class SidecarPlugin extends PluginBase<void> {
name = 'sidecar';
constructor() {
super();
this.getHooks = this.getHooks.bind(this);
debug('isStartScript:', isStartScrpt());
}
getHooks(): ForgeHookMap {
const DEFINE_NAME = 'ETCHER_UTIL_BIN_PATH';
const BASE_DIR = path.join('out', 'sidecar');
const SRC_DIR = path.join(BASE_DIR, 'src');
const BIN_DIR = path.join(BASE_DIR, 'bin');
const BIN_NAME = `etcher-util${process.platform === 'win32' ? '.exe' : ''}`;
return {
resolveForgeConfig: async (currentConfig) => {
debug('resolveForgeConfig');
return addWebpackDefine(currentConfig, DEFINE_NAME, BIN_DIR, BIN_NAME);
},
generateAssets: async (_config, platform, arch) => {
debug('generateAssets', { platform, arch });
build(SRC_DIR, arch, BIN_DIR, BIN_NAME);
},
packageAfterCopy: async (
_config,
buildPath,
electronVersion,
platform,
arch,
) => {
debug('packageAfterCopy', {
buildPath,
electronVersion,
platform,
arch,
});
copyArtifact(buildPath, arch, BIN_DIR, BIN_NAME);
},
};
}
}

View File

@@ -38,6 +38,7 @@ import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage'; import MainPage from './pages/main/MainPage';
import './css/main.css'; import './css/main.css';
import * as i18next from 'i18next'; import * as i18next from 'i18next';
import { promises } from 'dns';
import { SourceMetadata } from '../../shared/typings/source-selector'; import { SourceMetadata } from '../../shared/typings/source-selector';
window.addEventListener( window.addEventListener(
@@ -134,7 +135,7 @@ function setDrives(drives: Dictionary<DrivelistDrive>) {
} }
} }
// Spawning the child process without privileges to get the drives list // Spwaning the child process without privileges to get the drives list
// TODO: clean up this mess of exports // TODO: clean up this mess of exports
export let requestMetadata: any; export let requestMetadata: any;

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg'; import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import * as sourceDestination from 'etcher-sdk/build/source-destination/'; import * as sourceDestination from 'etcher-sdk/build/source-destination/';
import * as React from 'react'; import * as React from 'react';
@@ -42,7 +42,7 @@ import {
Table, Table,
} from '../../styled-components'; } from '../../styled-components';
import { SourceMetadata } from '../../../../shared/typings/source-selector'; import { SourceMetadata } from '../source-selector/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next'; import * as i18next from 'i18next';
@@ -310,17 +310,9 @@ export class DriveSelector extends React.Component<
case compatibility.system(): case compatibility.system():
return warning.systemDrive(); return warning.systemDrive();
case compatibility.tooSmall(): case compatibility.tooSmall():
return warning.tooSmall( const size =
{ this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
size: return warning.tooSmall({ size }, drive);
this.state.image?.recommendedDriveSize ||
this.state.image?.size ||
0,
},
drive,
);
default:
return '';
} }
} }
@@ -436,10 +428,11 @@ export class DriveSelector extends React.Component<
) : ( ) : (
<> <>
<DrivesTable <DrivesTable
refFn={() => { refFn={(t) => {
// noop if (t !== null) {
t.setRowSelection(selectedList);
}
}} }}
checkedItems={selectedList}
checkedRowsNumber={selectedList.length} checkedRowsNumber={selectedList.length}
multipleSelection={this.props.multipleSelection} multipleSelection={this.props.multipleSelection}
columns={this.tableColumns} columns={this.tableColumns}
@@ -449,10 +442,7 @@ export class DriveSelector extends React.Component<
isDrivelistDrive(row) && row.isSystem ? ['system'] : [] isDrivelistDrive(row) && row.isSystem ? ['system'] : []
} }
rowKey="displayName" rowKey="displayName"
onCheck={(rows) => { onCheck={(rows: Drive[]) => {
if (rows == null) {
rows = [];
}
let newSelection = rows.filter(isDrivelistDrive); let newSelection = rows.filter(isDrivelistDrive);
if (this.props.multipleSelection) { if (this.props.multipleSelection) {
if (rows.length === 0) { if (rows.length === 0) {

View File

@@ -1,10 +1,11 @@
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { Badge, Flex, Txt, ModalProps } from 'rendition'; import { Badge, Flex, Txt, ModalProps } from 'rendition';
import { Modal, ScrollableFlex } from '../../styled-components'; import { Modal, ScrollableFlex } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
import prettyBytes from 'pretty-bytes'; import * as prettyBytes from 'pretty-bytes';
import { DriveWithWarnings } from '../../pages/main/Flash'; import { DriveWithWarnings } from '../../pages/main/Flash';
import * as i18next from 'i18next'; import * as i18next from 'i18next';

View File

@@ -15,8 +15,9 @@
*/ */
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg'; import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-check.svg'; import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-xmark.svg'; import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
import outdent from 'outdent';
import * as React from 'react'; import * as React from 'react';
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition'; import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';

View File

@@ -17,7 +17,7 @@
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg'; import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { Box, Checkbox, Flex, Txt } from 'rendition'; import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
import { version, packageType } from '../../../../../package.json'; import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
@@ -61,9 +61,7 @@ const EPInfo = etcherProInfo();
const InfoBox = (props: any) => ( const InfoBox = (props: any) => (
<Box fontSize={14}> <Box fontSize={14}>
<Txt>{props.label}</Txt> <Txt>{props.label}</Txt>
<Txt code copy={props.value}> <TextWithCopy code text={props.value} copy={props.value} />
{props.value}{' '}
</Txt>
</Box> </Box>
); );

View File

@@ -17,7 +17,7 @@
import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg'; import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg'; import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg'; import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg'; import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg'; import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
import { ipcRenderer, IpcRendererEvent } from 'electron'; import { ipcRenderer, IpcRendererEvent } from 'electron';
@@ -388,9 +388,10 @@ export class SourceSelector extends React.Component<
SourceType: Source, SourceType: Source,
auth?: Authentication, auth?: Authentication,
): { promise: Promise<void>; cancel: () => void } { ): { promise: Promise<void>; cancel: () => void } {
let cancelled = false;
return { return {
cancel: () => { cancel: () => {
// noop cancelled = true;
}, },
promise: (async () => { promise: (async () => {
const sourcePath = isString(selected) ? selected : selected.device; const sourcePath = isString(selected) ? selected : selected.device;
@@ -518,8 +519,8 @@ export class SourceSelector extends React.Component<
} }
private async onDrop(event: React.DragEvent<HTMLDivElement>) { private async onDrop(event: React.DragEvent<HTMLDivElement>) {
const file = event.dataTransfer.files.item(0); const [file] = event.dataTransfer.files;
if (file != null) { if (file) {
await this.selectSource(file.path, 'File').promise; await this.selectSource(file.path, 'File').promise;
} }
} }
@@ -580,7 +581,7 @@ export class SourceSelector extends React.Component<
imageLoading, imageLoading,
} = this.state; } = this.state;
const selectionImage = selectionState.getImage(); const selectionImage = selectionState.getImage();
let image = let image: SourceMetadata | DrivelistDrive =
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata); selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
image = image.drive ?? image; image = image.drive ?? image;
@@ -683,7 +684,7 @@ export class SourceSelector extends React.Component<
style={{ style={{
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)', boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
}} }}
title={ titleElement={
<span> <span>
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '} <ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
<span>{this.state.warning.title}</span> <span>{this.state.warning.title}</span>

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import * as React from 'react'; import * as React from 'react';
import { Flex, FlexProps, Txt } from 'rendition'; import { Flex, FlexProps, Txt } from 'rendition';

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>balenaEtcher</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<main id="main"></main>
<script src="http://localhost:3030/gui.js"></script>
</body>
</html>

View File

@@ -3,8 +3,10 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>balenaEtcher</title> <title>balenaEtcher</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head> </head>
<body> <body>
<main id="main"></main> <main id="main"></main>
<script src="gui.js"></script>
</body> </body>
</html> </html>

View File

@@ -47,7 +47,13 @@ export function isFlashing(): boolean {
*/ */
export function setFlashingFlag() { export function setFlashingFlag() {
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods // see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.send('disable-screensaver'); try {
electron.ipcRenderer.invoke('disable-screensaver');
} catch (error) {
console.log(
"Can't disable-screensaver, we're probably not running on a balena-electron env",
);
}
store.dispatch({ store.dispatch({
type: Actions.SET_FLASHING_FLAG, type: Actions.SET_FLASHING_FLAG,
data: {}, data: {},
@@ -70,8 +76,7 @@ export function unsetFlashingFlag(results: {
data: results, data: results,
}); });
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods // see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.invoke('enable-screensaver');
electron.ipcRenderer.send('enable-screensaver');
} }
export function setDevicePaths(devicePaths: string[]) { export function setDevicePaths(devicePaths: string[]) {

View File

@@ -43,7 +43,7 @@ function blink(t: number) {
return Math.floor(t) % 2; return Math.floor(t) % 2;
} }
function one() { function one(_t: number) {
return 1; return 1;
} }

View File

@@ -52,7 +52,7 @@ export const anonymizeSentryData = (
return event; return event;
}; };
const extractPathRegex = /(.*)(^|\s)(file:\/\/)?(\w:)?([\\/].+)/; const extractPathRegex = /(.*)(^|\s)(file\:\/\/)?(\w\:)?([\\\/].+)/;
const etcherSegmentMarkers = ['app.asar', 'Resources']; const etcherSegmentMarkers = ['app.asar', 'Resources'];
export const anonymizePath = (input: string) => { export const anonymizePath = (input: string) => {
@@ -156,7 +156,7 @@ function flattenObject(obj: any) {
const toReturn: AnalyticsPayload = {}; const toReturn: AnalyticsPayload = {};
for (const i in obj) { for (const i in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, i)) { if (!obj.hasOwnProperty(i)) {
continue; continue;
} }
@@ -168,7 +168,7 @@ function flattenObject(obj: any) {
if (typeof obj[i] === 'object' && obj[i] !== null) { if (typeof obj[i] === 'object' && obj[i] !== null) {
const flatObject = flattenObject(obj[i]); const flatObject = flattenObject(obj[i]);
for (const x in flatObject) { for (const x in flatObject) {
if (!Object.prototype.hasOwnProperty.call(flatObject, x)) { if (!flatObject.hasOwnProperty(x)) {
continue; continue;
} }

View File

@@ -18,6 +18,7 @@ import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import * as packageJSON from '../../../../package.json'; import * as packageJSON from '../../../../package.json';
import * as permissions from '../../../shared/permissions'; import * as permissions from '../../../shared/permissions';
import { getAppPath } from '../../../shared/get-app-path';
import * as errors from '../../../shared/errors'; import * as errors from '../../../shared/errors';
const THREADS_PER_CPU = 16; const THREADS_PER_CPU = 16;
@@ -26,8 +27,8 @@ const THREADS_PER_CPU = 16;
// the stdout maxBuffer size to be exceeded when flashing // the stdout maxBuffer size to be exceeded when flashing
ipc.config.silent = true; ipc.config.silent = true;
async function writerArgv(): Promise<string[]> { function writerArgv(): string[] {
let entryPoint = await window.etcher.getEtcherUtilPath(); let entryPoint = path.join(getAppPath(), 'generated', 'etcher-util');
// AppImages run over FUSE, so the files inside the mount point // AppImages run over FUSE, so the files inside the mount point
// can only be accessed by the user that mounted the AppImage. // can only be accessed by the user that mounted the AppImage.
// This means we can't re-spawn Etcher as root from the same // This means we can't re-spawn Etcher as root from the same
@@ -74,7 +75,7 @@ async function spawnChild({
IPC_SERVER_ID: string; IPC_SERVER_ID: string;
IPC_SOCKET_ROOT: string; IPC_SOCKET_ROOT: string;
}) { }) {
const argv = await writerArgv(); const argv = writerArgv();
const env = writerEnv(IPC_CLIENT_ID, IPC_SERVER_ID, IPC_SOCKET_ROOT); const env = writerEnv(IPC_CLIENT_ID, IPC_SERVER_ID, IPC_SOCKET_ROOT);
if (withPrivileges) { if (withPrivileges) {
return await permissions.elevateCommand(argv, { return await permissions.elevateCommand(argv, {
@@ -112,10 +113,10 @@ function startApiAndSpawnChild({
// server/client has a different name. // server/client has a different name.
const IPC_SERVER_ID = `etcher-server-${process.pid}-${Date.now()}-${ const IPC_SERVER_ID = `etcher-server-${process.pid}-${Date.now()}-${
withPrivileges ? 'privileged' : 'unprivileged' withPrivileges ? 'privileged' : 'unprivileged'
}`; }}}`;
const IPC_CLIENT_ID = `etcher-client-${process.pid}-${Date.now()}-${ const IPC_CLIENT_ID = `etcher-client-${process.pid}-${Date.now()}-${
withPrivileges ? 'privileged' : 'unprivileged' withPrivileges ? 'privileged' : 'unprivileged'
}`; }}`;
const IPC_SOCKET_ROOT = path.join( const IPC_SOCKET_ROOT = path.join(
process.env.XDG_RUNTIME_DIR || os.tmpdir(), process.env.XDG_RUNTIME_DIR || os.tmpdir(),

View File

@@ -25,6 +25,7 @@ import * as settings from '../models/settings';
import * as analytics from '../modules/analytics'; import * as analytics from '../modules/analytics';
import * as windowProgress from '../os/window-progress'; import * as windowProgress from '../os/window-progress';
import { startApiAndSpawnChild } from './api'; import { startApiAndSpawnChild } from './api';
import { terminateScanningServer } from '../app';
/** /**
* @summary Handle a flash error and log it to analytics * @summary Handle a flash error and log it to analytics
@@ -80,13 +81,7 @@ async function performWrite(
console.log({ image, drives }); console.log({ image, drives });
// Spawn the child process with privileges and wait for the connection to be made return await new Promise(async (resolve, reject) => {
const { emit, registerHandler, terminateServer } =
await startApiAndSpawnChild({
withPrivileges: true,
});
return await new Promise((resolve, reject) => {
const flashResults: FlashResults = {}; const flashResults: FlashResults = {};
const analyticsData = { const analyticsData = {
@@ -97,7 +92,7 @@ async function performWrite(
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
}; };
const onFail = ({ device, error }: { device: any; error: any }) => { const onFail = ({ device, error }) => {
console.log('fail event'); console.log('fail event');
console.log(device); console.log(device);
console.log(error); console.log(error);
@@ -108,7 +103,7 @@ async function performWrite(
finish(); finish();
}; };
const onDone = (event: any) => { const onDone = (event) => {
console.log('done event'); console.log('done event');
event.results.errors = event.results.errors.map( event.results.errors = event.results.errors.map(
(data: Dictionary<any> & { message: string }) => { (data: Dictionary<any> & { message: string }) => {
@@ -135,7 +130,7 @@ async function performWrite(
console.log('Flash results', flashResults); console.log('Flash results', flashResults);
// The flash wasn't cancelled and we didn't get a 'done' event // The flash wasn't cancelled and we didn't get a 'done' event
// Catch unexpected situation // Catch unexepected situation
if ( if (
!flashResults.cancelled && !flashResults.cancelled &&
!flashResults.skip && !flashResults.skip &&
@@ -156,6 +151,12 @@ async function performWrite(
resolve(flashResults); resolve(flashResults);
}; };
// Spawn the child process with privileges and wait for the connection to be made
const { emit, registerHandler, terminateServer } =
await startApiAndSpawnChild({
withPrivileges: true,
});
registerHandler('state', onProgress); registerHandler('state', onProgress);
registerHandler('fail', onFail); registerHandler('fail', onFail);
registerHandler('done', onDone); registerHandler('done', onDone);
@@ -165,15 +166,15 @@ async function performWrite(
cancelEmitter = (cancelStatus: string) => emit(cancelStatus); cancelEmitter = (cancelStatus: string) => emit(cancelStatus);
// Now that we know we're connected we can instruct the child process to start the write // Now that we know we're connected we can instruct the child process to start the write
const parameters = { const paramaters = {
image, image,
destinations: drives, destinations: drives,
SourceType: image.SourceType, SourceType: image.SourceType,
autoBlockmapping, autoBlockmapping,
decompressFirst, decompressFirst,
}; };
console.log('params', parameters); console.log('params', paramaters);
emit('write', parameters); emit('write', paramaters);
}); });
// The process continue in the event handler // The process continue in the event handler

View File

@@ -14,8 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/gear.svg'; import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-question.svg'; import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
import * as path from 'path'; import * as path from 'path';
import * as prettyBytes from 'pretty-bytes'; import * as prettyBytes from 'pretty-bytes';
@@ -116,10 +116,10 @@ interface MainPageState {
} }
export class MainPage extends React.Component< export class MainPage extends React.Component<
object, {},
MainPageState & MainPageStateFromStore MainPageState & MainPageStateFromStore
> { > {
constructor(props: object) { constructor(props: {}) {
super(props); super(props);
this.state = { this.state = {
current: 'main', current: 'main',

View File

@@ -1,12 +0,0 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
import * as webapi from '../webapi';
declare global {
interface Window {
etcher: typeof webapi;
}
}
window['etcher'] = webapi;

View File

@@ -6,4 +6,10 @@ import { ipcRenderer } from 'electron';
ipcRenderer.send('change-lng', langParser()); ipcRenderer.send('change-lng', langParser());
if (module.hot) {
module.hot.accept('./app', () => {
main();
});
}
main(); main();

View File

@@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { import {
Alert as AlertBase, Alert as AlertBase,
@@ -112,25 +113,14 @@ export const DetailsText = (props: FlexProps) => (
const modalFooterShadowCss = css` const modalFooterShadowCss = css`
overflow: auto; overflow: auto;
background: background: 0, linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%, 0,
0,
linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%,
0,
linear-gradient(rgba(255, 255, 255, 0), rgba(221, 225, 240, 0.5) 70%) 0 100%; linear-gradient(rgba(255, 255, 255, 0), rgba(221, 225, 240, 0.5) 70%) 0 100%;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
100% 40px,
100% 40px,
100% 8px,
100% 8px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: white; background-color: white;
background-size: background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
100% 40px,
100% 40px,
100% 8px,
100% 8px;
background-attachment: local, local, scroll, scroll; background-attachment: local, local, scroll, scroll;
`; `;
@@ -246,15 +236,16 @@ export interface GenericTableProps<T> extends BaseTableProps<T> {
showWarnings?: boolean; showWarnings?: boolean;
} }
function GenericTable<T>( const GenericTable: <T>(
props: GenericTableProps<T>, props: GenericTableProps<T>,
): React.ReactElement<GenericTableProps<T>> { ) => React.ReactElement<GenericTableProps<T>> = <T extends {}>({
return ( refFn,
...props
}: GenericTableProps<T>) => (
<div> <div>
<BaseTable<T> ref={props.refFn} {...props} /> <BaseTable<T> ref={refFn} {...props} />
</div> </div>
); );
}
function StyledTable<T>() { function StyledTable<T>() {
return styled((props: GenericTableProps<T>) => ( return styled((props: GenericTableProps<T>) => (
@@ -293,6 +284,7 @@ function StyledTable<T>() {
[data-display='table-body'] > [data-display='table-row'] { [data-display='table-body'] > [data-display='table-row'] {
> [data-display='table-cell']:first-child { > [data-display='table-cell']:first-child {
padding-left: 15px; padding-left: 15px;
width: 6%;
} }
> [data-display='table-cell']:last-child { > [data-display='table-cell']:last-child {
@@ -327,7 +319,7 @@ function StyledTable<T>() {
`; `;
} }
export const Table = <T extends object>(props: GenericTableProps<T>) => { export const Table = <T extends {}>(props: GenericTableProps<T>) => {
const TypedStyledFunctional = StyledTable<T>(); const TypedStyledFunctional = StyledTable<T>();
return <TypedStyledFunctional {...props} />; return <TypedStyledFunctional {...props} />;
}; };

View File

@@ -14,12 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
// whether you're running in development or production).
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
import * as electron from 'electron'; import * as electron from 'electron';
import * as remoteMain from '@electron/remote/main'; import * as remoteMain from '@electron/remote/main';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
@@ -40,8 +34,6 @@ import * as SentryMain from '@sentry/electron/main';
import * as packageJSON from '../../package.json'; import * as packageJSON from '../../package.json';
import { anonymizeSentryData } from './app/modules/analytics'; import { anonymizeSentryData } from './app/modules/analytics';
import { delay } from '../shared/utils';
const customProtocol = 'etcher'; const customProtocol = 'etcher';
const scheme = `${customProtocol}://`; const scheme = `${customProtocol}://`;
const updatablePackageTypes = ['appimage', 'nsis', 'dmg']; const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
@@ -146,6 +138,14 @@ electron.app.on('open-url', async (event, data) => {
await selectImageURL(data); await selectImageURL(data);
}); });
interface AutoUpdaterConfig {
autoDownload?: boolean;
autoInstallOnAppQuit?: boolean;
allowPrerelease?: boolean;
fullChangelog?: boolean;
allowDowngrade?: boolean;
}
async function createMainWindow() { async function createMainWindow() {
const fullscreen = Boolean(await settings.get('fullscreen')); const fullscreen = Boolean(await settings.get('fullscreen'));
const defaultWidth = settings.DEFAULT_WIDTH; const defaultWidth = settings.DEFAULT_WIDTH;
@@ -176,7 +176,6 @@ async function createMainWindow() {
contextIsolation: false, contextIsolation: false,
webviewTag: true, webviewTag: true,
zoomFactor: width / defaultWidth, zoomFactor: width / defaultWidth,
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
}, },
}); });
@@ -196,11 +195,17 @@ async function createMainWindow() {
// Prevent external resources from being loaded (like images) // Prevent external resources from being loaded (like images)
// when dropping them on the WebView. // when dropping them on the WebView.
// See https://github.com/electron/electron/issues/5919 // See https://github.com/electron/electron/issues/5919
mainWindow.webContents.on('will-navigate', (event: any) => { mainWindow.webContents.on('will-navigate', (event) => {
event.preventDefault(); event.preventDefault();
}); });
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); mainWindow.loadURL(
`file://${path.join(
'/',
...__dirname.split(path.sep).map(encodeURIComponent),
'index.html',
)}`,
);
const page = mainWindow.webContents; const page = mainWindow.webContents;
remoteMain.enable(page); remoteMain.enable(page);
@@ -236,20 +241,6 @@ electron.app.on('before-quit', () => {
process.exit(EXIT_CODES.SUCCESS); process.exit(EXIT_CODES.SUCCESS);
}); });
// this is replaced at build-time with the path to helper binary,
// relative to the app resources directory.
declare const ETCHER_UTIL_BIN_PATH: string;
electron.ipcMain.handle('get-util-path', () => {
if (process.env.NODE_ENV === 'development') {
// In development there is no "app bundle" and we're working directly with
// artifacts from the "out" directory, where this value point to.
return ETCHER_UTIL_BIN_PATH;
}
// In any other case, resolve the helper relative to resources path.
return path.resolve(process.resourcesPath, ETCHER_UTIL_BIN_PATH);
});
async function main(): Promise<void> { async function main(): Promise<void> {
if (!electron.app.requestSingleInstanceLock()) { if (!electron.app.requestSingleInstanceLock()) {
electron.app.quit(); electron.app.quit();
@@ -281,7 +272,7 @@ async function main(): Promise<void> {
const webview = electron.webContents.fromId(id); const webview = electron.webContents.fromId(id);
// Open link in browser if it's opened as a 'foreground-tab' // Open link in browser if it's opened as a 'foreground-tab'
webview!.setWindowOpenHandler((event) => { webview.setWindowOpenHandler((event) => {
const url = new URL(event.url); const url = new URL(event.url);
if ( if (
(url.protocol === 'http:' || url.protocol === 'https:') && (url.protocol === 'http:' || url.protocol === 'https:') &&
@@ -296,13 +287,6 @@ async function main(): Promise<void> {
}); });
} }
} }
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
// tslint:disable-next-line:no-var-requires
if (require('electron-squirrel-startup')) {
app.quit();
}
main(); main();
console.time('ready-to-show'); console.time('ready-to-show');

View File

@@ -1,15 +0,0 @@
//
// Anything exported from this module will become available to the
// renderer process via preload. They're accessible as `window.etcher.foo()`.
//
import { ipcRenderer } from 'electron';
// FIXME: this is a workaround for the renderer to be able to find the etcher-util
// binary. We should instead export a function that asks the main process to launch
// the binary itself.
export async function getEtcherUtilPath(): Promise<string> {
const utilPath = await ipcRenderer.invoke('get-util-path');
console.log(utilPath);
return utilPath;
}

10
lib/pkg-sidekick.json Normal file
View File

@@ -0,0 +1,10 @@
{
"bin": "build/util/child-writer.js",
"pkg": {
"assets": [
"node_modules/usb/prebuilds/darwin-x64+arm64/node.napi.node",
"node_modules/lzma-native/prebuilds/darwin-arm64/node.napi.node",
"node_modules/drivelist/build/Release/drivelist.node"
]
}
}

View File

@@ -19,6 +19,7 @@ import { join } from 'path';
import { env } from 'process'; import { env } from 'process';
import { promisify } from 'util'; import { promisify } from 'util';
import { getAppPath } from '../get-app-path';
import { supportedLocales } from '../../gui/app/i18n'; import { supportedLocales } from '../../gui/app/i18n';
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
@@ -26,15 +27,6 @@ const execFileAsync = promisify(execFile);
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED'; const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n`; const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n`;
function getAskPassScriptPath(lang: string): string {
if (process.env.NODE_ENV === 'development') {
// Force webpack's hand to bundle the script.
return require.resolve(`./sudo-askpass.osascript-${lang}.js`);
}
// Otherwise resolve the script relative to resources path.
return join(process.resourcesPath, `sudo-askpass.osascript-${lang}.js`);
}
export async function sudo( export async function sudo(
command: string, command: string,
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> { ): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
@@ -55,7 +47,11 @@ export async function sudo(
encoding: 'utf8', encoding: 'utf8',
env: { env: {
PATH: env.PATH, PATH: env.PATH,
SUDO_ASKPASS: getAskPassScriptPath(lang), SUDO_ASKPASS: join(
getAppPath(),
__dirname,
`sudo-askpass.osascript-${lang}.js`,
),
}, },
}, },
); );

View File

@@ -0,0 +1,12 @@
export function getAppPath(): string {
return (
(require('electron').app || require('@electron/remote').app)
.getAppPath()
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
// include the app-x64 or app-arm64 folder depending on the arch.
// We don't care about the app.asar file, we want the actual folder.
.replace(/\.asar$/, () =>
process.platform === 'darwin' ? '-' + process.arch : '',
)
);
}

View File

@@ -16,7 +16,7 @@
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import { outdent } from 'outdent'; import { outdent } from 'outdent';
import prettyBytes from 'pretty-bytes'; import * as prettyBytes from 'pretty-bytes';
import '../gui/app/i18n'; import '../gui/app/i18n';
import * as i18next from 'i18next'; import * as i18next from 'i18next';

View File

@@ -70,14 +70,14 @@ export async function isElevated(): Promise<boolean> {
} }
return true; return true;
} }
return process.geteuid!() === UNIX_SUPERUSER_USER_ID; return process.geteuid() === UNIX_SUPERUSER_USER_ID;
} }
/** /**
* @summary Check if the current process is running with elevated permissions * @summary Check if the current process is running with elevated permissions
*/ */
export function isElevatedUnixSync(): boolean { export function isElevatedUnixSync(): boolean {
return process.geteuid!() === UNIX_SUPERUSER_USER_ID; return process.geteuid() === UNIX_SUPERUSER_USER_ID;
} }
function escapeSh(value: any): string { function escapeSh(value: any): string {

View File

@@ -15,18 +15,17 @@
*/ */
import * as ipc from 'node-ipc'; import * as ipc from 'node-ipc';
import { Dictionary, values } from 'lodash';
import type { MultiDestinationProgress } from 'etcher-sdk/build/multi-write';
import { toJSON } from '../shared/errors'; import { toJSON } from '../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../shared/exit-codes'; import { GENERAL_ERROR, SUCCESS } from '../shared/exit-codes';
import { delay } from '../shared/utils'; import { delay } from '../shared/utils';
import { WriteOptions } from './types/types'; import { WriteOptions } from './types/types';
import { MultiDestinationProgress } from 'etcher-sdk/build/multi-write';
import { write, cleanup } from './child-writer'; import { write, cleanup } from './child-writer';
import { startScanning } from './scanner'; import { startScanning } from './scanner';
import { getSourceMetadata } from './source-metadata'; import { getSourceMetadata } from './source-metadata';
import { DrivelistDrive } from '../shared/drive-constraints'; import { DrivelistDrive } from '../shared/drive-constraints';
import { Dictionary, values } from 'lodash';
ipc.config.id = process.env.IPC_CLIENT_ID as string; ipc.config.id = process.env.IPC_CLIENT_ID as string;
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string; ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
@@ -41,7 +40,6 @@ ipc.config.silent = true;
// The purpose behind this change is for this process // The purpose behind this change is for this process
// to emit a "disconnect" event as soon as the GUI // to emit a "disconnect" event as soon as the GUI
// process is closed, so we can kill this process as well. // process is closed, so we can kill this process as well.
// @ts-ignore (0 is a valid value for stopRetrying and is not the same as false) // @ts-ignore (0 is a valid value for stopRetrying and is not the same as false)
ipc.config.stopRetrying = 0; ipc.config.stopRetrying = 0;

View File

@@ -52,7 +52,6 @@ async function write(options: WriteOptions) {
const onFail = (destination: SourceDestination, error: Error) => { const onFail = (destination: SourceDestination, error: Error) => {
emitFail({ emitFail({
// TODO: device should be destination // TODO: device should be destination
// @ts-ignore (destination.drive is private) // @ts-ignore (destination.drive is private)
device: destination.drive, device: destination.drive,
error: toJSON(error), error: toJSON(error),

View File

@@ -30,13 +30,14 @@ const adapters: Adapter[] = [
// Can't use permissions.isElevated() here as it returns a promise and we need to set // Can't use permissions.isElevated() here as it returns a promise and we need to set
// module.exports = scanner right now. // module.exports = scanner right now.
if (platform !== 'linux' || (geteuid && geteuid() === 0)) { if (platform !== 'linux' || geteuid() === 0) {
adapters.push(new UsbbootDeviceAdapter()); adapters.push(new UsbbootDeviceAdapter());
} }
if (platform === 'win32') { if (platform === 'win32') {
const { const {
DriverlessDeviceAdapter: driverless, DriverlessDeviceAdapter: driverless,
// tslint:disable-next-line:no-var-requires
} = require('etcher-sdk/build/scanner/adapters/driverless'); } = require('etcher-sdk/build/scanner/adapters/driverless');
adapters.push(new driverless()); adapters.push(new driverless());
} }

View File

@@ -73,11 +73,9 @@ function prepareDrive(drive: Drive) {
return drive.drive; return drive.drive;
} else if (drive instanceof sdk.sourceDestination.UsbbootDrive) { } else if (drive instanceof sdk.sourceDestination.UsbbootDrive) {
// This is a workaround etcher expecting a device string and a size // This is a workaround etcher expecting a device string and a size
// @ts-ignore // @ts-ignore
drive.device = drive.usbDevice.portId; drive.device = drive.usbDevice.portId;
drive.size = null; drive.size = null;
// @ts-ignore // @ts-ignore
drive.progress = 0; drive.progress = 0;
drive.disabled = true; drive.disabled = true;
@@ -142,7 +140,6 @@ function updateDriveProgress(
progress: number, progress: number,
) { ) {
const drives = getDrives(); const drives = getDrives();
// @ts-ignore // @ts-ignore
const driveInMap = drives[drive.device]; const driveInMap = drives[drive.device];
if (driveInMap) { if (driveInMap) {

25373
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,9 @@
"name": "balena-etcher", "name": "balena-etcher",
"private": true, "private": true,
"displayName": "balenaEtcher", "displayName": "balenaEtcher",
"productName": "balenaEtcher", "version": "1.18.13",
"version": "1.19.4",
"packageType": "local", "packageType": "local",
"main": ".webpack/main", "main": "generated/etcher.js",
"description": "Flash OS images to SD cards and USB drives, safely and easily.", "description": "Flash OS images to SD cards and USB drives, safely and easily.",
"productDescription": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.", "productDescription": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.",
"homepage": "https://github.com/balena-io/etcher", "homepage": "https://github.com/balena-io/etcher",
@@ -14,139 +13,123 @@
"url": "git@github.com:balena-io/etcher.git" "url": "git@github.com:balena-io/etcher.git"
}, },
"scripts": { "scripts": {
"prettify": "prettier --write lib/**/*.css && balena-lint --fix --typescript typings lib tests forge.config.ts forge.sidecar.ts webpack.config.ts", "build": "npm run webpack && npm run build:sidecar",
"lint": "npm run prettify && catch-uncommitted", "build:rebuild-mountutils": "cd node_modules/mountutils && npm rebuild",
"test-gui": "xvfb-maybe electron-mocha --recursive --reporter spec --window-config tests/gui/window-config.json --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts", "build:sidecar": "npm run build:rebuild-mountutils && tsc --project tsconfig.sidecar.json && pkg build/util/api.js -c pkg-sidecar.json --target node18 --output generated/etcher-util",
"test-shared": "xvfb-maybe electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts", "flowzone-preinstall-linux": "sudo apt-get update && sudo apt-get install -y xvfb libudev-dev && cat < electron-builder.yml | yq e .deb.depends[] - | xargs -L1 echo | sed 's/|//g' | xargs -L1 sudo apt-get --ignore-missing install || true",
"test": "npm run test-gui && npm run test-shared", "flowzone-preinstall-macos": "true",
"package": "electron-forge package", "flowzone-preinstall-windows": "npx node-gyp install",
"start": "electron-forge start", "flowzone-preinstall": "npm run flowzone-preinstall-linux",
"make": "electron-forge make" "lint-css": "prettier --write lib/**/*.css",
"lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
"lint": "npm run lint-ts && npm run lint-css",
"postinstall": "electron-rebuild -t prod,dev,optional",
"sanity-checks": "bash scripts/ci/ensure-all-file-extensions-in-gitattributes.sh",
"start": "./node_modules/.bin/electron .",
"test-gui": "electron-mocha --recursive --reporter spec --window-config tests/gui/window-config.json --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
"test-shared": "electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
"test-macos": "npm run lint && npm run test-gui && npm run test-shared && npm run sanity-checks",
"test-linux": "npm run lint && xvfb-run --auto-servernum npm run test-gui && xvfb-run --auto-servernum npm run test-shared && npm run sanity-checks",
"test-windows": "npm run lint && npm run test-gui && npm run test-shared && npm run sanity-checks",
"test": "echo npm run test-{linux,windows,macos}",
"watch": "webpack serve --no-optimization-minimize --config ./webpack.dev.config.ts",
"webpack": "webpack"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "npm run prettify" "pre-commit": "lint-staged"
} }
}, },
"lint-staged": {
"./**/*.{ts,tsx}": [
"npm run lint-ts"
],
"./**/*.css": [
"npm run lint-css"
]
},
"author": "Balena Ltd. <hello@balena.io>", "author": "Balena Ltd. <hello@balena.io>",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "devDependencies": {
"@babel/register": "^7.22.15",
"@balena/lint": "5.4.2",
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534", "@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
"@electron/remote": "^2.1.0", "@electron/remote": "^2.0.9",
"@fortawesome/fontawesome-free": "6.5.1", "@fortawesome/fontawesome-free": "5.15.4",
"@sentry/electron": "^4.15.1", "@sentry/electron": "^4.1.2",
"@svgr/webpack": "5.5.0",
"@types/chai": "4.3.4",
"@types/copy-webpack-plugin": "6.4.3",
"@types/mime-types": "2.1.1",
"@types/mini-css-extract-plugin": "1.4.3",
"@types/mocha": "^9.1.1",
"@types/node": "^16.18.12",
"@types/node-ipc": "9.2.0",
"@types/react": "16.14.34",
"@types/react-dom": "16.9.17",
"@types/semver": "7.3.13",
"@types/sinon": "9.0.11",
"@types/terser-webpack-plugin": "5.0.4",
"@types/tmp": "0.2.3",
"@types/webpack-node-externals": "2.5.3",
"analytics-client": "^2.0.1", "analytics-client": "^2.0.1",
"axios": "^1.6.0", "axios": "^0.27.2",
"chai": "4.3.7",
"copy-webpack-plugin": "7.0.0",
"css-loader": "5.2.7",
"d3": "4.13.0",
"debug": "4.3.4", "debug": "4.3.4",
"electron-squirrel-startup": "^1.0.0", "electron": "^25.8.2",
"electron-updater": "6.1.7", "electron-builder": "^23.6.0",
"etcher-sdk": "9.0.0", "electron-mocha": "^11.0.2",
"i18next": "23.7.8", "electron-notarize": "1.2.2",
"electron-rebuild": "^3.2.9",
"electron-updater": "5.3.0",
"esbuild-loader": "2.20.0",
"etcher-sdk": "8.3.1",
"file-loader": "6.2.0",
"husky": "4.3.8",
"i18next": "21.10.0",
"immutable": "3.8.2", "immutable": "3.8.2",
"lint-staged": "10.5.4",
"lodash": "4.17.21", "lodash": "4.17.21",
"mini-css-extract-plugin": "1.6.2",
"mocha": "^9.1.1",
"native-addon-loader": "2.0.1",
"node-ipc": "9.2.1", "node-ipc": "9.2.1",
"omit-deep-lodash": "1.1.7",
"outdent": "0.8.0", "outdent": "0.8.0",
"path-is-inside": "1.0.2", "path-is-inside": "1.0.2",
"pkg": "^5.8.1",
"pnp-webpack-plugin": "1.7.0",
"pretty-bytes": "5.6.0", "pretty-bytes": "5.6.0",
"react": "17.0.2", "react": "16.8.5",
"react-dom": "17.0.2", "react-dom": "16.8.5",
"react-i18next": "13.5.0", "react-i18next": "11.18.6",
"redux": "4.2.1", "redux": "4.2.0",
"rendition": "35.1.2", "rendition": "19.3.2",
"semver": "7.5.4", "semver": "7.3.8",
"simple-progress-webpack-plugin": "1.1.2",
"sinon": "9.2.4",
"string-replace-loader": "3.1.0",
"style-loader": "2.0.0",
"styled-components": "5.3.6", "styled-components": "5.3.6",
"sys-class-rgb-led": "3.0.1", "sys-class-rgb-led": "3.0.1",
"uuid": "9.0.1" "terser-webpack-plugin": "5.3.6",
}, "ts-loader": "8.4.0",
"devDependencies": { "ts-node": "9.1.1",
"@balena/lint": "7.2.4", "tslib": "2.4.1",
"@electron-forge/cli": "7.2.0", "typescript": "4.4.4",
"@electron-forge/maker-deb": "7.2.0",
"@electron-forge/maker-dmg": "7.2.0",
"@electron-forge/maker-rpm": "7.2.0",
"@electron-forge/maker-squirrel": "7.2.0",
"@electron-forge/maker-zip": "7.2.0",
"@electron-forge/plugin-auto-unpack-natives": "7.2.0",
"@electron-forge/plugin-webpack": "7.2.0",
"@reforged/maker-appimage": "3.3.2",
"@svgr/webpack": "8.1.0",
"@types/chai": "4.3.11",
"@types/debug": "^4.1.12",
"@types/mime-types": "2.1.4",
"@types/mocha": "^10.0.6",
"@types/node": "^18.11.9",
"@types/node-ipc": "9.2.3",
"@types/react": "17.0.2",
"@types/react-dom": "17.0.2",
"@types/semver": "7.5.6",
"@types/sinon": "17.0.2",
"@types/tmp": "0.2.6",
"@vercel/webpack-asset-relocator-loader": "1.7.3",
"catch-uncommitted": "^2.0.0",
"chai": "4.3.10",
"css-loader": "5.2.7",
"electron": "27.1.3",
"electron-mocha": "^12.2.0",
"file-loader": "6.2.0",
"husky": "8.0.3",
"mocha": "^10.2.0",
"native-addon-loader": "2.0.1",
"node-loader": "^2.0.0",
"omit-deep-lodash": "1.1.7",
"pkg": "^5.8.1",
"sinon": "17.0.1",
"string-replace-loader": "3.1.0",
"style-loader": "3.3.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tslib": "2.6.2",
"typescript": "^5.3.3",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"xvfb-maybe": "^0.2.1" "uuid": "8.3.2",
}, "webpack": "5.75.0",
"hostDependencies": { "webpack-cli": "4.10.0",
"debian": [ "webpack-dev-server": "4.11.1"
"gconf-service",
"gconf2",
"libasound2",
"libatk1.0-0",
"libc6",
"libcairo2",
"libcups2",
"libdbus-1-3",
"libexpat1",
"libfontconfig1",
"libfreetype6",
"libgbm1",
"libgcc1",
"libgconf-2-4",
"libgdk-pixbuf2.0-0",
"libglib2.0-0",
"libgtk-3-0",
"liblzma5",
"libnotify4",
"libnspr4",
"libnss3",
"libpango1.0-0 | libpango-1.0-0",
"libstdc++6",
"libx11-6",
"libxcomposite1",
"libxcursor1",
"libxdamage1",
"libxext6",
"libxfixes3",
"libxi6",
"libxrandr2",
"libxrender1",
"libxss1",
"libxtst6",
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
]
}, },
"engines": { "engines": {
"node": ">=18 <20" "node": ">=18 <20"
}, },
"versionist": { "versionist": {
"publishedAt": "2024-01-26T17:29:27.845Z" "publishedAt": "2023-10-16T13:32:27.552Z"
} }
} }

View File

@@ -5,8 +5,6 @@
"node_modules/drivelist/**", "node_modules/drivelist/**",
"node_modules/mountutils/**", "node_modules/mountutils/**",
"node_modules/winusb-driver-generator/**", "node_modules/winusb-driver-generator/**",
"node_modules/node-raspberrypi-usbboot/**", "node_modules/node-raspberrypi-usbboot/**"
"node_modules/xxhash-addon/**",
"node_modules/axios/**"
] ]
} }

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
awscli==1.27.28
shyaml==0.6.2

View File

@@ -0,0 +1,52 @@
#!/bin/bash
###
# Copyright 2017 balena.io
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###
set -u
set -e
# Read list of wildcards from .gitattributes
wildcards=()
while IFS='' read -r line || [[ -n "$line" ]]; do
if [[ -n "$line" ]]; then
if [[ ! "$line" =~ "^#" ]]; then
filetype=$(echo "$line" | cut -d ' ' -f 2)
if [[ "$filetype" == "text" ]] || [[ "$filetype" == "binary" ]]; then
wildcards+=("$(echo "$line" | cut -d ' ' -f 1)")
fi
fi
fi
done < .gitattributes
# Verify those wildcards against all files stored in the repo
git ls-tree -r HEAD | while IFS='' read line; do
if [[ "$(echo $line | cut -d ' ' -f 2)" == "blob" ]]; then
# the cut delimiter in the line below is actually a tab character, not a space
filename=$(basename $(echo "$line" | cut -d ' ' -f 2))
found_match=0
for wildcard in "${wildcards[@]}"; do
if [[ "$filename" = $wildcard ]]; then
found_match=1
break
fi
done
if [[ $found_match -eq 0 ]]; then
echo "No wildcards match $filename"
exit 1
fi
fi
done

View File

@@ -0,0 +1,52 @@
/**
* This script is in charge of cleaning the `shrinkwrap` file.
*
* `npm shrinkwrap` has a bug where it will add optional dependencies
* to `npm-shrinkwrap.json`, therefore causing errors if these optional
* dependendencies are platform dependent and you then try to build
* the project in another platform.
*
* As a workaround, we keep a list of platform dependent dependencies in
* the `platformSpecificDependencies` property of `package.json`,
* and manually remove them from `npm-shrinkwrap.json` if they exist.
*
* See: https://github.com/npm/npm/issues/2679
*/
import { writeFile } from 'fs';
import * as omit from 'omit-deep-lodash';
import * as path from 'path';
import { promisify } from 'util';
import * as shrinkwrap from '../npm-shrinkwrap.json';
import * as packageInfo from '../package.json';
const writeFileAsync = promisify(writeFile);
const JSON_INDENT = 2;
const SHRINKWRAP_FILENAME = path.join(__dirname, '..', 'npm-shrinkwrap.json');
async function main() {
try {
const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies);
for (const item of Object.values(cleaned.dependencies)) {
// @ts-ignore
item.dev = true;
}
await writeFileAsync(
SHRINKWRAP_FILENAME,
JSON.stringify(cleaned, null, JSON_INDENT),
);
} catch (error: any) {
console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`);
process.exitCode = 1;
}
console.log(
`[OK] Wrote shrinkwrap file to ${path.relative(
__dirname,
SHRINKWRAP_FILENAME,
)}`,
);
}
main();

1
scripts/resin Submodule

Submodule scripts/resin added at 8dfa21cfc2

View File

@@ -1,6 +1,8 @@
// tslint:disable-next-line:no-var-requires
const { app } = require('electron'); const { app } = require('electron');
if (app !== undefined) { if (app !== undefined) {
// tslint:disable-next-line:no-var-requires
const remoteMain = require('@electron/remote/main'); const remoteMain = require('@electron/remote/main');
remoteMain.initialize(); remoteMain.initialize();

View File

@@ -15,6 +15,7 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import * as _ from 'lodash';
import { stub } from 'sinon'; import { stub } from 'sinon';
import * as settings from '../../../lib/gui/app/models/settings'; import * as settings from '../../../lib/gui/app/models/settings';
@@ -46,8 +47,8 @@ describe('Browser: settings', () => {
const writeConfigFileStub = stub(); const writeConfigFileStub = stub();
writeConfigFileStub.returns(Promise.reject(new Error('settings error'))); writeConfigFileStub.returns(Promise.reject(new Error('settings error')));
const promise = settings.set('foo', 'baz', writeConfigFileStub); const p = settings.set('foo', 'baz', writeConfigFileStub);
await checkError(promise, async (error) => { await checkError(p, async (error) => {
expect(error).to.be.an.instanceof(Error); expect(error).to.be.an.instanceof(Error);
expect(error.message).to.equal('settings error'); expect(error.message).to.equal('settings error');
expect(await settings.get('foo')).to.equal('bar'); expect(await settings.get('foo')).to.equal('bar');

View File

@@ -1,17 +1,18 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2019",
"allowJs": false, "allowJs": false,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": false,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"typeRoots": ["./node_modules/@types", "./typings"], "typeRoots": ["./node_modules/@types", "./typings"],
"module": "commonjs", "module": "CommonJS",
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true "isolatedModules": true,
"outDir": "build"
}, },
"include": ["lib/util"] "include": ["lib/util"]
} }

28
tsconfig.webpack.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"module": "es2015",
"target": "es2019",
"jsx": "react",
"typeRoots": ["./node_modules/@types", "./typings"],
"importHelpers": true,
"allowSyntheticDefaultImports": true,
"lib": ["dom", "esnext"],
"declaration": true,
"declarationMap": true,
"pretty": true,
"sourceMap": true,
"baseUrl": "./src",
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowJs": true
},
"include": [
"lib/**/*.ts",
"node_modules/electron/**/*.d.ts"
]
}

1
typings/pnp-webpack-plugin/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'pnp-webpack-plugin';

1
typings/resin-corvus/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'resin-corvus/browser';

View File

@@ -0,0 +1 @@
declare module 'simple-progress-webpack-plugin';

View File

@@ -14,13 +14,48 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Configuration, ModuleOptions } from 'webpack'; import * as CopyPlugin from 'copy-webpack-plugin';
import * as _ from 'lodash';
import * as path from 'path';
import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin';
import * as TerserPlugin from 'terser-webpack-plugin';
import { import {
BannerPlugin, BannerPlugin,
IgnorePlugin, IgnorePlugin,
NormalModuleReplacementPlugin, NormalModuleReplacementPlugin,
} from 'webpack'; } from 'webpack';
import * as PnpWebpackPlugin from 'pnp-webpack-plugin';
import * as tsconfigRaw from './tsconfig.webpack.json';
/**
* Don't webpack package.json as sentry tokens
* will be inserted in it after webpacking
*/
function externalPackageJson(packageJsonPath: string) {
return (
{ request }: { context: string; request: string },
callback: (error?: Error | null, result?: string) => void,
) => {
if (_.endsWith(request, 'package.json')) {
return callback(null, `commonjs ${packageJsonPath}`);
}
return callback();
};
}
function renameNodeModules(resourcePath: string) {
// electron-builder excludes the node_modules folder even if you specifically include it
// Work around by renaming it to "modules"
// See https://github.com/electron-userland/electron-builder/issues/4545
return (
path
.relative(__dirname, resourcePath)
.replace('node_modules', 'modules')
// file-loader expects posix paths, even on Windows
.replace(/\\/g, '/')
);
}
interface ReplacementRule { interface ReplacementRule {
search: string; search: string;
@@ -40,34 +75,28 @@ function replace(test: RegExp, ...replacements: ReplacementRule[]) {
}; };
} }
const rules: Required<ModuleOptions>['rules'] = [ const commonConfig = {
// Add support for native node modules mode: 'production',
{ optimization: {
// We're specifying native_modules in the test because the asset relocator loader generates a moduleIds: 'natural',
// "fake" .node file which is really a cjs file. minimize: true,
test: /native_modules[/\\].+\.node$/, minimizer: [
use: 'node-loader', new TerserPlugin({
}, parallel: true,
{ terserOptions: {
test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, compress: false,
parser: { amd: false }, mangle: false,
use: { format: {
loader: '@vercel/webpack-asset-relocator-loader', comments: false,
options: { ecma: 2020,
outputAssetBase: 'native_modules',
},
},
},
{
test: /\.tsx?$/,
exclude: /(node_modules|\.webpack)/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
}, },
}, },
extractComments: false,
}),
],
}, },
module: {
rules: [
{ {
test: /\.css$/, test: /\.css$/,
use: ['style-loader', 'css-loader'], use: ['style-loader', 'css-loader'],
@@ -75,23 +104,45 @@ const rules: Required<ModuleOptions>['rules'] = [
{ {
test: /\.(woff|woff2|eot|ttf|otf)$/, test: /\.(woff|woff2|eot|ttf|otf)$/,
loader: 'file-loader', loader: 'file-loader',
options: { name: renameNodeModules },
}, },
{ {
test: /\.svg$/, test: /\.svg$/,
use: '@svgr/webpack', use: '@svgr/webpack',
}, },
{
test: /\.tsx?$/,
use: [
{
loader: 'esbuild-loader',
options: {
loader: 'tsx',
target: 'es2021',
tsconfigRaw,
},
},
],
},
// don't import WeakMap polyfill in deep-map-keys (required in corvus)
replace(/node_modules\/deep-map-keys\/lib\/deep-map-keys\.js$/, {
search: "var WeakMap = require('es6-weak-map');",
replace: '',
}),
// force axios to use http backend (not xhr) to support streams // force axios to use http backend (not xhr) to support streams
replace(/node_modules\/axios\/lib\/defaults\.js$/, { replace(/node_modules\/axios\/lib\/defaults\.js$/, {
search: './adapters/xhr', search: './adapters/xhr',
replace: './adapters/http', replace: './adapters/http',
}), }),
]; ],
},
export const rendererConfig: Configuration = { resolve: {
module: { extensions: ['.js', '.json', '.ts', '.tsx'],
rules,
}, },
plugins: [ plugins: [
PnpWebpackPlugin,
new SimpleProgressWebpackPlugin({
format: process.env.WEBPACK_PROGRESS || 'verbose',
}),
// Force axios to use http.js, not xhr.js as we need stream support // Force axios to use http.js, not xhr.js as we need stream support
// (its package.json file replaces http with xhr for browser targets). // (its package.json file replaces http with xhr for browser targets).
new NormalModuleReplacementPlugin( new NormalModuleReplacementPlugin(
@@ -106,25 +157,62 @@ export const rendererConfig: Configuration = {
new IgnorePlugin({ new IgnorePlugin({
resourceRegExp: /^aws-crt$/, resourceRegExp: /^aws-crt$/,
}), }),
],
resolveLoader: {
plugins: [PnpWebpackPlugin.moduleLoader(module)],
},
output: {
path: path.join(__dirname, 'generated'),
filename: '[name].js',
},
externals: [
// '../package.json' because we are in 'generated'
externalPackageJson('../package.json'),
],
};
const guiConfig = {
...commonConfig,
target: 'electron-renderer',
node: {
__dirname: true,
__filename: true,
},
entry: {
gui: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
},
plugins: [
...commonConfig.plugins,
new CopyPlugin({
patterns: [
{ from: 'lib/gui/app/index.html', to: 'index.html' },
// electron-builder doesn't bundle folders named "assets"
// See https://github.com/electron-userland/electron-builder/issues/4545
{ from: 'assets/icon.png', to: 'media/icon.png' },
],
}),
// Remove "Download the React DevTools for a better development experience" message // Remove "Download the React DevTools for a better development experience" message
new BannerPlugin({ new BannerPlugin({
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };', banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
raw: true, raw: true,
}), }),
], ],
resolve: { };
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
const mainConfig = {
...commonConfig,
target: 'electron-main',
node: {
__dirname: false,
__filename: true,
}, },
}; };
export const mainConfig: Configuration = { const etcherConfig = {
...mainConfig,
entry: { entry: {
etcher: './lib/gui/etcher.ts', etcher: path.join(__dirname, 'lib', 'gui', 'etcher.ts'),
},
module: {
rules,
},
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
}, },
}; };
export default [guiConfig, etcherConfig];

24
webpack.dev.config.ts Normal file
View File

@@ -0,0 +1,24 @@
import configs from './webpack.config';
import { WebpackOptionsNormalized } from 'webpack';
import * as fs from 'fs';
const [
guiConfig,
etcherConfig,
childWriterConfig,
] = (configs as unknown) as WebpackOptionsNormalized[];
configs.forEach((config) => {
config.mode = 'development';
// @ts-ignore
config.devtool = 'source-map';
});
guiConfig.devServer = {
hot: true,
port: 3030,
};
fs.copyFileSync('./lib/gui/app/index.dev.html', './generated/index.html');
export default [guiConfig, etcherConfig, childWriterConfig];