mirror of
https://github.com/balena-io/etcher.git
synced 2025-09-09 04:58:32 +00:00
Compare commits
425 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7420283249 | ||
![]() |
453952440f | ||
![]() |
2475d576c7 | ||
![]() |
8cd6da1260 | ||
![]() |
82dd4fc1d1 | ||
![]() |
33fe4b2c1a | ||
![]() |
b1c1188107 | ||
![]() |
63b45aefae | ||
![]() |
f79cb0fac5 | ||
![]() |
ec42892c7c | ||
![]() |
371716fe6a | ||
![]() |
d5fb6bec15 | ||
![]() |
c5e7bf26d7 | ||
![]() |
e3072ac416 | ||
![]() |
dfaf06e4cf | ||
![]() |
6e24d25576 | ||
![]() |
b59b171e43 | ||
![]() |
28726584c2 | ||
![]() |
00b151311a | ||
![]() |
36c813714b | ||
![]() |
2ae6764dd9 | ||
![]() |
debefc9652 | ||
![]() |
b068b847c7 | ||
![]() |
6c410c07ce | ||
![]() |
c01206c1f3 | ||
![]() |
2e85fb45de | ||
![]() |
67513e384d | ||
![]() |
828dafa493 | ||
![]() |
5c5a761222 | ||
![]() |
fab10e5fc5 | ||
![]() |
797345fc1c | ||
![]() |
a0388a43c3 | ||
![]() |
f5b0a3023b | ||
![]() |
dc1d7bd1fd | ||
![]() |
9d674321b6 | ||
![]() |
f9c8378d6a | ||
![]() |
65da751a52 | ||
![]() |
72142be0de | ||
![]() |
11cea7c926 | ||
![]() |
8d46ee4c22 | ||
![]() |
d63c09e2c2 | ||
![]() |
c9e9d7d109 | ||
![]() |
2412d20eb4 | ||
![]() |
7f90d23a12 | ||
![]() |
b9a82be29b | ||
![]() |
638673ba5e | ||
![]() |
898fe4f216 | ||
![]() |
7e805662d1 | ||
![]() |
baf59c73ac | ||
![]() |
38ad9c97c6 | ||
![]() |
8fc574f059 | ||
![]() |
78b0f00e88 | ||
![]() |
0f10f2d483 | ||
![]() |
eb5f5bbb9e | ||
![]() |
67d26ff790 | ||
![]() |
17f2008d88 | ||
![]() |
db1bf7e488 | ||
![]() |
4b786b8a9f | ||
![]() |
fdfa0d3258 | ||
![]() |
757aa77d89 | ||
![]() |
d70ea06565 | ||
![]() |
f2ebd10053 | ||
![]() |
cd67b442c9 | ||
![]() |
852c83c4fb | ||
![]() |
e3b2ee3b83 | ||
![]() |
927a026b86 | ||
![]() |
c851e1d54f | ||
![]() |
e6fdca171f | ||
![]() |
c9cfb87733 | ||
![]() |
b0b7c53294 | ||
![]() |
e8dc6579fe | ||
![]() |
f0747abe3f | ||
![]() |
32fab87340 | ||
![]() |
adcd8e0325 | ||
![]() |
7b5808eb2b | ||
![]() |
a8f7422cf5 | ||
![]() |
5ae9a26361 | ||
![]() |
cf1fdb8c5f | ||
![]() |
bf7ebde100 | ||
![]() |
88c5fa5035 | ||
![]() |
887b0dd538 | ||
![]() |
364d1db56a | ||
![]() |
c431222909 | ||
![]() |
55a0f68b97 | ||
![]() |
af2563dfc2 | ||
![]() |
33f8851083 | ||
![]() |
fe1f19b9fa | ||
![]() |
871cf3ec0a | ||
![]() |
686a5837b6 | ||
![]() |
23f2dd5ce5 | ||
![]() |
d5d39b395b | ||
![]() |
2acad790d3 | ||
![]() |
30133306d6 | ||
![]() |
04a62f2ad8 | ||
![]() |
17858a7d72 | ||
![]() |
620307568f | ||
![]() |
a349c5d9ac | ||
![]() |
0d740ad12d | ||
![]() |
85a3f28869 | ||
![]() |
dbd5397405 | ||
![]() |
85c183b9ef | ||
![]() |
0d0af1d1dd | ||
![]() |
ad423fc187 | ||
![]() |
d8b2a7a236 | ||
![]() |
13ec8cbe98 | ||
![]() |
a7cae23612 | ||
![]() |
86bb093f3d | ||
![]() |
997e1eb2f2 | ||
![]() |
34cc8b8933 | ||
![]() |
f26b074811 | ||
![]() |
adaa07b4b0 | ||
![]() |
96f4569342 | ||
![]() |
be190c6c80 | ||
![]() |
809617a82d | ||
![]() |
df02732002 | ||
![]() |
d35f3c3049 | ||
![]() |
8b047e3b14 | ||
![]() |
fa41d21e27 | ||
![]() |
54e6c5e2c1 | ||
![]() |
43fc3dd7eb | ||
![]() |
12a1340c8e | ||
![]() |
cf8b5790a1 | ||
![]() |
659d85a833 | ||
![]() |
96c44d31c9 | ||
![]() |
ba812b4f64 | ||
![]() |
4087258fbd | ||
![]() |
955be13129 | ||
![]() |
32011c0dea | ||
![]() |
b68325c71c | ||
![]() |
84bce86fce | ||
![]() |
d68eab1dda | ||
![]() |
09cf014d14 | ||
![]() |
d5bab5805f | ||
![]() |
b5ab500a14 | ||
![]() |
49253d37c9 | ||
![]() |
97cf3b25ad | ||
![]() |
99862b95a5 | ||
![]() |
8b765d58e5 | ||
![]() |
8f566e45b8 | ||
![]() |
b8af86e30c | ||
![]() |
784f193b6d | ||
![]() |
3967adb1b5 | ||
![]() |
0667d1110f | ||
![]() |
61dd22bdf3 | ||
![]() |
24eb8b05b0 | ||
![]() |
6991a4950b | ||
![]() |
bb169cf674 | ||
![]() |
e5d0d2e262 | ||
![]() |
72b4d4f4fa | ||
![]() |
9b2f2eb4c3 | ||
![]() |
ce52ef95a9 | ||
![]() |
aa3756ad17 | ||
![]() |
73081e726d | ||
![]() |
d53dc4149b | ||
![]() |
0d5bb4935f | ||
![]() |
14aeb0060b | ||
![]() |
239726f3ce | ||
![]() |
4ed3002716 | ||
![]() |
7286fba240 | ||
![]() |
895c306fb7 | ||
![]() |
f3844d56e2 | ||
![]() |
540dc3150a | ||
![]() |
035c8dfec3 | ||
![]() |
03d6a011db | ||
![]() |
27f64650f9 | ||
![]() |
ccca009972 | ||
![]() |
57a6ceff0e | ||
![]() |
30c4baa58b | ||
![]() |
a930d77064 | ||
![]() |
0d1cfffa5c | ||
![]() |
3c7422764c | ||
![]() |
55176b9f8f | ||
![]() |
156b9314b5 | ||
![]() |
76d22280dc | ||
![]() |
e4251a3862 | ||
![]() |
831339bd2c | ||
![]() |
952ea80e15 | ||
![]() |
813c497e4b | ||
![]() |
1b5b647135 | ||
![]() |
7de99003ca | ||
![]() |
e09bdd734b | ||
![]() |
306e087ec6 | ||
![]() |
c6b0178a87 | ||
![]() |
4e581ea1ce | ||
![]() |
26dc2d19e5 | ||
![]() |
b99282acfb | ||
![]() |
4e48724d0c | ||
![]() |
448ce141d5 | ||
![]() |
695f287190 | ||
![]() |
4de3271e15 | ||
![]() |
77b33b127d | ||
![]() |
9cd13ba381 | ||
![]() |
9df23c8a3f | ||
![]() |
e3618b939e | ||
![]() |
6a39f5869a | ||
![]() |
fd472efadc | ||
![]() |
7e2c2eae63 | ||
![]() |
5266571ca4 | ||
![]() |
797868c474 | ||
![]() |
2c2a5c7c2b | ||
![]() |
9e536d5337 | ||
![]() |
860e680dd9 | ||
![]() |
7bb52aa170 | ||
![]() |
1c370f9100 | ||
![]() |
ec7c772d0b | ||
![]() |
cc0285a77d | ||
![]() |
256d3550d1 | ||
![]() |
db3a5f3b0a | ||
![]() |
0e58edf113 | ||
![]() |
db136926a9 | ||
![]() |
d84e7211be | ||
![]() |
8357cc19d2 | ||
![]() |
2752b9fa95 | ||
![]() |
0214be4953 | ||
![]() |
a4f944e795 | ||
![]() |
cd2ebf15fc | ||
![]() |
7a7ea374e9 | ||
![]() |
330df325f9 | ||
![]() |
2fc0882b2e | ||
![]() |
4dd779e010 | ||
![]() |
3dc54405fe | ||
![]() |
3f1aa5bac3 | ||
![]() |
8f52fdb900 | ||
![]() |
1b93891ed8 | ||
![]() |
33adc8ecf8 | ||
![]() |
0455f7ea58 | ||
![]() |
ea5a167f4f | ||
![]() |
8a1c4a4cc8 | ||
![]() |
bd8bc81713 | ||
![]() |
98a5ddf58a | ||
![]() |
6223dbc541 | ||
![]() |
7c56621c57 | ||
![]() |
a61aa8e2be | ||
![]() |
7df4f9615b | ||
![]() |
5742452fdf | ||
![]() |
fe09f9f862 | ||
![]() |
3a4687ea0f | ||
![]() |
db6490fb1b | ||
![]() |
1642297101 | ||
![]() |
5ecd223cfc | ||
![]() |
306e40fd7b | ||
![]() |
b58249b9c8 | ||
![]() |
b23b4b34d0 | ||
![]() |
73bc921713 | ||
![]() |
f356e4c303 | ||
![]() |
9888167f2e | ||
![]() |
4561690478 | ||
![]() |
576113febf | ||
![]() |
cc139bf750 | ||
![]() |
ae91958c06 | ||
![]() |
33dea6267f | ||
![]() |
c9a8bca96f | ||
![]() |
8af376e608 | ||
![]() |
9ab307df4f | ||
![]() |
e8a716f8bb | ||
![]() |
a40e64f6cd | ||
![]() |
2e53feb38c | ||
![]() |
5945ab1f50 | ||
![]() |
59d67220d4 | ||
![]() |
61610ded84 | ||
![]() |
c87a132f40 | ||
![]() |
350d4de32b | ||
![]() |
f5f9025d6d | ||
![]() |
549d744d04 | ||
![]() |
6194460dc2 | ||
![]() |
8370f638b4 | ||
![]() |
ac34c51125 | ||
![]() |
b241470fe1 | ||
![]() |
179697040c | ||
![]() |
335766ed12 | ||
![]() |
4c5d052a71 | ||
![]() |
86423342a8 | ||
![]() |
d8b41552e3 | ||
![]() |
11c65fb392 | ||
![]() |
bed126506f | ||
![]() |
f6aeb52b16 | ||
![]() |
a5201942b8 | ||
![]() |
c1f7164273 | ||
![]() |
6774bf784c | ||
![]() |
56ec8b4eac | ||
![]() |
35868509af | ||
![]() |
3ab6749f49 | ||
![]() |
7a012a92bc | ||
![]() |
aba01825a0 | ||
![]() |
907a3308de | ||
![]() |
4366bb372f | ||
![]() |
a6f6cd4a19 | ||
![]() |
03ee428039 | ||
![]() |
8d652d064d | ||
![]() |
28adc34239 | ||
![]() |
120e9bf42f | ||
![]() |
59f54e194b | ||
![]() |
c4834e61a7 | ||
![]() |
e4d02bc561 | ||
![]() |
b9e54e39f7 | ||
![]() |
f3c32eac65 | ||
![]() |
9a303ab344 | ||
![]() |
9c1b55bebc | ||
![]() |
30ae4bbd86 | ||
![]() |
c6126a980a | ||
![]() |
ef90d048ca | ||
![]() |
b938132038 | ||
![]() |
3cb2e78fe7 | ||
![]() |
ea9875ddf0 | ||
![]() |
65dacd2ff2 | ||
![]() |
a190818827 | ||
![]() |
98e33b619b | ||
![]() |
685ed715ac | ||
![]() |
3cf3c4b398 | ||
![]() |
1c2ef4b1d4 | ||
![]() |
d22fc91585 | ||
![]() |
0a28af5c35 | ||
![]() |
0c1e5b88ef | ||
![]() |
790201be90 | ||
![]() |
d8d379f05e | ||
![]() |
b5e9701048 | ||
![]() |
292f86d6f5 | ||
![]() |
76ca9934c8 | ||
![]() |
37b826ee4e | ||
![]() |
1e1bd3c508 | ||
![]() |
00e8f11913 | ||
![]() |
a3c24a26a0 | ||
![]() |
4232928ad8 | ||
![]() |
b165fb78da | ||
![]() |
e9f6c5ead9 | ||
![]() |
b2d0c1c9dd | ||
![]() |
14d91400a4 | ||
![]() |
d0114aece7 | ||
![]() |
dff2df4aab | ||
![]() |
13159f93ee | ||
![]() |
3ece1fd841 | ||
![]() |
f46963b6b3 | ||
![]() |
b97f4e0031 | ||
![]() |
e2d233d74b | ||
![]() |
a7ca2e527b | ||
![]() |
396a053c0a | ||
![]() |
d1a3f1cb88 | ||
![]() |
9f96558cdd | ||
![]() |
b3bc589d70 | ||
![]() |
18d2c28110 | ||
![]() |
b272ef296d | ||
![]() |
32ca28a3a9 | ||
![]() |
4d5e5a3b0b | ||
![]() |
8b3f37102d | ||
![]() |
4b74253631 | ||
![]() |
a81b552b95 | ||
![]() |
53f53c0f75 | ||
![]() |
fdaf5c69d6 | ||
![]() |
061afca5d3 | ||
![]() |
ccb08a48f1 | ||
![]() |
a8f3d45b12 | ||
![]() |
7e333caaf9 | ||
![]() |
70229e8684 | ||
![]() |
261700389b | ||
![]() |
250aed2eb1 | ||
![]() |
ed1f008fe2 | ||
![]() |
e9ce270dab | ||
![]() |
1ee110bc95 | ||
![]() |
33dd07c675 | ||
![]() |
39ccbbeeda | ||
![]() |
55d2400ac7 | ||
![]() |
0bdea5c54c | ||
![]() |
3be372d49f | ||
![]() |
d0c66b2c48 | ||
![]() |
65082c4790 | ||
![]() |
e87ed9beed | ||
![]() |
bc5563d9c2 | ||
![]() |
ad83ab5dcc | ||
![]() |
0dc1cf9701 | ||
![]() |
11489c6538 | ||
![]() |
2619d4bc86 | ||
![]() |
3730efd350 | ||
![]() |
6ece32c546 | ||
![]() |
fd9996a3cc | ||
![]() |
f06cc89152 | ||
![]() |
c1d7ab3fa9 | ||
![]() |
b206483c7c | ||
![]() |
c3eb8c7b56 | ||
![]() |
0849d4f435 | ||
![]() |
1dba3ae19b | ||
![]() |
f33f2e3771 | ||
![]() |
e56aaed973 | ||
![]() |
a4659f038e | ||
![]() |
cd462818da | ||
![]() |
37769efbed | ||
![]() |
0f70c4bbce | ||
![]() |
48b5e8b9d9 | ||
![]() |
1f138f0ecc | ||
![]() |
73f67e99ca | ||
![]() |
9114da2445 | ||
![]() |
554bbcc780 | ||
![]() |
4db2289cfd | ||
![]() |
c15b56bc23 | ||
![]() |
9f52dda6ae | ||
![]() |
fadcefb11a | ||
![]() |
361c32913c | ||
![]() |
5c2042198e | ||
![]() |
99df53098c | ||
![]() |
aa563c87bd | ||
![]() |
1188888956 | ||
![]() |
f9d7991dc8 | ||
![]() |
53954e81fd | ||
![]() |
f82996bfd1 | ||
![]() |
b74069eb41 | ||
![]() |
e8c7591751 | ||
![]() |
3521b61a81 | ||
![]() |
93db90c725 | ||
![]() |
1dc56aed14 | ||
![]() |
d814202424 | ||
![]() |
c54856a616 | ||
![]() |
fc45df270a | ||
![]() |
3cde2faed0 | ||
![]() |
b4b8c89aad | ||
![]() |
36d05724c0 | ||
![]() |
b1e4e681d1 | ||
![]() |
3987078c11 | ||
![]() |
de0010eb72 | ||
![]() |
1f94f44b18 | ||
![]() |
fe0b45cae6 | ||
![]() |
c32e485f27 | ||
![]() |
409b78fc21 | ||
![]() |
2f08142f5a | ||
![]() |
8c4edaabba | ||
![]() |
05497ce85c | ||
![]() |
d3df2fe57e |
@@ -7,7 +7,6 @@ indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,3 +1,6 @@
|
||||
# default
|
||||
* text
|
||||
|
||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
@@ -27,6 +30,7 @@ Makefile text
|
||||
*.yml text
|
||||
*.patch text
|
||||
*.txt text
|
||||
*.tpl text
|
||||
CODEOWNERS text
|
||||
*.plist text
|
||||
|
||||
@@ -58,3 +62,7 @@ CODEOWNERS text
|
||||
*.ttf binary diff=hex
|
||||
xz-without-extension binary diff=hex
|
||||
wmic-output.txt binary diff=hex
|
||||
|
||||
# gitsecret
|
||||
*.secret binary
|
||||
.gitsecret/** binary
|
||||
|
7
.github/ISSUE_TEMPLATE.md
vendored
7
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,6 +1,11 @@
|
||||
- **Etcher version:**
|
||||
- **Operating system and architecture:**
|
||||
- **Image flashed:**
|
||||
- **What do you think should have happened:** <!-- or a step by step reproduction process -->
|
||||
- **What happened:**
|
||||
- **Do you see any meaningful error information in the DevTools?**
|
||||
|
||||
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
|
||||
|
||||
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
|
||||
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
|
||||
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->
|
||||
|
214
.github/actions/publish/action.yml
vendored
Normal file
214
.github/actions/publish/action.yml
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
name: package and publish GitHub (draft) release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
XCODE_APP_LOADER_EMAIL:
|
||||
type: string
|
||||
default: "accounts+apple@balena.io"
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: "14.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Extract custom source artifact
|
||||
shell: pwsh
|
||||
working-directory: .
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Install yq
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: choco install yq
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
# FIXME: resinci-deploy is not actively maintained
|
||||
# https://github.com/product-os/resinci-deploy
|
||||
- name: Checkout resinci-deploy
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: product-os/resinci-deploy
|
||||
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
||||
path: resinci-deploy
|
||||
|
||||
- name: Build and install resinci-deploy
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
rm -rf ../resinci-deploy && mv resinci-deploy ..
|
||||
|
||||
pushd ../resinci-deploy && npm ci && npm link && popd
|
||||
|
||||
if [[ $runner_os =~ linux|macos ]]; then
|
||||
chmod +x "$(dirname "$(which node)")/resinci-deploy" && which resinci-deploy
|
||||
fi
|
||||
|
||||
# FIXME: store sentry workflow is not documented
|
||||
# https://github.com/product-os/resinci-deploy/blob/master/lib/sentry.ts
|
||||
# https://github.com/getsentry/sentry-cli
|
||||
# https://docs.sentry.io/api/projects/create-a-new-client-key/
|
||||
- name: Generate Sentry DSN
|
||||
id: sentry
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
branch="$(echo '${{ github.event.pull_request.head.ref }}' | sed 's/[^[:alnum:]]/-/g')"
|
||||
|
||||
stdout="$(resinci-deploy store sentry \
|
||||
--branch="${branch}" \
|
||||
--name="$(jq -r '.name' package.json)" \
|
||||
--team="$(yq e '.sentry.team' repo.yml)" \
|
||||
--org="$(yq e '.sentry.org' repo.yml)" \
|
||||
--type="$(yq e '.sentry.type' repo.yml)")"
|
||||
|
||||
echo "dsn=$(echo "${stdout}" | tail -n 1)" >> $GITHUB_OUTPUT
|
||||
|
||||
env:
|
||||
SENTRY_TOKEN: ${{ fromJSON(inputs.secrets).SENTRY_AUTH_TOKEN }}
|
||||
|
||||
# https://www.electron.build/code-signing.html
|
||||
# https://github.com/Apple-Actions/import-codesign-certs
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
|
||||
- name: Import Windows code signing certificate
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: |
|
||||
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
|
||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
||||
|
||||
Import-PfxCertificate `
|
||||
-FilePath ${{ runner.temp }}/certificate.pfx `
|
||||
-CertStoreLocation Cert:\CurrentUser\My `
|
||||
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
|
||||
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.pfx
|
||||
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
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
|
||||
id: package_release
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ 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)"
|
||||
ARCHITECTURE_FLAGS="--${ELECTRON_BUILDER_ARCHITECTURE}"
|
||||
|
||||
if [[ $runner_os =~ linux ]]; then
|
||||
ELECTRON_BUILDER_OS='--linux'
|
||||
TARGETS="$(yq e .linux.target[] electron-builder.yml)"
|
||||
|
||||
elif [[ $runner_os =~ darwin|macos|osx ]]; then
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
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|win ]]; then
|
||||
ARCHITECTURE_FLAGS="--ia32 ${ARCHITECTURE_FLAGS}"
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm link electron-builder
|
||||
|
||||
for target in ${TARGETS}; do
|
||||
electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \
|
||||
--c.extraMetadata.analytics.sentry.token='${{ steps.sentry.outputs.dsn }}' \
|
||||
--c.extraMetadata.analytics.mixpanel.token='balena-etcher' \
|
||||
--c.extraMetadata.packageType="${target}"
|
||||
|
||||
find dist -type f -maxdepth 1
|
||||
done
|
||||
|
||||
echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
env:
|
||||
# Apple notarization (afterSignHook.js)
|
||||
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
|
||||
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
|
||||
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
|
||||
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
|
||||
# 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
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
|
||||
path: dist
|
||||
retention-days: 1
|
58
.github/actions/test/action.yml
vendored
Normal file
58
.github/actions/test/action.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: test release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: "14.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Test release
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ 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 run build
|
||||
npm run test-${runner_os}
|
||||
|
||||
env:
|
||||
# https://www.electronjs.org/docs/latest/api/environment-variables
|
||||
ELECTRON_NO_ATTACH_CONSOLE: true
|
||||
|
||||
- name: Compress custom source
|
||||
shell: pwsh
|
||||
run: tar -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
retention-days: 1
|
29
.github/workflows/flowzone.yml
vendored
Normal file
29
.github/workflows/flowzone.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Flowzone
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
# allow external contributions to use secrets within trusted code
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
flowzone:
|
||||
name: Flowzone
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
||||
# prevent duplicate workflows and only allow one `pull_request` or `pull_request_target` for
|
||||
# internal or external contributions respectively
|
||||
if: |
|
||||
(github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request') ||
|
||||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
|
||||
secrets: inherit
|
||||
with:
|
||||
tests_run_on: '["ubuntu-20.04","macos-latest","windows-2019"]'
|
||||
restrict_custom_actions: false
|
||||
github_prerelease: true
|
||||
repo_config: true
|
||||
repo_description: "Flash OS images to SD cards & USB drives, safely and easily."
|
||||
repo_homepage: https://etcher.io/
|
||||
repo_enable_wiki: true
|
13
.github/workflows/winget.yml
vendored
Normal file
13
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Publish to WinGet
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest # action can only be run on windows
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v1
|
||||
with:
|
||||
identifier: Balena.Etcher
|
||||
installers-regex: 'balenaEtcher-Setup.*.exe$'
|
||||
token: ${{ secrets.WINGET_PAT }}
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -51,3 +51,9 @@ node_modules
|
||||
# VSCode files
|
||||
|
||||
.vscode
|
||||
.gitsecret/keys/random_seed
|
||||
!*.secret
|
||||
secrets/APPLE_SIGNING_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING_PASSWORD.txt
|
||||
secrets/XCODE_APP_LOADER_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING.pfx
|
||||
|
BIN
.gitsecret/keys/pubring.kbx
Normal file
BIN
.gitsecret/keys/pubring.kbx
Normal file
Binary file not shown.
BIN
.gitsecret/keys/pubring.kbx~
Normal file
BIN
.gitsecret/keys/pubring.kbx~
Normal file
Binary file not shown.
BIN
.gitsecret/keys/trustdb.gpg
Normal file
BIN
.gitsecret/keys/trustdb.gpg
Normal file
Binary file not shown.
5
.gitsecret/paths/mapping.cfg
Normal file
5
.gitsecret/paths/mapping.cfg
Normal file
@@ -0,0 +1,5 @@
|
||||
secrets/APPLE_SIGNING_PASSWORD.txt:5c9cfeb1ea5142b547bc842cc6e0b4a932641ae9811ee47abe2c3953f2a4de5d
|
||||
secrets/WINDOWS_SIGNING_PASSWORD.txt:852e431628494f2559793c39cf09c34e9406dd79bb15b90c9f88194020470568
|
||||
secrets/XCODE_APP_LOADER_PASSWORD.txt:005eb9a3c7035c77232973c9355468fc396b94e62783fb8e6dce16bce95b94a1
|
||||
secrets/WINDOWS_SIGNING.pfx:929f401db38733ffc41572539de7c0d938023af51ed06c205a72a71c1f815714
|
||||
secrets/APPLE_SIGNING.p12:61abf7b4ff2eec76ce889d71bcdd568b99a6a719b4947ac20f03966265b0946a
|
@@ -1,74 +0,0 @@
|
||||
{
|
||||
"electron": {
|
||||
"npm_version": "6.14.5",
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
"libusb-1.0-0-dev",
|
||||
"libyaml-dev",
|
||||
"libgtk-3-0",
|
||||
"libatk-bridge2.0-0",
|
||||
"libdbus-1-3",
|
||||
"libgbm1",
|
||||
"libc6"
|
||||
]
|
||||
},
|
||||
"builder": {
|
||||
"appId": "io.balena.etcher",
|
||||
"copyright": "Copyright 2016-2020 Balena Ltd",
|
||||
"productName": "balenaEtcher",
|
||||
"nodeGypRebuild": false,
|
||||
"afterPack": "./afterPack.js",
|
||||
"asar": false,
|
||||
"files": [
|
||||
"generated",
|
||||
"lib/shared/catalina-sudo/sudo-askpass.osascript.js"
|
||||
],
|
||||
"beforeBuild": "./beforeBuild.js",
|
||||
"afterSign": "./afterSignHook.js",
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "entitlements.mac.plist",
|
||||
"entitlementsInherit": "entitlements.mac.plist"
|
||||
},
|
||||
"dmg": {
|
||||
"iconSize": 110,
|
||||
"contents": [
|
||||
{
|
||||
"x": 140,
|
||||
"y": 245
|
||||
},
|
||||
{
|
||||
"x": 415,
|
||||
"y": 245,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"width": 544,
|
||||
"height": 407
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
"packageCategory": "utils",
|
||||
"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."
|
||||
},
|
||||
"deb": {
|
||||
"compression": "bzip2",
|
||||
"priority": "optional",
|
||||
"depends": [
|
||||
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
|
||||
]
|
||||
},
|
||||
"protocols": {
|
||||
"name": "etcher",
|
||||
"schemes": [
|
||||
"etcher"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12848
.versionbot/CHANGELOG.yml
Normal file
12848
.versionbot/CHANGELOG.yml
Normal file
File diff suppressed because it is too large
Load Diff
969
CHANGELOG.md
969
CHANGELOG.md
@@ -3,6 +3,975 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# v1.13.3
|
||||
## (2023-01-11)
|
||||
|
||||
* patch: progress cm4 to second stage [Peter Makra]
|
||||
|
||||
# v1.13.2
|
||||
## (2023-01-02)
|
||||
|
||||
* patch: fixed winget parameter name [mcraa]
|
||||
|
||||
# v1.13.1
|
||||
## (2023-01-02)
|
||||
|
||||
* patch: updated sdk to fix bz2 issue [Peter Makra]
|
||||
* patch: update copyright in electron-builder [JOASSART Edwin]
|
||||
|
||||
# v1.13.0
|
||||
## (2022-12-28)
|
||||
|
||||
* minor: electron version bump [Peter Makra]
|
||||
* patch: handle ext2fs with webpack [Peter Makra]
|
||||
* Patch: update etcher-sdk version to fix CM4 issues [builder555]
|
||||
|
||||
# v1.12.7
|
||||
## (2022-12-20)
|
||||
|
||||
* Update dependency i18next to 21.10.0 [Renovate Bot]
|
||||
|
||||
# v1.12.6
|
||||
## (2022-12-20)
|
||||
|
||||
* Update dependency react-i18next to 11.18.6 [Renovate Bot]
|
||||
|
||||
# v1.12.5
|
||||
## (2022-12-20)
|
||||
|
||||
* Patch: made trim setting more readable [builder555]
|
||||
|
||||
# v1.12.4
|
||||
## (2022-12-19)
|
||||
|
||||
* patch: publish to winget with gh action [Begula]
|
||||
|
||||
# v1.12.3
|
||||
## (2022-12-19)
|
||||
|
||||
* Patch: replaced plain text with i18n in settings [builder555]
|
||||
|
||||
# v1.12.2
|
||||
## (2022-12-16)
|
||||
|
||||
* Update dependency webpack-dev-server to 4.11.1 [Renovate Bot]
|
||||
|
||||
# v1.12.1
|
||||
## (2022-12-16)
|
||||
|
||||
* Patch: expose trim ext{2,3,4} setting [builder555]
|
||||
|
||||
# v1.12.0
|
||||
## (2022-12-14)
|
||||
|
||||
* i18n support and Chinese translation [ab77]
|
||||
* minor: optimize i18n [r-q]
|
||||
|
||||
# v1.11.10
|
||||
## (2022-12-13)
|
||||
|
||||
* Update dependency webpack-cli to 4.10.0 [Renovate Bot]
|
||||
|
||||
# v1.11.9
|
||||
## (2022-12-12)
|
||||
|
||||
* Update dependency webpack to 5.75.0 [Renovate Bot]
|
||||
|
||||
# v1.11.8
|
||||
## (2022-12-12)
|
||||
|
||||
* Update dependency awscli to 1.27.28 [Renovate Bot]
|
||||
|
||||
# v1.11.7
|
||||
## (2022-12-12)
|
||||
|
||||
* Update dependency uuid to 8.3.2 [Renovate Bot]
|
||||
|
||||
# v1.11.6
|
||||
## (2022-12-12)
|
||||
|
||||
* Update dependency tslib to 2.4.1 [Renovate Bot]
|
||||
* Patch: run linux build on ubuntu-20.04 [Edwin Joassart]
|
||||
|
||||
# v1.11.5
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency ts-loader to 8.4.0 [Renovate Bot]
|
||||
|
||||
# v1.11.4
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency styled-components to 5.3.6 [Renovate Bot]
|
||||
|
||||
# v1.11.3
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency terser-webpack-plugin to 5.3.6 [Renovate Bot]
|
||||
|
||||
# v1.11.2
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency string-replace-loader to 3.1.0 [Renovate Bot]
|
||||
|
||||
# v1.11.1
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency sinon to 9.2.4 [Renovate Bot]
|
||||
|
||||
# v1.11.0
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency shyaml to 0.6.2 [Renovate Bot]
|
||||
|
||||
# v1.10.29
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency awscli to 1.27.27 [Renovate Bot]
|
||||
|
||||
# v1.10.28
|
||||
## (2022-12-10)
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependency rendition to 19.3.2 [Renovate Bot] </summary>
|
||||
|
||||
> ## rendition-19.3.2
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Add Breadcrumbs component export [JSReds]
|
||||
>
|
||||
> ## rendition-19.3.1
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Fix max-width on breadcrumbs container [JSReds]
|
||||
>
|
||||
> ## rendition-19.3.0
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Add Breadcrumbs component [JSReds]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
# v1.10.27
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency redux to 4.2.0 [Renovate Bot]
|
||||
|
||||
# v1.10.26
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency pretty-bytes to 5.6.0 [Renovate Bot]
|
||||
|
||||
# v1.10.25
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency pnp-webpack-plugin to 1.7.0 [Renovate Bot]
|
||||
|
||||
# v1.10.24
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency node-ipc to 9.2.1 [Renovate Bot]
|
||||
|
||||
# v1.10.23
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency mocha to 8.4.0 [Renovate Bot]
|
||||
|
||||
# v1.10.22
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency mini-css-extract-plugin to 1.6.2 [Renovate Bot]
|
||||
|
||||
# v1.10.21
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency lint-staged to 10.5.4 [Renovate Bot]
|
||||
|
||||
# v1.10.20
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency husky to 4.3.8 [Renovate Bot]
|
||||
|
||||
# v1.10.19
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency esbuild-loader to 2.20.0 [Renovate Bot]
|
||||
|
||||
# v1.10.18
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency electron-updater to 4.6.5 [Renovate Bot]
|
||||
|
||||
# v1.10.17
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency electron-notarize to 1.2.2 [Renovate Bot]
|
||||
|
||||
# v1.10.16
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency awscli to 1.27.26 [Renovate Bot]
|
||||
|
||||
# v1.10.15
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency electron-builder to 22.14.13 [Renovate Bot]
|
||||
|
||||
# v1.10.14
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency debug to 4.3.4 [Renovate Bot]
|
||||
|
||||
# v1.10.13
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency awscli to 1.27.25 [Renovate Bot]
|
||||
|
||||
# v1.10.12
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency css-loader to 5.2.7 [Renovate Bot]
|
||||
|
||||
# v1.10.11
|
||||
## (2022-12-07)
|
||||
|
||||
* Update dependency awscli to 1.27.24 [Renovate Bot]
|
||||
|
||||
# v1.10.10
|
||||
## (2022-12-07)
|
||||
|
||||
* Update dependency @types/node to 14.18.34 [Renovate Bot]
|
||||
|
||||
# v1.10.9
|
||||
## (2022-12-06)
|
||||
|
||||
* Enable repository configuration [ab77]
|
||||
|
||||
# v1.10.8
|
||||
## (2022-12-05)
|
||||
|
||||
* Update dependency chai to 4.3.7 [Renovate Bot]
|
||||
|
||||
# v1.10.7
|
||||
## (2022-12-05)
|
||||
|
||||
* Use core workflow for GitHub publish [ab77]
|
||||
|
||||
# v1.10.6
|
||||
## (2022-12-02)
|
||||
|
||||
* Dummy update to fix asset version issue [Edwin Joassart]
|
||||
|
||||
# v1.10.5
|
||||
## (2022-12-02)
|
||||
|
||||
* Patch: run linux build on ubuntu-18.04 [Edwin Joassart]
|
||||
|
||||
# v1.10.4
|
||||
## (2022-12-01)
|
||||
|
||||
* patch: remove Homebrew instructions in README [Patrick Linnane]
|
||||
|
||||
# v1.10.3
|
||||
## (2022-12-01)
|
||||
|
||||
* Allow external contributors [ab77]
|
||||
|
||||
# v1.10.2
|
||||
## (2022-11-25)
|
||||
|
||||
* Fix missing analytics token [Edwin Joassart]
|
||||
|
||||
# v1.10.1
|
||||
## (2022-11-21)
|
||||
|
||||
* Fixing call to electron block screensaver methods invocation [Aurelien VALADE]
|
||||
|
||||
# v1.10.0
|
||||
## (2022-11-10)
|
||||
|
||||
* testing renovate [builder555]
|
||||
|
||||
# v1.9.0
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency awscli to 1.27.5 [Renovate Bot]
|
||||
|
||||
# v1.8.17
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/react-dom to 16.9.17 [Renovate Bot]
|
||||
|
||||
# v1.8.16
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/react to 16.14.34 [Renovate Bot]
|
||||
|
||||
# v1.8.15
|
||||
## (2022-11-08)
|
||||
|
||||
* CI: generalise artefact handling [ab77]
|
||||
|
||||
# v1.8.14
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/node to 14.18.33 [Renovate Bot]
|
||||
|
||||
# v1.8.13
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/copy-webpack-plugin to 6.4.3 [Renovate Bot]
|
||||
|
||||
# v1.8.12
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @fortawesome/fontawesome-free to 5.15.4 [Renovate Bot]
|
||||
|
||||
# v1.8.11
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @balena/lint to 5.4.2 [Renovate Bot]
|
||||
|
||||
# v1.8.10
|
||||
## (2022-11-08)
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependency sys-class-rgb-led to 3.0.1 [Renovate Bot] </summary>
|
||||
|
||||
> ## sys-class-rgb-led-3.0.1
|
||||
> ### (2021-07-01)
|
||||
>
|
||||
> * patch: Delete Codeowners [Vipul Gupta]
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.8.9
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency semver to 7.3.8 [Renovate Bot]
|
||||
|
||||
# v1.8.8
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency omit-deep-lodash to 1.1.7 [Renovate Bot]
|
||||
|
||||
# v1.8.7
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency immutable to 3.8.2 [Renovate Bot]
|
||||
|
||||
# v1.8.6
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency electron-rebuild to 3.2.9 [Renovate Bot]
|
||||
|
||||
# v1.8.5
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency electron-mocha to 9.3.3 [Renovate Bot]
|
||||
|
||||
# v1.8.4
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/webpack-node-externals to 2.5.3 [Renovate Bot]
|
||||
|
||||
# v1.8.3
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/tmp to 0.2.3 [Renovate Bot]
|
||||
|
||||
# v1.8.2
|
||||
## (2022-11-08)
|
||||
|
||||
* Generate release notes with git [ab77]
|
||||
|
||||
# v1.8.1
|
||||
## (2022-11-07)
|
||||
|
||||
* Update dependency @types/mime-types to 2.1.1 [Renovate Bot]
|
||||
|
||||
# v1.8.0
|
||||
## (2022-11-07)
|
||||
|
||||
* Update scripts/resin digest to 652fdd4 [Renovate Bot]
|
||||
|
||||
# v1.7.15
|
||||
## (2022-11-07)
|
||||
|
||||
* Build targets individually [ab77]
|
||||
|
||||
# v1.7.14
|
||||
## (2022-11-07)
|
||||
|
||||
* Update dependency lodash to 4.17.21 [SECURITY] [Renovate Bot]
|
||||
|
||||
# v1.7.13
|
||||
## (2022-11-07)
|
||||
|
||||
* Update release notes on finalize [ab77]
|
||||
|
||||
# v1.7.12
|
||||
## (2022-11-07)
|
||||
|
||||
* Avoid duplicate releases [ab77]
|
||||
|
||||
# v1.7.11
|
||||
## (2022-11-07)
|
||||
|
||||
* Only run finalize on Linux runners [ab77]
|
||||
|
||||
# v1.7.10
|
||||
## (2022-11-07)
|
||||
|
||||
* Switch to Flowzone [ab77]
|
||||
|
||||
# v1.7.9
|
||||
## (2022-04-22)
|
||||
|
||||
* patch: update allowed extensions to include deb afterinstall in build [mcraa]
|
||||
* patch: add update notification [Peter Makra]
|
||||
* patch: fix usb-device-boot link in README [Andrew Scheller]
|
||||
* Fix application directory for Debian postinst script [Ken Bannister]
|
||||
|
||||
# v1.7.8
|
||||
## (2022-03-18)
|
||||
|
||||
* patch: complete suse uninstall readme [Peter Makra]
|
||||
* patch: completed suse instructions [Peter Makra]
|
||||
* patch: order rpm instrictions [Peter Makra]
|
||||
* patch: enabled update notification for version 1.7.8 [Peter Makra]
|
||||
* patch: updated title to balenaEtcher [Peter Makra]
|
||||
* patch: cleanup and organize readme [Peter Makra]
|
||||
* patch: extend cloudsmith attribution in readme [Peter Makra]
|
||||
* Update macOS Icon to Big Sur Style [Logicer]
|
||||
|
||||
# v1.7.7
|
||||
## (2022-02-22)
|
||||
|
||||
* patch: clarified update check [Peter Makra]
|
||||
* patch: autoupdate stagingPercentage check, include default [Peter Makra]
|
||||
|
||||
# v1.7.6
|
||||
## (2022-02-21)
|
||||
|
||||
* patch: version number notification [Peter Makra]
|
||||
* patch: fixed typos in template [Peter Makra]
|
||||
* patch: add requirements and help to issue template [mcraa]
|
||||
* patch: add requirements and help to issue template [mcraa]
|
||||
|
||||
# v1.7.5
|
||||
## (2022-02-21)
|
||||
|
||||
* patch: fix flashing from URL when using basic auth [Marco Füllemann]
|
||||
|
||||
# v1.7.4
|
||||
## (2022-02-21)
|
||||
|
||||
* patch: set version update notification 1.7.3 [Peter Makra]
|
||||
* patch: updated electron to 12.2.3 [Peter Makra]
|
||||
* patch: updated electron to 12.2.3 [Peter Makra]
|
||||
|
||||
# v1.7.3
|
||||
## (2021-12-29)
|
||||
|
||||
* patch: fix mesage of null [Peter Makra]
|
||||
|
||||
# v1.7.2
|
||||
## (2021-12-21)
|
||||
|
||||
* patch: fixed open from browser on windows [Peter Makra]
|
||||
|
||||
# v1.7.1
|
||||
## (2021-11-22)
|
||||
|
||||
* patch: Revert back to electron-rebuild [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Disallow TS in JS [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Remove esInterop TS flag [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Use @balena/sudo-prompt [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Update rpiboot guide link [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Improve webpack build time [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.7.0
|
||||
## (2021-11-09)
|
||||
|
||||
* patch: Add missing @types/react@16.8.5 [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Use npm ci in Makefile [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Add draft info boxes for system information [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Remove electron-rebuild package [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Make electron a dev. dependency [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Remove electron-rebuild package [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Use exact modules versions [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Update etcher-sdk from v6.2.5 to v6.3.0 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Fix write step for Http file process [JSReds]
|
||||
* patch: Fix linting errors [Lorenzo Alberto Maria Ambrosi]
|
||||
* minor: Refactor dependencies installation to avoid custom scripts [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Fix LEDs init error [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.6.0
|
||||
## (2021-09-20)
|
||||
|
||||
* Add support for basic auth when downloading images from URL. [Marco Füllemann]
|
||||
* patch: Update etcher-sdk from v6.2.1 to v6.2.5 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Update Makefile to Apple M1 info [David Gaspar]
|
||||
* Add LED settings for potentially different hardware [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.122
|
||||
## (2021-09-02)
|
||||
|
||||
* Restore image file selection LED-drive pathing [Lorenzo Alberto Maria Ambrosi]
|
||||
* Update scripts submodule [Lorenzo Alberto Maria Ambrosi]
|
||||
* Change LEDs colours [Lorenzo Alberto Maria Ambrosi]
|
||||
* Windows images now show the proper warning again [Lorenzo Alberto Maria Ambrosi]
|
||||
* Fix Update and install with DNF instructions [Mohamed Salah]
|
||||
* Add possibile authorization as a query param [JSReds]
|
||||
* update the windows part [Xtraim]
|
||||
* Update SUPPORT.md [thambu1710]
|
||||
* replace make webpack with npm run webpack [Seth Falco]
|
||||
* Add loader on image select [JSReds]
|
||||
* add pnp-webpack-plugin [Zane Hitchcox]
|
||||
* Remove redundant codespell dependency/tests [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.121
|
||||
## (2021-07-05)
|
||||
|
||||
* patch: Delete Codeowners [Vipul Gupta]
|
||||
* Add source maps for devtools [Lorenzo Alberto Maria Ambrosi]
|
||||
* Clone submodules when initializing modules [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Select drive on list interaction rather than modal closing [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.120
|
||||
## (2021-05-11)
|
||||
|
||||
* Update README to reference Cloudsmith [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.119
|
||||
## (2021-04-30)
|
||||
|
||||
* Update readme for new PPA provider [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.118
|
||||
## (2021-04-27)
|
||||
|
||||
* patch: development environment [Zane Hitchcox]
|
||||
* patch: watch files for electron [Zane Hitchcox]
|
||||
|
||||
# v1.5.117
|
||||
## (2021-04-02)
|
||||
|
||||
* Rename mac releases (keep old naming) [Alexis Svinartchouk]
|
||||
* Disable spectron tests on macOS [Alexis Svinartchouk]
|
||||
* Update electron to v12.0.2 [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 6.1.1 to 6.2.1 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-6.2.1
|
||||
> ### (2021-03-26)
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update node-raspberrypi-usbboot from 0.2.11 to 0.3.0 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### node-raspberrypi-usbboot-0.3.0
|
||||
>> #### (2021-03-26)
|
||||
>>
|
||||
>> * Add support for compute module 4 [Alexis Svinartchouk]
|
||||
>> * Fix size endianness of boot_message_t message [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ## etcher-sdk-6.2.0
|
||||
> ### (2021-02-18)
|
||||
>
|
||||
> * Added BeagleBone USB Boot example [Parthiban Gandhi]
|
||||
> * Added BeagleBone USB Boot support [Parthiban Gandhi]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Fix getAppPath() returning an asar file on macOS [Alexis Svinartchouk]
|
||||
* Grammar fix [Andrew Scheller]
|
||||
* (docs) update README.md [vlad doster]
|
||||
* Update copyright year in electron-builder.yml [Andrew Scheller]
|
||||
* Update copyright year in .resinci.json [Andrew Scheller]
|
||||
* Separate the Yum and DNF instructions. [Dugan Chen]
|
||||
* Set msvs_version to 2019 when rebuilding [Alexis Svinartchouk]
|
||||
* Use moduleIds: 'natural' in webpack config to keep js files in arm64 and x64 mac builds identical [Alexis Svinartchouk]
|
||||
* Update electron-builder to 22.10.5 [Alexis Svinartchouk]
|
||||
* Update spectron to v13 [Alexis Svinartchouk]
|
||||
* Update dependencies, use aws4-axios@2.2.1 to avoid adding more dependiencies [Alexis Svinartchouk]
|
||||
* Update scripts to build universal mac dmgs on the ci [Alexis Svinartchouk]
|
||||
* Fix beforeBuild.js script to also work on mac [Alexis Svinartchouk]
|
||||
* Support building universal dmgs (x64 and arm64) for mac [Alexis Svinartchouk]
|
||||
* Update electron-builder to 22.10.4 [Alexis Svinartchouk]
|
||||
* Fix titlebar z-index [Alexis Svinartchouk]
|
||||
* Explicitly set contextIsolation to false [Alexis Svinartchouk]
|
||||
* Update electron from 9.4.1 to 11.2.3 [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 6.1.0 to 6.1.1 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-6.1.1
|
||||
> ### (2021-02-10)
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update node-raspberrypi-usbboot from 0.2.10 to 0.2.11 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### node-raspberrypi-usbboot-0.2.11
|
||||
>> #### (2021-02-10)
|
||||
>>
|
||||
>> * Update @balena.io/usb from 1.3.12 to 1.3.14 [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.5.116
|
||||
## (2021-02-03)
|
||||
|
||||
* Only cleanup temporary decompressed files in child-writer [Alexis Svinartchouk]
|
||||
* Add .versionbot/CHANGELOG.yml [Alexis Svinartchouk]
|
||||
* Stop using node-tmp, use withTmpFile from etcher-sdk instead [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.2.2 to 6.1.0 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-6.1.0
|
||||
> ### (2021-02-03)
|
||||
>
|
||||
> * Prefix temporary decompressed images filenames [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-6.0.1
|
||||
> ### (2021-02-02)
|
||||
>
|
||||
> * Ignore ENOENT errors on unlink in withTmpFile [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-6.0.0
|
||||
> ### (2021-02-01)
|
||||
>
|
||||
> * Export tmp and add prefix and postfix options [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-5.2.3
|
||||
> ### (2021-01-26)
|
||||
>
|
||||
> * upgrade lint [Zane Hitchcox]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Revert "Change some border colors to have higher contrast" [Alexis Svinartchouk]
|
||||
* Update electron to v9.4.1 [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.2.1 to 5.2.2 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-5.2.2
|
||||
> ### (2021-01-19)
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update drivelist from 9.2.2 to 9.2.4 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### drivelist-9.2.4
|
||||
>> #### (2021-01-19)
|
||||
>>
|
||||
>> * Pass strings between methods as std::string instead of char * [Floris Bos]
|
||||
>>
|
||||
>> ### drivelist-9.2.3
|
||||
>> #### (2021-01-19)
|
||||
>>
|
||||
>> * Support lsblk versions that do no support the pttype column [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.5.115
|
||||
## (2021-01-18)
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.1.12 to 5.2.1 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-5.2.1
|
||||
> ### (2021-01-15)
|
||||
>
|
||||
> * Only run one diskpart at a time [Alexis Svinartchouk]
|
||||
> * Ignore diskpart VDS_E_DISK_IS_OFFLINE errors [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-5.2.0
|
||||
> ### (2021-01-06)
|
||||
>
|
||||
> * Store progress on usbboot devices [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.5.114
|
||||
## (2021-01-12)
|
||||
|
||||
* Remove libappindicator1 debian dependency [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.1.11 to 5.1.12 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-5.1.12
|
||||
> ### (2021-01-06)
|
||||
>
|
||||
> * Remove BlockDevice.mountpoints incorrect typing [Alexis Svinartchouk]
|
||||
> * Update axios to 0.21.1 and aws4-axios to 2.0.1 [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update rendition from 18.8.3 to 19.2.0 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## rendition-19.2.0
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Add truncate property to Txt component [JSReds]
|
||||
>
|
||||
> ## rendition-19.1.0
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Add fallback image source to Img component [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-19.0.0
|
||||
> ### (2020-12-21)
|
||||
>
|
||||
> * Remove Arcslider component [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.20.4
|
||||
> ### (2020-12-17)
|
||||
>
|
||||
> * Upgrade rehype-raw to latest version [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.20.3
|
||||
> ### (2020-12-17)
|
||||
>
|
||||
> * Fix disabled button tooltip [JSReds]
|
||||
>
|
||||
> ## rendition-18.20.2
|
||||
> ### (2020-12-16)
|
||||
>
|
||||
> * Turn keydown handler into an arrow function [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.20.1
|
||||
> ### (2020-12-14)
|
||||
>
|
||||
> * Fix form not getting the Enter key event when nested in a modal [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.20.0
|
||||
> ### (2020-12-14)
|
||||
>
|
||||
> * feat: Add new StatsBar component [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.19.2
|
||||
> ### (2020-12-14)
|
||||
>
|
||||
> * Update snapshots [Graham McCulloch]
|
||||
> * Removed out-of-date documentation and template text [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.19.1
|
||||
> ### (2020-12-04)
|
||||
>
|
||||
> * Markdown: Fix line breaks [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.19.0
|
||||
> ### (2020-12-02)
|
||||
>
|
||||
> * Make card size responsive [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.18.0
|
||||
> ### (2020-12-02)
|
||||
>
|
||||
> * Allow passing responsive values to datagrid width props [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.17.2
|
||||
> ### (2020-12-01)
|
||||
>
|
||||
> * Update snapshots due to a Card change [JSReds]
|
||||
>
|
||||
> ## rendition-18.17.1
|
||||
> ### (2020-12-01)
|
||||
>
|
||||
> * Card: make body to be full height [JSReds]
|
||||
>
|
||||
> ## rendition-18.17.0
|
||||
> ### (2020-12-01)
|
||||
>
|
||||
> * Add star rating component [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.16.0
|
||||
> ### (2020-11-23)
|
||||
>
|
||||
> * Completely revamp the development setup for rendition [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.15.1
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Modal: Change the button margins to use the predefined spacing palette [Thodoris Greasidis]
|
||||
>
|
||||
> ## rendition-18.15.0
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Modal: Move the cancel button first for dangerous & warning actions [Thodoris Greasidis]
|
||||
>
|
||||
> ## rendition-18.14.0
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Allow passing checked items as a prop to Table [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.13.4
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Fix accidental complete lodash import [Thodoris Greasidis]
|
||||
>
|
||||
> ## rendition-18.13.3
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Form: Remove the flaky Captcha sceenshot test [Thodoris Greasidis]
|
||||
> * Update react-simplemde-editor & snapshots for upstream versions [Thodoris Greasidis]
|
||||
>
|
||||
> ## rendition-18.13.2
|
||||
> ### (2020-10-29)
|
||||
>
|
||||
> * Updated snapshots [Graham McCulloch]
|
||||
> * Fix: Confirm only depends on the files it needs [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.13.1
|
||||
> ### (2020-10-23)
|
||||
>
|
||||
> * Button: Preserve event during confirmation [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.13.0
|
||||
> ### (2020-10-22)
|
||||
>
|
||||
> * Button: Add confirmation property [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.12.2
|
||||
> ### (2020-10-21)
|
||||
>
|
||||
> * Tabs: changed interfaces and props [JSReds]
|
||||
>
|
||||
> ## rendition-18.12.1
|
||||
> ### (2020-10-20)
|
||||
>
|
||||
> * Fix Tabs typings [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.12.0
|
||||
> ### (2020-10-19)
|
||||
>
|
||||
> * Add a Grid component [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.11.3
|
||||
> ### (2020-10-14)
|
||||
>
|
||||
> * Added more documentation for JsonSchemaRenderer [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.11.2
|
||||
> ### (2020-10-14)
|
||||
>
|
||||
> * fix: UI schema for JsonSchemaRenderer DropDownButton and ButtonGroup widgets [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.11.1
|
||||
> ### (2020-10-13)
|
||||
>
|
||||
> * Add dark mode to storybook [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.11.0
|
||||
> ### (2020-10-08)
|
||||
>
|
||||
> * Allow passing widget to extraFormats field [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.10.2
|
||||
> ### (2020-09-30)
|
||||
>
|
||||
> * Resolve module path not relying on node_moules dir [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.10.1
|
||||
> ### (2020-09-29)
|
||||
>
|
||||
> * Set tabpanel height so it stretches to full height [StefKors]
|
||||
> * Specify tabs width to fix layout problems [StefKors]
|
||||
>
|
||||
> ## rendition-18.10.0
|
||||
> ### (2020-09-24)
|
||||
>
|
||||
> * feat: Add ColorWidget for JsonSchemaRenderer [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.9.2
|
||||
> ### (2020-09-22)
|
||||
>
|
||||
> * Markdown: Ignore decorators inside a code block [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.9.1
|
||||
> ### (2020-09-21)
|
||||
>
|
||||
> * Add compact variation to tabs [StefKors]
|
||||
>
|
||||
> ## rendition-18.9.0
|
||||
> ### (2020-09-18)
|
||||
>
|
||||
> * Improve spacing for Modal and Select components [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.8.4
|
||||
> ### (2020-09-17)
|
||||
>
|
||||
> * fix: Use widget's display name to reference the widget [Graham McCulloch]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Update dependencies [Alexis Svinartchouk]
|
||||
* Update @balena/lint to 5.3.0 [Alexis Svinartchouk]
|
||||
* Update webpack to v5 [Alexis Svinartchouk]
|
||||
* Fix typo in webpack.config.ts comment [Alexis Svinartchouk]
|
||||
* docs: fix quote marks [Aaron Shaw]
|
||||
* Disable screensaver while flashing (on balena-electron-env) [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.113
|
||||
## (2020-12-16)
|
||||
|
||||
* Show the first error for each drive (not the last) [Alexis Svinartchouk]
|
||||
* Fix red leds not showing for failed devices [Alexis Svinartchouk]
|
||||
* docs: add documentation links [Aaron Shaw]
|
||||
* docs: update macOS version [Aaron Shaw]
|
||||
* Improve hover message when the drive is too small [Alexis Svinartchouk]
|
||||
* Update electron to v9.4.0 [Alexis Svinartchouk]
|
||||
* Update npm to v6.14.8 [Giovanni Garufi]
|
||||
* Update rgb leds colors [Alexis Svinartchouk]
|
||||
* Remove unmountOnSuccess setting [Alexis Svinartchouk]
|
||||
* Only show auto-updates setting on supported targets [Alexis Svinartchouk]
|
||||
* Remove dead code in settings modal [Alexis Svinartchouk]
|
||||
* Fix effective flashing speed calculation for compressed images [Alexis Svinartchouk]
|
||||
* Change some border colors to have higher contrast [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.1.10 to 5.1.11 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-5.1.11
|
||||
> ### (2020-12-07)
|
||||
>
|
||||
> * Don't use the O_SYNC flag for block devices, only O_DIRECT [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update sys-class-rgb-led from 2.1.1 to 3.0.0 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## sys-class-rgb-led-3.0.0
|
||||
> ### (2020-12-03)
|
||||
>
|
||||
> * Add example etcher-pro rainbow animation [Alexis Svinartchouk]
|
||||
> * Use one setInterval instead of a loop for each led, t in seconds [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.5.112
|
||||
## (2020-12-02)
|
||||
|
||||
|
@@ -1,2 +0,0 @@
|
||||
* @thundron @zvin @jviotti
|
||||
/scripts @nazrhom
|
4
FAQ.md
4
FAQ.md
@@ -37,10 +37,10 @@ modules=xwayland.so
|
||||
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
|
||||
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
|
||||
|
||||
## I receive ”No polkit authentication agent found” error in GNU/Linux
|
||||
## I receive "No polkit authentication agent found" error in GNU/Linux
|
||||
|
||||
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
|
||||
|
||||
## May I run Etcher in older macOS versions?
|
||||
|
||||
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.9 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
||||
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
||||
|
15
Makefile
15
Makefile
@@ -3,7 +3,7 @@
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
RESIN_SCRIPTS ?= ./scripts/resin
|
||||
export NPM_VERSION ?= 6.14.5
|
||||
export NPM_VERSION ?= 6.14.8
|
||||
S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||
|
||||
# This directory will be completely deleted by the `clean` rule
|
||||
@@ -66,6 +66,9 @@ else
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
HOST_ARCH = x64
|
||||
endif
|
||||
ifeq ($(shell uname -m),arm64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -86,11 +89,9 @@ TARGET_ARCH ?= $(HOST_ARCH)
|
||||
# Electron
|
||||
# ---------------------------------------------------------------------
|
||||
electron-develop:
|
||||
$(RESIN_SCRIPTS)/electron/install.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-s $(PLATFORM) \
|
||||
-m $(NPM_VERSION)
|
||||
git submodule update --init && \
|
||||
npm ci && \
|
||||
npm run webpack
|
||||
|
||||
electron-test:
|
||||
$(RESIN_SCRIPTS)/electron/test.sh \
|
||||
@@ -125,7 +126,7 @@ TARGETS = \
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
|
||||
lint:
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
test:
|
||||
|
185
README.md
185
README.md
@@ -5,16 +5,15 @@
|
||||
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. It can also flash directly Raspberry Pi devices that support the usbboot protocol
|
||||
was written correctly, and much more. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode).
|
||||
|
||||
[](https://balena.io/etcher)
|
||||
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||
[](https://david-dm.org/balena-io/etcher)
|
||||
[](https://forums.balena.io/c/etcher)
|
||||
|
||||
***
|
||||
---
|
||||
|
||||
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones]
|
||||
[**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
|
||||
|
||||
## Supported Operating Systems
|
||||
|
||||
@@ -22,7 +21,7 @@ was written correctly and much more. It can also flash directly Raspberry Pi dev
|
||||
- macOS 10.10 (Yosemite) and later
|
||||
- Microsoft Windows 7 and later
|
||||
|
||||
Note that Etcher will run on any platform officially supported by
|
||||
**Note**: Etcher will run on any platform officially supported by
|
||||
[Electron][electron]. Read more in their
|
||||
[documentation][electron-supported-platforms].
|
||||
|
||||
@@ -31,81 +30,118 @@ Note that Etcher will run on any platform officially supported by
|
||||
Refer to the [downloads page][etcher] for the latest pre-made
|
||||
installers for all supported operating systems.
|
||||
|
||||
## Packages
|
||||
|
||||
> [](https://cloudsmith.com) \
|
||||
Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com).
|
||||
Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that
|
||||
enables your organization to create, store and share packages in any format, to any place, with total
|
||||
confidence.
|
||||
|
||||
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher debian repository:
|
||||
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-deb)
|
||||
|
||||
```sh
|
||||
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
|
||||
```
|
||||
1. Add Etcher Debian repository:
|
||||
|
||||
2. Trust Bintray.com's GPG key:
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
```sh
|
||||
sudo apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys 379CE192D401AB61
|
||||
```
|
||||
2. Update and install:
|
||||
|
||||
3. Update and install:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install balena-etcher-electron
|
||||
```
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo apt-get remove balena-etcher-electron
|
||||
sudo rm /etc/apt/sources.list.d/balena-etcher.list
|
||||
sudo apt-get update
|
||||
rm /etc/apt/sources.list.d/balena-etcher.list
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
apt-get update
|
||||
```
|
||||
|
||||
##### OpenSUSE LEAP & Tumbleweed install
|
||||
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
|
||||
|
||||
|
||||
##### DNF
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
###### Uninstall
|
||||
|
||||
```sh
|
||||
sudo zypper ar https://balena.io/etcher/static/etcher-rpm.repo
|
||||
sudo zypper ref
|
||||
sudo zypper in balena-etcher-electron
|
||||
rm /etc/yum.repos.d/balena-etcher.repo
|
||||
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||
```
|
||||
|
||||
##### Yum
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
###### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
rm /etc/yum.repos.d/balena-etcher.repo
|
||||
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||
```
|
||||
|
||||
#### OpenSUSE LEAP & Tumbleweed install (zypper)
|
||||
|
||||
1. Add the repo
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
2. Update and install
|
||||
|
||||
```sh
|
||||
sudo zypper up
|
||||
sudo zypper install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo zypper rm balena-etcher-electron
|
||||
```
|
||||
|
||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo yum clean all
|
||||
sudo yum makecache fast
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
# remove the repo
|
||||
sudo zypper rr balena-etcher
|
||||
sudo zypper rr balena-etcher-source
|
||||
```
|
||||
|
||||
#### Solus (GNU/Linux x64)
|
||||
@@ -120,11 +156,10 @@ sudo eopkg it etcher
|
||||
sudo eopkg rm etcher
|
||||
```
|
||||
|
||||
#### Arch Linux / Manjaro (GNU/Linux x64)
|
||||
#### Arch/Manjaro Linux (GNU/Linux x64)
|
||||
|
||||
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
|
||||
|
||||
|
||||
```sh
|
||||
yay -S balena-etcher
|
||||
```
|
||||
@@ -135,22 +170,6 @@ yay -S balena-etcher
|
||||
yay -R balena-etcher
|
||||
```
|
||||
|
||||
#### Brew Cask (macOS)
|
||||
|
||||
Note that the Etcher Cask has to be updated manually to point to new versions,
|
||||
so it might not refer to the latest version immediately after an Etcher
|
||||
release.
|
||||
|
||||
```sh
|
||||
brew cask install balenaetcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
brew cask uninstall balenaetcher
|
||||
```
|
||||
|
||||
#### Chocolatey (Windows)
|
||||
|
||||
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
|
||||
@@ -168,20 +187,22 @@ choco uninstall etcher
|
||||
|
||||
## Support
|
||||
|
||||
If you're having any problem, please [raise an issue][newissue] on GitHub and
|
||||
If you're having any problem, please [raise an issue][newissue] on GitHub, and
|
||||
the balena.io team will be happy to help.
|
||||
|
||||
## License
|
||||
|
||||
Etcher is free software, and may be redistributed under the terms specified in
|
||||
Etcher is free software and may be redistributed under the terms specified in
|
||||
the [license].
|
||||
|
||||
[etcher]: https://balena.io/etcher
|
||||
[electron]: https://electronjs.org/
|
||||
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||
[SUPPORT]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
||||
[CONTRIBUTING]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[USER-DOCUMENTATION]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[support]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
||||
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[milestones]: https://github.com/balena-io/etcher/milestones
|
||||
[newissue]: https://github.com/balena-io/etcher/issues/new
|
||||
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE
|
||||
|
||||
|
||||
|
17
SUPPORT.md
17
SUPPORT.md
@@ -1,9 +1,16 @@
|
||||
Getting help with Etcher
|
||||
========================
|
||||
Getting help with BalenaEtcher
|
||||
===============================
|
||||
|
||||
There are various ways to get support for Etcher if you experience an issue or
|
||||
have an idea you'd like to share with us.
|
||||
|
||||
Documentation
|
||||
------
|
||||
|
||||
We have answers to a variety of frequently asked questions in the [user
|
||||
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
|
||||
|
||||
|
||||
Forums
|
||||
------
|
||||
|
||||
@@ -15,7 +22,7 @@ a look at the existing threads before opening a new one!
|
||||
Make sure to mention the following information to help us provide better
|
||||
support:
|
||||
|
||||
- The Etcher version you're running.
|
||||
- The BalenaEtcher version you're running.
|
||||
|
||||
- The operating system you're running Etcher in.
|
||||
|
||||
@@ -25,10 +32,12 @@ support:
|
||||
GitHub
|
||||
------
|
||||
|
||||
If you encounter an issue or have a suggestion, head on over to Etcher's [issue
|
||||
If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue
|
||||
tracker][issues] and if there isn't a ticket covering it, [create
|
||||
one][new-issue].
|
||||
|
||||
[discourse]: https://forums.balena.io/c/etcher
|
||||
[issues]: https://github.com/balena-io/etcher/issues
|
||||
[new-issue]: https://github.com/balena-io/etcher/issues/new
|
||||
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[faq]: https://etcher.io
|
||||
|
11
after-install.tpl
Normal file
11
after-install.tpl
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Link to the binary
|
||||
# Must hardcode balenaEtcher directory; no variable available
|
||||
ln -sf '/opt/balenaEtcher/${executable}' '/usr/bin/${executable}'
|
||||
|
||||
# SUID chrome-sandbox for Electron 5+
|
||||
chmod 4755 '/opt/balenaEtcher/chrome-sandbox' || true
|
||||
|
||||
update-mime-database /usr/share/mime || true
|
||||
update-desktop-database /usr/share/applications || true
|
@@ -10,13 +10,15 @@ async function main(context) {
|
||||
}
|
||||
|
||||
const appName = context.packager.appInfo.productFilename
|
||||
const appleId = 'accounts+apple@balena.io'
|
||||
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: `@keychain:Application Loader: ${appleId}`
|
||||
appleIdPassword
|
||||
})
|
||||
}
|
||||
|
||||
|
BIN
assets/icon.icns
BIN
assets/icon.icns
Binary file not shown.
@@ -1,26 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const cp = require('child_process');
|
||||
const rimraf = require('rimraf');
|
||||
const process = require('process');
|
||||
|
||||
// Rebuild native modules for ia32 and run webpack again for the ia32 part of windows packages
|
||||
exports.default = function(context) {
|
||||
if (context.platform.name === 'windows') {
|
||||
cp.execFileSync(
|
||||
'bash',
|
||||
['./node_modules/.bin/electron-rebuild', '--types', 'dev', '--arch', context.arch],
|
||||
);
|
||||
rimraf.sync('generated');
|
||||
cp.execFileSync(
|
||||
'bash',
|
||||
['./node_modules/.bin/webpack'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_target_arch: context.arch,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@@ -91,7 +91,7 @@ make electron-develop
|
||||
|
||||
```sh
|
||||
# Build the GUI
|
||||
make webpack
|
||||
npm run webpack
|
||||
# Start Electron
|
||||
npm start
|
||||
```
|
||||
|
@@ -159,6 +159,18 @@ pre-installed in all modern Windows versions.
|
||||
|
||||
- Run `clean`. This command will completely clean your drive by erasing any
|
||||
existent filesystem.
|
||||
|
||||
- Run `create partition primary`. This command will create a new partition.
|
||||
|
||||
- Run `active`. This command will active the partition.
|
||||
|
||||
- Run `list partition`. This command will show available partition.
|
||||
|
||||
- Run `select partition N`, where `N` corresponds to the id of the newly available partition.
|
||||
|
||||
- Run `format override quick`. This command will format the partition. You can choose a specific formatting by adding `FS=xx` where `xx` could be `NTFS or FAT or FAT32` after `format`. Example : `format FS=NTFS override quick`
|
||||
|
||||
- Run `exit` to quit diskpart.
|
||||
|
||||
### OS X
|
||||
|
||||
|
@@ -1,21 +1,23 @@
|
||||
# https://www.electron.build/configuration/configuration
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2020 Balena Ltd
|
||||
copyright: Copyright 2016-2023 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: false
|
||||
publish: null
|
||||
beforeBuild: "./beforeBuild.js"
|
||||
afterPack: "./afterPack.js"
|
||||
afterPack: ./afterPack.js
|
||||
afterSign: ./afterSignHook.js
|
||||
asar: false
|
||||
files:
|
||||
- generated
|
||||
- lib/shared/catalina-sudo/sudo-askpass.osascript.js
|
||||
- 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
|
||||
@@ -32,6 +34,10 @@ dmg:
|
||||
height: 405
|
||||
win:
|
||||
icon: assets/icon.ico
|
||||
target:
|
||||
- zip
|
||||
- nsis
|
||||
- portable
|
||||
nsis:
|
||||
oneClick: true
|
||||
runAfterFinish: true
|
||||
@@ -44,17 +50,23 @@ portable:
|
||||
artifactName: "${productName}-Portable-${version}.${ext}"
|
||||
requestExecutionLevel: user
|
||||
linux:
|
||||
icon: assets/iconset
|
||||
target:
|
||||
- AppImage
|
||||
- rpm
|
||||
- deb
|
||||
category: Utility
|
||||
packageCategory: utils
|
||||
executableName: balena-etcher-electron
|
||||
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.
|
||||
icon: assets/iconset
|
||||
appImage:
|
||||
artifactName: ${productName}-${version}-${env.ELECTRON_BUILDER_ARCHITECTURE}.${ext}
|
||||
deb:
|
||||
priority: optional
|
||||
compression: bzip2
|
||||
depends:
|
||||
- gconf2
|
||||
- gconf-service
|
||||
- libappindicator1
|
||||
- gconf2
|
||||
- libasound2
|
||||
- libatk1.0-0
|
||||
- libc6
|
||||
@@ -88,6 +100,7 @@ deb:
|
||||
- libxss1
|
||||
- libxtst6
|
||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||
afterInstall: "./after-install.tpl"
|
||||
rpm:
|
||||
depends:
|
||||
- util-linux
|
||||
|
@@ -28,7 +28,6 @@ import * as EXIT_CODES from '../../shared/exit-codes';
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as availableDrives from './models/available-drives';
|
||||
import * as flashState from './models/flash-state';
|
||||
import { init as ledsInit } from './models/leds';
|
||||
import { deselectImage, getImage } from './models/selection-state';
|
||||
import * as settings from './models/settings';
|
||||
import { Actions, observe, store } from './models/store';
|
||||
@@ -38,6 +37,8 @@ import * as exceptionReporter from './modules/exception-reporter';
|
||||
import * as osDialog from './os/dialog';
|
||||
import * as windowProgress from './os/window-progress';
|
||||
import MainPage from './pages/main/MainPage';
|
||||
import './css/main.css';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
@@ -216,8 +217,7 @@ function prepareDrive(drive: Drive) {
|
||||
disabled: true,
|
||||
icon: 'warning',
|
||||
size: null,
|
||||
link:
|
||||
'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
|
||||
link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
|
||||
linkCTA: 'Install',
|
||||
linkTitle: 'Install missing drivers',
|
||||
linkMessage: outdent`
|
||||
@@ -314,9 +314,9 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
|
||||
try {
|
||||
const confirmed = await osDialog.showWarning({
|
||||
confirmationLabel: 'Yes, quit',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: 'Are you sure you want to close Etcher?',
|
||||
confirmationLabel: i18next.t('yesExit'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
title: i18next.t('reallyExit'),
|
||||
description: messages.warning.exitWhileFlashing(),
|
||||
});
|
||||
if (confirmed) {
|
||||
@@ -334,13 +334,19 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
popupExists = false;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
await ledsInit();
|
||||
export async function main() {
|
||||
try {
|
||||
const { init: ledsInit } = require('./models/leds');
|
||||
await ledsInit();
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(MainPage),
|
||||
document.getElementById('main'),
|
||||
@@ -356,5 +362,3 @@ async function main() {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
@@ -43,6 +43,8 @@ import {
|
||||
} from '../../styled-components';
|
||||
|
||||
import { SourceMetadata } from '../source-selector/source-selector';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||
progress: number;
|
||||
@@ -136,17 +138,18 @@ const InitProgress = styled(
|
||||
`;
|
||||
|
||||
export interface DriveSelectorProps
|
||||
extends Omit<ModalProps, 'done' | 'cancel'> {
|
||||
extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
|
||||
write: boolean;
|
||||
multipleSelection: boolean;
|
||||
showWarnings?: boolean;
|
||||
cancel: () => void;
|
||||
cancel: (drives: DrivelistDrive[]) => void;
|
||||
done: (drives: DrivelistDrive[]) => void;
|
||||
titleLabel: string;
|
||||
emptyListLabel: string;
|
||||
emptyListIcon: JSX.Element;
|
||||
selectedList?: DrivelistDrive[];
|
||||
updateSelectedList?: () => DrivelistDrive[];
|
||||
onSelect?: (drive: DrivelistDrive) => void;
|
||||
}
|
||||
|
||||
interface DriveSelectorState {
|
||||
@@ -167,12 +170,14 @@ export class DriveSelector extends React.Component<
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
tableColumns: Array<TableColumn<Drive>>;
|
||||
originalList: DrivelistDrive[];
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = this.props.selectedList || [];
|
||||
this.originalList = [...(this.props.selectedList || [])];
|
||||
|
||||
this.state = {
|
||||
drives: getDrives(),
|
||||
@@ -185,7 +190,7 @@ export class DriveSelector extends React.Component<
|
||||
this.tableColumns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
label: i18next.t('drives.name'),
|
||||
render: (description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive)) {
|
||||
const isLargeDrive = isDriveSizeLarge(drive);
|
||||
@@ -199,7 +204,9 @@ export class DriveSelector extends React.Component<
|
||||
fill={drive.isSystem ? '#fca321' : '#8f9297'}
|
||||
/>
|
||||
)}
|
||||
<Txt ml={(hasWarnings && 8) || 0}>{description}</Txt>
|
||||
<Txt ml={(hasWarnings && 8) || 0}>
|
||||
{middleEllipsis(description, 32)}
|
||||
</Txt>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -209,7 +216,7 @@ export class DriveSelector extends React.Component<
|
||||
{
|
||||
field: 'description',
|
||||
key: 'size',
|
||||
label: 'Size',
|
||||
label: i18next.t('drives.size'),
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return prettyBytes(drive.size);
|
||||
@@ -219,7 +226,7 @@ export class DriveSelector extends React.Component<
|
||||
{
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: 'Location',
|
||||
label: i18next.t('drives.location'),
|
||||
render: (_description: string, drive: Drive) => {
|
||||
return (
|
||||
<Txt>
|
||||
@@ -259,7 +266,7 @@ export class DriveSelector extends React.Component<
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
!isDriveValid(drive, image) ||
|
||||
!isDriveValid(drive, image, this.props.write) ||
|
||||
(this.props.write && drive.isReadOnly)
|
||||
);
|
||||
}
|
||||
@@ -303,9 +310,9 @@ export class DriveSelector extends React.Component<
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
case compatibility.tooSmall():
|
||||
const recommendedDriveSize =
|
||||
const size =
|
||||
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
||||
return warning.unrecommendedDriveSize({ recommendedDriveSize }, drive);
|
||||
return warning.tooSmall({ size }, drive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,16 +355,6 @@ export class DriveSelector extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
private deselectingAll(rows: DrivelistDrive[]) {
|
||||
return (
|
||||
rows.length > 0 &&
|
||||
rows.length === this.state.selectedList.length &&
|
||||
this.state.selectedList.every(
|
||||
(d) => rows.findIndex((r) => d.device === r.device) > -1,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = store.subscribe(() => {
|
||||
const drives = getDrives();
|
||||
@@ -383,8 +380,8 @@ export class DriveSelector extends React.Component<
|
||||
const displayedDrives = this.getDisplayedDrives(drives);
|
||||
const disabledDrives = this.getDisabledDrives(drives, image);
|
||||
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
|
||||
const numberOfDisplayedSystemDrives = displayedDrives.filter(isSystemDrive)
|
||||
.length;
|
||||
const numberOfDisplayedSystemDrives =
|
||||
displayedDrives.filter(isSystemDrive).length;
|
||||
const numberOfHiddenSystemDrives =
|
||||
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
|
||||
@@ -403,14 +400,14 @@ export class DriveSelector extends React.Component<
|
||||
color="#5b82a7"
|
||||
style={{ fontWeight: 600 }}
|
||||
>
|
||||
{drives.length} found
|
||||
{i18next.t('drives.find', { length: drives.length })}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||
cancel={cancel}
|
||||
cancel={() => cancel(this.originalList)}
|
||||
done={() => done(selectedList)}
|
||||
action={`Select (${selectedList.length})`}
|
||||
action={i18next.t('drives.select', { select: selectedList.length })}
|
||||
primaryButtonProps={{
|
||||
primary: !showWarnings,
|
||||
warning: showWarnings,
|
||||
@@ -448,14 +445,34 @@ export class DriveSelector extends React.Component<
|
||||
onCheck={(rows: Drive[]) => {
|
||||
let newSelection = rows.filter(isDrivelistDrive);
|
||||
if (this.props.multipleSelection) {
|
||||
if (this.deselectingAll(newSelection)) {
|
||||
if (rows.length === 0) {
|
||||
newSelection = [];
|
||||
}
|
||||
const deselecting = selectedList.filter(
|
||||
(selected) =>
|
||||
newSelection.filter(
|
||||
(row) => row.device === selected.device,
|
||||
).length === 0,
|
||||
);
|
||||
const selecting = newSelection.filter(
|
||||
(row) =>
|
||||
selectedList.filter(
|
||||
(selected) => row.device === selected.device,
|
||||
).length === 0,
|
||||
);
|
||||
deselecting.concat(selecting).forEach((row) => {
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(row);
|
||||
}
|
||||
});
|
||||
this.setState({
|
||||
selectedList: newSelection,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(newSelection[newSelection.length - 1]);
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newSelection.slice(newSelection.length - 1),
|
||||
});
|
||||
@@ -467,6 +484,9 @@ export class DriveSelector extends React.Component<
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(row);
|
||||
}
|
||||
const index = selectedList.findIndex(
|
||||
(d) => d.device === row.device,
|
||||
);
|
||||
@@ -493,7 +513,11 @@ export class DriveSelector extends React.Component<
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
|
||||
<Txt ml={8}>
|
||||
{i18next.t('drives.showHidden', {
|
||||
num: numberOfHiddenSystemDrives,
|
||||
})}
|
||||
</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
@@ -501,7 +525,7 @@ export class DriveSelector extends React.Component<
|
||||
)}
|
||||
{this.props.showWarnings && hasSystemDrives ? (
|
||||
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||
Selecting your system drive is dangerous and will erase your drive!
|
||||
{i18next.t('drives.systemDriveDanger')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
@@ -515,19 +539,21 @@ export class DriveSelector extends React.Component<
|
||||
if (missingDriversModal.drive !== undefined) {
|
||||
openExternal(missingDriversModal.drive.link);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logException(error);
|
||||
} finally {
|
||||
this.setState({ missingDriversModal: {} });
|
||||
}
|
||||
}}
|
||||
action="Yes, continue"
|
||||
action={i18next.t('yesContinue')}
|
||||
cancelButtonProps={{
|
||||
children: 'Cancel',
|
||||
children: i18next.t('cancel'),
|
||||
}}
|
||||
children={
|
||||
missingDriversModal.drive.linkMessage ||
|
||||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
|
||||
i18next.t('drives.openInBrowser', {
|
||||
link: missingDriversModal.drive.link,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@@ -7,6 +7,7 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const DriveStatusWarningModal = ({
|
||||
done,
|
||||
@@ -17,12 +18,12 @@ const DriveStatusWarningModal = ({
|
||||
isSystem: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}) => {
|
||||
let warningSubtitle = 'You are about to erase an unusually large drive';
|
||||
let warningCta = 'Are you sure the selected drive is not a storage drive?';
|
||||
let warningSubtitle = i18next.t('drives.largeDriveWarning');
|
||||
let warningCta = i18next.t('drives.largeDriveWarningMsg');
|
||||
|
||||
if (isSystem) {
|
||||
warningSubtitle = "You are about to erase your computer's drives";
|
||||
warningCta = 'Are you sure you want to flash your system drive?';
|
||||
warningSubtitle = i18next.t('drives.systemDriveWarning');
|
||||
warningCta = i18next.t('drives.systemDriveWarningMsg');
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
@@ -33,9 +34,9 @@ const DriveStatusWarningModal = ({
|
||||
cancelButtonProps={{
|
||||
primary: false,
|
||||
warning: true,
|
||||
children: 'Change target',
|
||||
children: i18next.t('drives.changeTarget'),
|
||||
}}
|
||||
action={"Yes, I'm sure"}
|
||||
action={i18next.t('sure')}
|
||||
primaryButtonProps={{
|
||||
primary: false,
|
||||
outline: true,
|
||||
@@ -50,7 +51,7 @@ const DriveStatusWarningModal = ({
|
||||
<Flex flexDirection="column">
|
||||
<ExclamationTriangleSvg height="2em" fill="#fca321" />
|
||||
<Txt fontSize="24px" color="#fca321">
|
||||
WARNING!
|
||||
{i18next.t('warning')}
|
||||
</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize="24px">{warningSubtitle}</Txt>
|
||||
|
@@ -59,13 +59,8 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
).map(([, error]: [string, FlashError]) => ({
|
||||
...error,
|
||||
}));
|
||||
const {
|
||||
averageSpeed,
|
||||
blockmappedSize,
|
||||
bytesWritten,
|
||||
failed,
|
||||
size,
|
||||
} = flashState.getFlashState();
|
||||
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
|
||||
flashState.getFlashState();
|
||||
const {
|
||||
skip,
|
||||
results = {
|
||||
|
@@ -17,6 +17,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { BaseButton } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export interface FlashAnotherProps {
|
||||
onClick: () => void;
|
||||
@@ -25,7 +26,7 @@ export interface FlashAnotherProps {
|
||||
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||
return (
|
||||
<BaseButton primary onClick={props.onClick}>
|
||||
Flash another
|
||||
{i18next.t('flash.another')}
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
|
@@ -17,7 +17,6 @@
|
||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
|
||||
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
||||
@@ -32,6 +31,7 @@ import { resetState } from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { Modal, Table } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
|
||||
&&& [data-display='table-head'],
|
||||
@@ -89,21 +89,34 @@ function formattedErrors(errors: FlashError[]) {
|
||||
const columns: Array<TableColumn<FlashError>> = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Target',
|
||||
label: i18next.t('flash.target'),
|
||||
},
|
||||
{
|
||||
field: 'device',
|
||||
label: 'Location',
|
||||
label: i18next.t('flash.location'),
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
label: 'Error',
|
||||
label: i18next.t('flash.error'),
|
||||
render: (message: string, { code }: FlashError) => {
|
||||
return message ?? code;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function getEffectiveSpeed(results: {
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
}) {
|
||||
const flashedSize =
|
||||
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
|
||||
const timeSpent = flashedSize / results.averageFlashingSpeed;
|
||||
return results.sourceMetadata.size / timeSpent;
|
||||
}
|
||||
|
||||
export function FlashResults({
|
||||
goToMain,
|
||||
image = '',
|
||||
@@ -117,10 +130,9 @@ export function FlashResults({
|
||||
errors: FlashError[];
|
||||
skip: boolean;
|
||||
results: {
|
||||
bytesWritten: number;
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
devices: { failed: number; successful: number };
|
||||
@@ -129,11 +141,7 @@ export function FlashResults({
|
||||
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||
const allFailed = !skip && results.devices.successful === 0;
|
||||
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
|
||||
const effectiveSpeed = _.round(
|
||||
bytesToMegabytes(
|
||||
results.sourceMetadata.size /
|
||||
(results.sourceMetadata.blockmappedSize / results.averageFlashingSpeed),
|
||||
),
|
||||
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
|
||||
1,
|
||||
);
|
||||
return (
|
||||
@@ -155,9 +163,11 @@ export function FlashResults({
|
||||
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize={24} color="#fff" mb="17px">
|
||||
Flash {allFailed ? 'Failed' : 'Complete'}!
|
||||
{allFailed
|
||||
? i18next.t('flash.flashFailed')
|
||||
: i18next.t('flash.flashCompleted')}
|
||||
</Txt>
|
||||
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
|
||||
{skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
|
||||
</Flex>
|
||||
<Flex flexDirection="column" color="#7e8085">
|
||||
{results.devices.successful !== 0 ? (
|
||||
@@ -181,7 +191,7 @@ export function FlashResults({
|
||||
{progress.failed(errors.length)}
|
||||
</Txt>
|
||||
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||
more info
|
||||
{i18next.t('flash.moreInfo')}
|
||||
</Link>
|
||||
</Flex>
|
||||
) : null}
|
||||
@@ -192,12 +202,9 @@ export function FlashResults({
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
tooltip={outdent({ newline: ' ' })`
|
||||
The speed is calculated by dividing the image size by the flashing time.
|
||||
Disk images with ext partitions flash faster as we are able to skip unused parts.
|
||||
`}
|
||||
tooltip={i18next.t('flash.speedTip')}
|
||||
>
|
||||
Effective speed: {effectiveSpeed} MB/s
|
||||
{i18next.t('flash.speed', { speed: effectiveSpeed })}
|
||||
</Txt>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -207,11 +214,11 @@ export function FlashResults({
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
Failed targets
|
||||
{i18next.t('failedTarget')}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
action="Retry failed targets"
|
||||
action={i18next.t('failedRetry')}
|
||||
cancel={() => setShowErrorsInfo(false)}
|
||||
done={() => {
|
||||
setShowErrorsInfo(false);
|
||||
|
@@ -20,6 +20,7 @@ import { default as styled } from 'styled-components';
|
||||
|
||||
import { fromFlashState } from '../../modules/progress-status';
|
||||
import { StepButton } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
> div {
|
||||
@@ -28,6 +29,7 @@ const FlashProgressBar = styled(ProgressBar)`
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
transition-duration: 0s;
|
||||
|
||||
> div {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
@@ -61,7 +63,7 @@ const colors = {
|
||||
} as const;
|
||||
|
||||
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
const status = type === 'verifying' ? 'Skip' : 'Cancel';
|
||||
const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
|
||||
return (
|
||||
<Button plain onClick={() => onClick(status)} {...props}>
|
||||
{status}
|
||||
@@ -69,6 +71,7 @@ const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
);
|
||||
})`
|
||||
font-weight: 600;
|
||||
|
||||
&&& {
|
||||
width: auto;
|
||||
height: auto;
|
||||
@@ -126,7 +129,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
marginTop: 30,
|
||||
}}
|
||||
>
|
||||
Flash!
|
||||
{i18next.t('flash.flashNow')}
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
|
@@ -31,9 +31,7 @@ interface ReducedFlashingInfosProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export class ReducedFlashingInfos extends React.Component<
|
||||
ReducedFlashingInfosProps
|
||||
> {
|
||||
export class ReducedFlashingInfos extends React.Component<ReducedFlashingInfosProps> {
|
||||
constructor(props: ReducedFlashingInfosProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
|
@@ -16,56 +16,54 @@
|
||||
|
||||
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as React from 'react';
|
||||
import { Flex, Checkbox, Txt } from 'rendition';
|
||||
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
|
||||
|
||||
import { version, packageType } from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { Modal } from '../../styled-components';
|
||||
|
||||
const platform = os.platform();
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
interface Setting {
|
||||
name: string;
|
||||
label: string | JSX.Element;
|
||||
options?: {
|
||||
description: string;
|
||||
confirmLabel: string;
|
||||
};
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
async function getSettingsList(): Promise<Setting[]> {
|
||||
return [
|
||||
const list: Setting[] = [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||
label: i18next.t('settings.errorReporting'),
|
||||
},
|
||||
{
|
||||
name: 'unmountOnSuccess',
|
||||
/**
|
||||
* On Windows, "Unmounting" basically means "ejecting".
|
||||
* On top of that, Windows users are usually not even
|
||||
* familiar with the meaning of "unmount", which comes
|
||||
* from the UNIX world.
|
||||
*/
|
||||
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
|
||||
},
|
||||
{
|
||||
name: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
hide: ['rpm', 'deb'].includes(packageType),
|
||||
name: 'autoBlockmapping',
|
||||
label: i18next.t('settings.trimExtPartitions'),
|
||||
},
|
||||
];
|
||||
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||
list.push({
|
||||
name: 'updatesEnabled',
|
||||
label: i18next.t('settings.autoUpdate'),
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const UUID = process.env.BALENA_DEVICE_UUID;
|
||||
|
||||
const InfoBox = (props: any) => (
|
||||
<Box fontSize={14}>
|
||||
<Txt>{props.label}</Txt>
|
||||
<TextWithCopy code text={props.value} copy={props.value} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
|
||||
React.useEffect(() => {
|
||||
@@ -86,50 +84,45 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
})();
|
||||
});
|
||||
|
||||
const toggleSetting = async (
|
||||
setting: string,
|
||||
options?: Setting['options'],
|
||||
) => {
|
||||
const toggleSetting = async (setting: string) => {
|
||||
const value = currentSettings[setting];
|
||||
const dangerous = options !== undefined;
|
||||
|
||||
analytics.logEvent('Toggle setting', {
|
||||
setting,
|
||||
value,
|
||||
dangerous,
|
||||
});
|
||||
|
||||
analytics.logEvent('Toggle setting', { setting, value });
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Txt fontSize={24} mb={24}>
|
||||
Settings
|
||||
{i18next.t('settings.settings')}
|
||||
</Txt>
|
||||
}
|
||||
done={() => toggleModal(false)}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
{settingsList.map((setting: Setting, i: number) => {
|
||||
return setting.hide ? null : (
|
||||
return (
|
||||
<Flex key={setting.name} mb={14}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
label={setting.label}
|
||||
checked={currentSettings[setting.name]}
|
||||
onChange={() => toggleSetting(setting.name, setting.options)}
|
||||
onChange={() => toggleSetting(setting.name)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
{UUID !== undefined && (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={24}>{i18next.t('settings.systemInformation')}</Txt>
|
||||
<InfoBox label="UUID" value={UUID.substr(0, 7)} />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
mt={18}
|
||||
alignItems="center"
|
||||
|
@@ -18,6 +18,8 @@ import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
|
||||
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
||||
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
@@ -33,6 +35,7 @@ import {
|
||||
Card as BaseCard,
|
||||
Input,
|
||||
Spinner,
|
||||
Link,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@@ -61,6 +64,9 @@ import ImageSvg from '../../../assets/image.svg';
|
||||
import SrcSvg from '../../../assets/src.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { isJson } from '../../../../shared/utils';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const recentUrlImagesKey = 'recentUrlImages';
|
||||
|
||||
@@ -72,7 +78,7 @@ function normalizeRecentUrlImages(urls: any[]): URL[] {
|
||||
.map((url) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// Invalid URL, skip
|
||||
}
|
||||
})
|
||||
@@ -133,12 +139,15 @@ const URLSelector = ({
|
||||
done,
|
||||
cancel,
|
||||
}: {
|
||||
done: (imageURL: string) => void;
|
||||
done: (imageURL: string, auth?: Authentication) => void;
|
||||
cancel: () => void;
|
||||
}) => {
|
||||
const [imageURL, setImageURL] = React.useState('');
|
||||
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
|
||||
const [username, setUsername] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
const fetchRecentUrlImages = async () => {
|
||||
const recentUrlImages: URL[] = await getRecentUrlImages();
|
||||
@@ -152,7 +161,7 @@ const URLSelector = ({
|
||||
primaryButtonProps={{
|
||||
disabled: loading || !imageURL,
|
||||
}}
|
||||
action={loading ? <Spinner /> : 'OK'}
|
||||
action={loading ? <Spinner /> : i18next.t('ok')}
|
||||
done={async () => {
|
||||
setLoading(true);
|
||||
const urlStrings = recentImages.map((url: URL) => url.href);
|
||||
@@ -161,22 +170,66 @@ const URLSelector = ({
|
||||
imageURL,
|
||||
]);
|
||||
setRecentUrlImages(normalizedRecentUrls);
|
||||
await done(imageURL);
|
||||
const auth = username ? { username, password } : undefined;
|
||||
await done(imageURL, auth);
|
||||
}}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
<Flex style={{ width: '100%' }} flexDirection="column">
|
||||
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
|
||||
<Txt mb="10px" fontSize="24px">
|
||||
Use Image URL
|
||||
{i18next.t('source.useSourceURL')}
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
placeholder={i18next.t('source.enterValidURL')}
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
fontSize="14px"
|
||||
onClick={() => {
|
||||
if (showBasicAuth) {
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
}
|
||||
setShowBasicAuth(!showBasicAuth);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
{showBasicAuth && (
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
{!showBasicAuth && (
|
||||
<ChevronRightSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
<Txt ml={8}>{i18next.t('source.auth')}</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
{showBasicAuth && (
|
||||
<React.Fragment>
|
||||
<Input
|
||||
mb={15}
|
||||
value={username}
|
||||
placeholder={i18next.t('source.username')}
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setUsername(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
placeholder={i18next.t('source.password')}
|
||||
type="password"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPassword(evt.target.value)
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Flex>
|
||||
{recentImages.length > 0 && (
|
||||
<Flex flexDirection="column" height="78.6%">
|
||||
@@ -243,7 +296,7 @@ const FlowSelector = styled(
|
||||
font-weight: 600;
|
||||
|
||||
svg {
|
||||
color: ${colors.primary.foreground}!important;
|
||||
color: ${colors.primary.foreground} !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -263,6 +316,7 @@ export interface SourceMetadata extends sourceDestination.Metadata {
|
||||
drive?: DrivelistDrive;
|
||||
extension?: string;
|
||||
archiveExtension?: string;
|
||||
auth?: Authentication;
|
||||
}
|
||||
|
||||
interface SourceSelectorProps {
|
||||
@@ -279,6 +333,12 @@ interface SourceSelectorState {
|
||||
showDriveSelector: boolean;
|
||||
defaultFlowActive: boolean;
|
||||
imageSelectorOpen: boolean;
|
||||
imageLoading: boolean;
|
||||
}
|
||||
|
||||
interface Authentication {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class SourceSelector extends React.Component<
|
||||
@@ -297,6 +357,7 @@ export class SourceSelector extends React.Component<
|
||||
showDriveSelector: false,
|
||||
defaultFlowActive: true,
|
||||
imageSelectorOpen: false,
|
||||
imageLoading: false,
|
||||
};
|
||||
|
||||
// Bind `this` since it's used in an event's callback
|
||||
@@ -317,25 +378,52 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||
this.setState({ imageLoading: true });
|
||||
await this.selectSource(
|
||||
imagePath,
|
||||
isURL(imagePath) ? sourceDestination.Http : sourceDestination.File,
|
||||
isURL(this.normalizeImagePath(imagePath))
|
||||
? sourceDestination.Http
|
||||
: sourceDestination.File,
|
||||
).promise;
|
||||
this.setState({ imageLoading: false });
|
||||
}
|
||||
|
||||
private async createSource(selected: string, SourceType: Source) {
|
||||
private async createSource(
|
||||
selected: string,
|
||||
SourceType: Source,
|
||||
auth?: Authentication,
|
||||
) {
|
||||
try {
|
||||
selected = await replaceWindowsNetworkDriveLetter(selected);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
|
||||
if (isJson(decodeURIComponent(selected))) {
|
||||
const config: AxiosRequestConfig = JSON.parse(
|
||||
decodeURIComponent(selected),
|
||||
);
|
||||
return new sourceDestination.Http({
|
||||
url: config.url!,
|
||||
axiosInstance: axios.create(_.omit(config, ['url'])),
|
||||
});
|
||||
}
|
||||
|
||||
if (SourceType === sourceDestination.File) {
|
||||
return new sourceDestination.File({
|
||||
path: selected,
|
||||
});
|
||||
}
|
||||
return new sourceDestination.Http({ url: selected });
|
||||
|
||||
return new sourceDestination.Http({ url: selected, auth });
|
||||
}
|
||||
|
||||
public normalizeImagePath(imgPath: string) {
|
||||
const decodedPath = decodeURIComponent(imgPath);
|
||||
if (isJson(decodedPath)) {
|
||||
return JSON.parse(decodedPath).url ?? decodedPath;
|
||||
}
|
||||
return decodedPath;
|
||||
}
|
||||
|
||||
private reselectSource() {
|
||||
@@ -349,6 +437,7 @@ export class SourceSelector extends React.Component<
|
||||
private selectSource(
|
||||
selected: string | DrivelistDrive,
|
||||
SourceType: Source,
|
||||
auth?: Authentication,
|
||||
): { promise: Promise<void>; cancel: () => void } {
|
||||
let cancelled = false;
|
||||
return {
|
||||
@@ -360,9 +449,12 @@ export class SourceSelector extends React.Component<
|
||||
let source;
|
||||
let metadata: SourceMetadata | undefined;
|
||||
if (isString(selected)) {
|
||||
if (SourceType === sourceDestination.Http && !isURL(selected)) {
|
||||
if (
|
||||
SourceType === sourceDestination.Http &&
|
||||
!isURL(this.normalizeImagePath(selected))
|
||||
) {
|
||||
this.handleError(
|
||||
'Unsupported protocol',
|
||||
i18next.t('source.unsupportedProtocol'),
|
||||
selected,
|
||||
messages.error.unsupportedProtocol(),
|
||||
);
|
||||
@@ -374,11 +466,11 @@ export class SourceSelector extends React.Component<
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.looksLikeWindowsImage(),
|
||||
title: 'Possible Windows image detected',
|
||||
title: i18next.t('source.windowsImage'),
|
||||
},
|
||||
});
|
||||
}
|
||||
source = await this.createSource(selected, SourceType);
|
||||
source = await this.createSource(selected, SourceType, auth);
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
@@ -395,18 +487,18 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
metadata.SourceType = SourceType;
|
||||
|
||||
if (!metadata.hasMBR) {
|
||||
if (!metadata.hasMBR && this.state.warning === null) {
|
||||
analytics.logEvent('Missing partition table', { metadata });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.missingPartitionTable(),
|
||||
title: 'Missing partition table',
|
||||
title: i18next.t('source.partitionTable'),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.handleError(
|
||||
'Error opening source',
|
||||
i18next.t('source.errorOpen'),
|
||||
sourcePath,
|
||||
messages.error.openSource(sourcePath, error.message),
|
||||
error,
|
||||
@@ -414,7 +506,7 @@ export class SourceSelector extends React.Component<
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
@@ -424,7 +516,7 @@ export class SourceSelector extends React.Component<
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.driveMissingPartitionTable(),
|
||||
title: 'Missing partition table',
|
||||
title: i18next.t('source.partitionTable'),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -439,6 +531,7 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
if (metadata !== undefined) {
|
||||
metadata.auth = auth;
|
||||
selectionState.selectSource(metadata);
|
||||
analytics.logEvent('Select image', {
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
@@ -504,7 +597,7 @@ export class SourceSelector extends React.Component<
|
||||
return;
|
||||
}
|
||||
await this.selectSource(imagePath, sourceDestination.File).promise;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
} finally {
|
||||
this.setState({ imageSelectorOpen: false });
|
||||
@@ -558,10 +651,21 @@ export class SourceSelector extends React.Component<
|
||||
this.setState({ defaultFlowActive });
|
||||
}
|
||||
|
||||
private closeModal() {
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO add a visual change when dragging a file over the selector
|
||||
public render() {
|
||||
const { flashing } = this.props;
|
||||
const { showImageDetails, showURLSelector, showDriveSelector } = this.state;
|
||||
const {
|
||||
showImageDetails,
|
||||
showURLSelector,
|
||||
showDriveSelector,
|
||||
imageLoading,
|
||||
} = this.state;
|
||||
const selectionImage = selectionState.getImage();
|
||||
let image: SourceMetadata | DrivelistDrive =
|
||||
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
||||
@@ -599,25 +703,27 @@ export class SourceSelector extends React.Component<
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectionImage !== undefined ? (
|
||||
{selectionImage !== undefined || imageLoading ? (
|
||||
<>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={() => this.showSelectedImageDetails()}
|
||||
tooltip={imageName || imageBasename}
|
||||
>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
<Spinner show={imageLoading}>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
</Spinner>
|
||||
</StepNameButton>
|
||||
{!flashing && (
|
||||
{!flashing && !imageLoading && (
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={() => this.reselectSource()}
|
||||
>
|
||||
Remove
|
||||
{i18next.t('cancel')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{!_.isNil(imageSize) && (
|
||||
{!_.isNil(imageSize) && !imageLoading && (
|
||||
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
@@ -629,7 +735,7 @@ export class SourceSelector extends React.Component<
|
||||
key="Flash from file"
|
||||
flow={{
|
||||
onClick: () => this.openImageSelector(),
|
||||
label: 'Flash from file',
|
||||
label: i18next.t('source.fromFile'),
|
||||
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
@@ -639,7 +745,7 @@ export class SourceSelector extends React.Component<
|
||||
key="Flash from URL"
|
||||
flow={{
|
||||
onClick: () => this.openURLSelector(),
|
||||
label: 'Flash from URL',
|
||||
label: i18next.t('source.fromURL'),
|
||||
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
@@ -649,7 +755,7 @@ export class SourceSelector extends React.Component<
|
||||
key="Clone drive"
|
||||
flow={{
|
||||
onClick: () => this.openDriveSelector(),
|
||||
label: 'Clone drive',
|
||||
label: i18next.t('source.clone'),
|
||||
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
@@ -661,13 +767,16 @@ export class SourceSelector extends React.Component<
|
||||
|
||||
{this.state.warning != null && (
|
||||
<SmallModal
|
||||
style={{
|
||||
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
titleElement={
|
||||
<span>
|
||||
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
|
||||
<span>{this.state.warning.title}</span>
|
||||
</span>
|
||||
}
|
||||
action="Continue"
|
||||
action={i18next.t('continue')}
|
||||
cancel={() => {
|
||||
this.setState({ warning: null });
|
||||
this.reselectSource();
|
||||
@@ -685,17 +794,17 @@ export class SourceSelector extends React.Component<
|
||||
|
||||
{showImageDetails && (
|
||||
<SmallModal
|
||||
title="Image"
|
||||
title={i18next.t('source.image')}
|
||||
done={() => {
|
||||
this.setState({ showImageDetails: false });
|
||||
}}
|
||||
>
|
||||
<Txt.p>
|
||||
<Txt.span bold>Name: </Txt.span>
|
||||
<Txt.span bold>{i18next.t('source.name')}</Txt.span>
|
||||
<Txt.span>{imageName || imageBasename}</Txt.span>
|
||||
</Txt.p>
|
||||
<Txt.p>
|
||||
<Txt.span bold>Path: </Txt.span>
|
||||
<Txt.span bold>{i18next.t('source.path')}</Txt.span>
|
||||
<Txt.span>{imagePath}</Txt.span>
|
||||
</Txt.p>
|
||||
</SmallModal>
|
||||
@@ -709,7 +818,7 @@ export class SourceSelector extends React.Component<
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
done={async (imageURL: string) => {
|
||||
done={async (imageURL: string, auth?: Authentication) => {
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (!imageURL) {
|
||||
@@ -719,6 +828,7 @@ export class SourceSelector extends React.Component<
|
||||
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||
imageURL,
|
||||
sourceDestination.Http,
|
||||
auth,
|
||||
));
|
||||
await promise;
|
||||
}
|
||||
@@ -733,24 +843,33 @@ export class SourceSelector extends React.Component<
|
||||
<DriveSelector
|
||||
write={false}
|
||||
multipleSelection={false}
|
||||
titleLabel="Select source"
|
||||
emptyListLabel="Plug a source drive"
|
||||
titleLabel={i18next.t('source.selectSource')}
|
||||
emptyListLabel={i18next.t('source.plugSource')}
|
||||
emptyListIcon={<SrcSvg width="40px" />}
|
||||
cancel={() => {
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}}
|
||||
done={async (drives: DrivelistDrive[]) => {
|
||||
if (drives.length) {
|
||||
await this.selectSource(
|
||||
drives[0],
|
||||
sourceDestination.BlockDevice,
|
||||
);
|
||||
cancel={(originalList) => {
|
||||
if (originalList.length) {
|
||||
const originalSource = originalList[0];
|
||||
if (selectionImage?.drive?.device !== originalSource.device) {
|
||||
this.selectSource(
|
||||
originalSource,
|
||||
sourceDestination.BlockDevice,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
this.closeModal();
|
||||
}}
|
||||
done={() => this.closeModal()}
|
||||
onSelect={(drive) => {
|
||||
if (drive) {
|
||||
if (
|
||||
selectionState.getImage()?.drive?.device === drive?.device
|
||||
) {
|
||||
return selectionState.deselectImage();
|
||||
}
|
||||
this.selectSource(drive, sourceDestination.BlockDevice);
|
||||
}
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@@ -32,6 +32,7 @@ import {
|
||||
StepNameButton,
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
interface TargetSelectorProps {
|
||||
targets: any[];
|
||||
@@ -95,7 +96,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
|
||||
Change
|
||||
{i18next.t('target.change')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{target.size != null && (
|
||||
@@ -132,11 +133,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{targets.length} Targets
|
||||
{targets.length} {i18next.t('target.targets')}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
|
||||
Change
|
||||
{i18next.t('target.change')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{targetsTemplate}
|
||||
@@ -151,7 +152,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
Select target
|
||||
{i18next.t('target.selectTarget')}
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { scanner } from 'etcher-sdk';
|
||||
import * as React from 'react';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
getSelectedDrives,
|
||||
deselectDrive,
|
||||
selectDrive,
|
||||
deselectAllDrives,
|
||||
} from '../../models/selection-state';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
@@ -36,6 +36,8 @@ import { TargetSelectorButton } from './target-selector-button';
|
||||
import TgtSvg from '../../../assets/tgt.svg';
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import { warning } from '../../../../shared/messages';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export const getDriveListLabel = () => {
|
||||
return getSelectedDrives()
|
||||
@@ -59,8 +61,8 @@ export const TargetSelectorModal = (
|
||||
) => (
|
||||
<DriveSelector
|
||||
multipleSelection={true}
|
||||
titleLabel="Select target"
|
||||
emptyListLabel="Plug a target drive"
|
||||
titleLabel={i18next.t('target.selectTarget')}
|
||||
emptyListLabel={i18next.t('target.plugTarget')}
|
||||
emptyListIcon={<TgtSvg width="40px" />}
|
||||
showWarnings={true}
|
||||
selectedList={getSelectedDrives()}
|
||||
@@ -69,9 +71,7 @@ export const TargetSelectorModal = (
|
||||
/>
|
||||
);
|
||||
|
||||
export const selectAllTargets = (
|
||||
modalTargets: scanner.adapters.DrivelistDrive[],
|
||||
) => {
|
||||
export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
|
||||
const selectedDrivesFromState = getSelectedDrives();
|
||||
const deselected = selectedDrivesFromState.filter(
|
||||
(drive) =>
|
||||
@@ -113,9 +113,8 @@ export const TargetSelector = ({
|
||||
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
|
||||
getDriveSelectionStateSlice(),
|
||||
);
|
||||
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
||||
false,
|
||||
);
|
||||
const [showTargetSelectorModal, setShowTargetSelectorModal] =
|
||||
React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
return observe(() => {
|
||||
@@ -164,11 +163,30 @@ export const TargetSelector = ({
|
||||
{showTargetSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
write={true}
|
||||
cancel={() => setShowTargetSelectorModal(false)}
|
||||
done={(modalTargets) => {
|
||||
selectAllTargets(modalTargets);
|
||||
cancel={(originalList) => {
|
||||
if (originalList.length) {
|
||||
selectAllTargets(originalList);
|
||||
} else {
|
||||
deselectAllDrives();
|
||||
}
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
done={(modalTargets) => {
|
||||
if (modalTargets.length === 0) {
|
||||
deselectAllDrives();
|
||||
}
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
onSelect={(drive) => {
|
||||
if (
|
||||
getSelectedDrives().find(
|
||||
(selectedDrive) => selectedDrive.device === drive.device,
|
||||
)
|
||||
) {
|
||||
return deselectDrive(drive.device);
|
||||
}
|
||||
selectDrive(drive.device);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
42
lib/gui/app/i18n.ts
Normal file
42
lib/gui/app/i18n.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import zh_CN_translation from './i18n/zh-CN';
|
||||
import zh_TW_translation from './i18n/zh-TW';
|
||||
import en_translation from './i18n/en';
|
||||
|
||||
export function langParser() {
|
||||
if (process.env.LANG !== undefined) {
|
||||
// Bypass mocha, where lang-detect don't works
|
||||
return 'en';
|
||||
}
|
||||
|
||||
const lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
|
||||
switch (lang.substr(0, 2)) {
|
||||
case 'zh':
|
||||
if (lang === 'zh-CN' || lang === 'zh-SG') {
|
||||
return 'zh-CN';
|
||||
} // Simplified Chinese
|
||||
else {
|
||||
return 'zh-TW';
|
||||
} // Traditional Chinese
|
||||
default:
|
||||
return lang.substr(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
i18next.use(initReactI18next).init({
|
||||
lng: langParser(),
|
||||
fallbackLng: 'en',
|
||||
nonExplicitSupportedLngs: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
resources: {
|
||||
'zh-CN': zh_CN_translation,
|
||||
'zh-TW': zh_TW_translation,
|
||||
en: en_translation,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18next;
|
23
lib/gui/app/i18n/README.md
Normal file
23
lib/gui/app/i18n/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# i18n
|
||||
|
||||
## How it was done
|
||||
|
||||
Using the open-source lib [i18next](https://www.i18next.com/).
|
||||
|
||||
## How to add your own language
|
||||
|
||||
1. Go to `lib/gui/app/i18n` and add a file named `xx.ts` (use the codes mentioned
|
||||
in [the link](https://www.science.co.il/language/Locale-codes.php), and we support styles as `fr`, `de`, `es-ES`
|
||||
and `pt-BR`)
|
||||
.
|
||||
2. Copy the content from an existing translation and start to translate.
|
||||
3. Once done, go to `lib/gui/app/i18n.ts` and add a line of `import xx_translation from './i18n/xx'` after the
|
||||
already-added imports and add `xx: xx_translation` in the `resources` section of `i18next.init()` function.
|
||||
4. Now go to `lib/shared/catalina-sudo/` and copy the `sudo-askpass.osascript-en.js`, change it to
|
||||
be `sudo-askpass.osascript-xx.js` and edit
|
||||
the `'balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.'` line and
|
||||
those `Ok`s and `Cancel`s to your own language.
|
||||
5. If, your language has several variations when they are used in several countries/regions, such as `zh-CN` and `zh-TW`
|
||||
, or `pt-BR` and `pt-PT`, edit
|
||||
the `langParser()` in the `lib/gui/app/i18n.ts` file to meet your need.
|
||||
6. Make a commit, and then a pull request on GitHub.
|
161
lib/gui/app/i18n/en.ts
Normal file
161
lib/gui/app/i18n/en.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
continue: 'Continue',
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
skip: 'Skip',
|
||||
sure: "Yes, I'm sure",
|
||||
warning: 'WARNING! ',
|
||||
attention: 'Attention',
|
||||
failed: 'Failed',
|
||||
completed: 'Completed',
|
||||
yesContinue: 'Yes, continue',
|
||||
reallyExit: 'Are you sure you want to close Etcher?',
|
||||
yesExit: 'Yes, quit',
|
||||
progress: {
|
||||
starting: 'Starting...',
|
||||
decompressing: 'Decompressing...',
|
||||
flashing: 'Flashing...',
|
||||
finishing: 'Finishing...',
|
||||
verifying: 'Validating...',
|
||||
failing: 'Failed',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: 'Not recommended',
|
||||
tooSmall: 'Too small',
|
||||
locked: 'Locked',
|
||||
system: 'System drive',
|
||||
containsImage: 'Source drive',
|
||||
largeDrive: 'Large drive',
|
||||
sourceLarger: 'The selected source is {{byte}} larger than this drive.',
|
||||
flashSucceed_one: 'Successful target',
|
||||
flashSucceed_other: 'Successful targets',
|
||||
flashFail_one: 'Failed target',
|
||||
flashFail_other: 'Failed targets',
|
||||
toDrive: 'to {{description}} ({{name}})',
|
||||
toTarget_one: 'to {{num}} target',
|
||||
toTarget_other: 'to {{num}} targets',
|
||||
andFailTarget_one: 'and failed to be flashed to {{num}} target',
|
||||
andFailTarget_other: 'and failed to be flashed to {{num}} targets',
|
||||
succeedTo: '{{name}} was successfully flashed {{target}}',
|
||||
exitWhileFlashing:
|
||||
'You are currently flashing a drive. Closing Etcher may leave your drive in an unusable state.',
|
||||
looksLikeWindowsImage:
|
||||
'It looks like you are trying to burn a Windows image.\n\nUnlike other images, Windows images require special processing to be made bootable. We suggest you use a tool specially designed for this purpose, such as <a href="https://rufus.akeo.ie">Rufus</a> (Windows), <a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux), or Boot Camp Assistant (macOS).',
|
||||
image: 'image',
|
||||
drive: 'drive',
|
||||
missingPartitionTable:
|
||||
'It looks like this is not a bootable {{type}}.\n\nThe {{type}} does not appear to contain a partition table, and might not be recognized or bootable by your device.',
|
||||
largeDriveSize:
|
||||
"This is a large drive! Make sure it doesn't contain files that you want to keep.",
|
||||
systemDrive:
|
||||
'Selecting your system drive is dangerous and will erase your drive!',
|
||||
sourceDrive: 'Contains the image you chose to flash',
|
||||
noSpace:
|
||||
'Not enough space on the drive. Please insert larger one and try again.',
|
||||
genericFlashError:
|
||||
'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n{{error}}',
|
||||
validation:
|
||||
'The write has been completed successfully but Etcher detected potential corruption issues when reading the image back from the drive. \n\nPlease consider writing the image to a different drive.',
|
||||
openError:
|
||||
'Something went wrong while opening {{source}}.\n\nError: {{error}}',
|
||||
flashError: 'Something went wrong while writing {{image}} {{targets}}.',
|
||||
unplug:
|
||||
"Looks like Etcher lost access to the drive. Did it get unplugged accidentally?\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
|
||||
cannotWrite:
|
||||
'Looks like Etcher is not able to write to this location of the drive. This error is usually caused by a faulty drive, reader, or port. \n\nPlease try again with another drive, reader, or port.',
|
||||
childWriterDied:
|
||||
'The writer process ended unexpectedly. Please try again, and contact the Etcher team if the problem persists.',
|
||||
badProtocol: 'Only http:// and https:// URLs are supported.',
|
||||
},
|
||||
target: {
|
||||
selectTarget: 'Select target',
|
||||
plugTarget: 'Plug a target drive',
|
||||
targets: 'Targets',
|
||||
change: 'Change',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: 'Use Image URL',
|
||||
auth: 'Authentication',
|
||||
username: 'Enter username',
|
||||
password: 'Enter password',
|
||||
unsupportedProtocol: 'Unsupported protocol',
|
||||
windowsImage: 'Possible Windows image detected',
|
||||
partitionTable: 'Missing partition table',
|
||||
errorOpen: 'Error opening source',
|
||||
fromFile: 'Flash from file',
|
||||
fromURL: 'Flash from URL',
|
||||
clone: 'Clone drive',
|
||||
image: 'Image',
|
||||
name: 'Name: ',
|
||||
path: 'Path: ',
|
||||
selectSource: 'Select source',
|
||||
plugSource: 'Plug a source drive',
|
||||
osImages: 'OS Images',
|
||||
allFiles: 'All',
|
||||
enterValidURL: 'Enter a valid URL',
|
||||
},
|
||||
drives: {
|
||||
name: 'Name',
|
||||
size: 'Size',
|
||||
location: 'Location',
|
||||
find: '{{length}} found',
|
||||
select: 'Select {{select}}',
|
||||
showHidden: 'Show {{num}} hidden',
|
||||
systemDriveDanger:
|
||||
'Selecting your system drive is dangerous and will erase your drive!',
|
||||
openInBrowser: '`Etcher will open {{link}} in your browser`',
|
||||
changeTarget: 'Change target',
|
||||
largeDriveWarning: 'You are about to erase an unusually large drive',
|
||||
largeDriveWarningMsg:
|
||||
'Are you sure the selected drive is not a storage drive?',
|
||||
systemDriveWarning: "You are about to erase your computer's drives",
|
||||
systemDriveWarningMsg:
|
||||
'Are you sure you want to flash your system drive?',
|
||||
},
|
||||
flash: {
|
||||
another: 'Flash another',
|
||||
target: 'Target',
|
||||
location: 'Location',
|
||||
error: 'Error',
|
||||
flash: 'Flash',
|
||||
flashNow: 'Flash!',
|
||||
skip: 'Validation has been skipped',
|
||||
moreInfo: 'more info',
|
||||
speedTip:
|
||||
'The speed is calculated by dividing the image size by the flashing time.\nDisk images with ext partitions flash faster as we are able to skip unused parts.',
|
||||
speed: 'Effective speed: {{speed}} MB/s',
|
||||
speedShort: '{{speed}} MB/s',
|
||||
eta: 'ETA: {{eta}}',
|
||||
failedTarget: 'Failed targets',
|
||||
failedRetry: 'Retry failed targets',
|
||||
flashFailed: 'Flash Failed.',
|
||||
flashCompleted: 'Flash Completed!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting:
|
||||
'Anonymously report errors and usage statistics to balena.io',
|
||||
autoUpdate: 'Auto-updates enabled',
|
||||
settings: 'Settings',
|
||||
systemInformation: 'System Information',
|
||||
trimExtPartitions: 'Trim unallocated space on raw images (in ext-type partitions)',
|
||||
},
|
||||
menu: {
|
||||
edit: 'Edit',
|
||||
view: 'View',
|
||||
devTool: 'Toggle Developer Tools',
|
||||
window: 'Window',
|
||||
help: 'Help',
|
||||
pro: 'Etcher Pro',
|
||||
website: 'Etcher Website',
|
||||
issue: 'Report an issue',
|
||||
about: 'About Etcher',
|
||||
hide: 'Hide Etcher',
|
||||
hideOthers: 'Hide Others',
|
||||
unhide: 'Unhide All',
|
||||
quit: 'Quit Etcher',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
152
lib/gui/app/i18n/zh-CN.ts
Normal file
152
lib/gui/app/i18n/zh-CN.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
ok: '好',
|
||||
cancel: '取消',
|
||||
continue: '继续',
|
||||
skip: '跳过',
|
||||
sure: '我确定',
|
||||
warning: '请注意!',
|
||||
attention: '请注意',
|
||||
failed: '失败',
|
||||
completed: '完毕',
|
||||
yesExit: '是的,可以退出',
|
||||
reallyExit: '真的要现在退出 Etcher 吗?',
|
||||
yesContinue: '是的,继续',
|
||||
progress: {
|
||||
starting: '正在启动……',
|
||||
decompressing: '正在解压……',
|
||||
flashing: '正在烧录……',
|
||||
finishing: '正在结束……',
|
||||
verifying: '正在验证……',
|
||||
failing: '失败……',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: '大小不推荐',
|
||||
tooSmall: '空间太小',
|
||||
locked: '被锁定',
|
||||
system: '系统盘',
|
||||
containsImage: '存放源镜像',
|
||||
largeDrive: '很大的磁盘',
|
||||
sourceLarger: '所选的镜像比目标盘大了 {{byte}} 比特。',
|
||||
flashSucceed_one: '烧录成功',
|
||||
flashSucceed_other: '烧录成功',
|
||||
flashFail_one: '烧录失败',
|
||||
flashFail_other: '烧录失败',
|
||||
toDrive: '到 {{description}} ({{name}})',
|
||||
toTarget_one: '到 {{num}} 个目标',
|
||||
toTarget_other: '到 {{num}} 个目标',
|
||||
andFailTarget_one: '并烧录失败了 {{num}} 个目标',
|
||||
andFailTarget_other: '并烧录失败了 {{num}} 个目标',
|
||||
succeedTo: '{{name}} 被成功烧录 {{target}}',
|
||||
exitWhileFlashing:
|
||||
'您当前正在刷机。 关闭 Etcher 可能会导致您的磁盘无法使用。',
|
||||
looksLikeWindowsImage:
|
||||
'看起来您正在尝试刻录 Windows 镜像。\n\n与其他镜像不同,Windows 镜像需要特殊处理才能使其可启动。 我们建议您使用专门为此目的设计的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
|
||||
image: '镜像',
|
||||
drive: '磁盘',
|
||||
missingPartitionTable:
|
||||
'看起来这不是一个可启动的{{type}}。\n\n这个{{type}}似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。',
|
||||
largeDriveSize: '这是个很大的磁盘!请检查并确认它不包含对您很重要的信息',
|
||||
systemDrive: '选择系统盘很危险,因为这将会删除你的系统',
|
||||
sourceDrive: '源镜像位于这个分区中',
|
||||
noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。',
|
||||
genericFlashError:
|
||||
'出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。\n{{error}}',
|
||||
validation:
|
||||
'写入已成功完成,但 Etcher 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。',
|
||||
openError: '打开 {{source}} 时出错。\n\n错误信息: {{error}}',
|
||||
flashError: '烧录 {{image}} {{targets}} 失败。',
|
||||
unplug:
|
||||
'看起来 Etcher 失去了对磁盘的连接。 它是不是被意外拔掉了?\n\n有时这个错误是因为读卡器出了故障。',
|
||||
cannotWrite:
|
||||
'看起来 Etcher 无法写入磁盘的这个位置。 此错误通常是由故障的磁盘、读取器或端口引起的。 \n\n请使用其他磁盘、读卡器或端口重试。',
|
||||
childWriterDied:
|
||||
'写入进程意外崩溃。请再试一次,如果问题仍然存在,请联系 Etcher 团队。',
|
||||
badProtocol: '仅支持 http:// 和 https:// 开头的网址。',
|
||||
},
|
||||
target: {
|
||||
selectTarget: '选择目标磁盘',
|
||||
plugTarget: '请插入目标磁盘',
|
||||
targets: '个目标',
|
||||
change: '更改',
|
||||
},
|
||||
menu: {
|
||||
edit: '编辑',
|
||||
view: '视图',
|
||||
devTool: '打开开发者工具',
|
||||
window: '窗口',
|
||||
help: '帮助',
|
||||
pro: 'Etcher 专业版',
|
||||
website: 'Etcher 的官网',
|
||||
issue: '提交一个 issue',
|
||||
about: '关于 Etcher',
|
||||
hide: '隐藏 Etcher',
|
||||
hideOthers: '隐藏其它窗口',
|
||||
unhide: '取消隐藏',
|
||||
quit: '退出 Etcher',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: '使用镜像网络地址',
|
||||
auth: '验证',
|
||||
username: '输入用户名',
|
||||
password: '输入密码',
|
||||
unsupportedProtocol: '不支持的协议',
|
||||
windowsImage: '这可能是 Windows 系统镜像',
|
||||
partitionTable: '找不到分区表',
|
||||
errorOpen: '打开源镜像时出错',
|
||||
fromFile: '从文件烧录',
|
||||
fromURL: '从在线地址烧录',
|
||||
clone: '克隆磁盘',
|
||||
image: '镜像信息',
|
||||
name: '名称:',
|
||||
path: '路径:',
|
||||
selectSource: '选择源',
|
||||
plugSource: '请插入源磁盘',
|
||||
osImages: '系统镜像格式',
|
||||
allFiles: '任何文件格式',
|
||||
enterValidURL: '请输入一个正确的地址',
|
||||
},
|
||||
drives: {
|
||||
name: '名称',
|
||||
size: '大小',
|
||||
location: '位置',
|
||||
find: '找到 {{length}} 个',
|
||||
select: '选定 {{select}}',
|
||||
showHidden: '显示 {{num}} 个隐藏的磁盘',
|
||||
systemDriveDanger: '选择系统盘很危险,因为这将会删除你的系统!',
|
||||
openInBrowser: 'Etcher 会在浏览器中打开 {{link}}',
|
||||
changeTarget: '改变目标',
|
||||
largeDriveWarning: '您即将擦除一个非常大的磁盘',
|
||||
largeDriveWarningMsg: '您确定所选磁盘不是存储磁盘吗?',
|
||||
systemDriveWarning: '您将要擦除系统盘',
|
||||
systemDriveWarningMsg: '您确定要烧录到系统盘吗?',
|
||||
},
|
||||
flash: {
|
||||
another: '烧录另一目标',
|
||||
target: '目标',
|
||||
location: '位置',
|
||||
error: '错误',
|
||||
flash: '烧录',
|
||||
flashNow: '现在烧录!',
|
||||
skip: '跳过了验证',
|
||||
moreInfo: '更多信息',
|
||||
speedTip:
|
||||
'通过将镜像大小除以烧录时间来计算速度。\n由于我们能够跳过未使用的部分,因此具有EXT分区的磁盘镜像烧录速度更快。',
|
||||
speed: '速度:{{speed}} MB/秒',
|
||||
speedShort: '{{speed}} MB/秒',
|
||||
eta: '预计还需要:{{eta}}',
|
||||
failedTarget: '失败的烧录目标',
|
||||
failedRetry: '重试烧录失败目标',
|
||||
flashFailed: '烧录失败。',
|
||||
flashCompleted: '烧录成功!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting: '匿名地向 balena.io 报告运行错误和使用统计',
|
||||
autoUpdate: '自动更新',
|
||||
settings: '软件设置',
|
||||
systemInformation: '系统信息',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
152
lib/gui/app/i18n/zh-TW.ts
Normal file
152
lib/gui/app/i18n/zh-TW.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
ok: '好',
|
||||
cancel: '取消',
|
||||
continue: '繼續',
|
||||
skip: '跳過',
|
||||
sure: '我確定',
|
||||
warning: '請注意!',
|
||||
attention: '請注意',
|
||||
failed: '失敗',
|
||||
completed: '完畢',
|
||||
yesExit: '是的,可以退出',
|
||||
reallyExit: '真的要現在退出 Etcher 嗎?',
|
||||
yesContinue: '是的,繼續',
|
||||
progress: {
|
||||
starting: '正在啓動……',
|
||||
decompressing: '正在解壓……',
|
||||
flashing: '正在燒錄……',
|
||||
finishing: '正在結束……',
|
||||
verifying: '正在驗證……',
|
||||
failing: '失敗……',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: '大小不推薦',
|
||||
tooSmall: '空間太小',
|
||||
locked: '被鎖定',
|
||||
system: '系統盤',
|
||||
containsImage: '存放源鏡像',
|
||||
largeDrive: '很大的磁盤',
|
||||
sourceLarger: '所選的鏡像比目標盤大了 {{byte}} 比特。',
|
||||
flashSucceed_one: '燒錄成功',
|
||||
flashSucceed_other: '燒錄成功',
|
||||
flashFail_one: '燒錄失敗',
|
||||
flashFail_other: '燒錄失敗',
|
||||
toDrive: '到 {{description}} ({{name}})',
|
||||
toTarget_one: '到 {{num}} 個目標',
|
||||
toTarget_other: '到 {{num}} 個目標',
|
||||
andFailTarget_one: '並燒錄失敗了 {{num}} 個目標',
|
||||
andFailTarget_other: '並燒錄失敗了 {{num}} 個目標',
|
||||
succeedTo: '{{name}} 被成功燒錄 {{target}}',
|
||||
exitWhileFlashing:
|
||||
'您當前正在刷機。 關閉 Etcher 可能會導致您的磁盤無法使用。',
|
||||
looksLikeWindowsImage:
|
||||
'看起來您正在嘗試刻錄 Windows 鏡像。\n\n與其他鏡像不同,Windows 鏡像需要特殊處理才能使其可啓動。 我們建議您使用專門爲此目的設計的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
|
||||
image: '鏡像',
|
||||
drive: '磁盤',
|
||||
missingPartitionTable:
|
||||
'看起來這不是一個可啓動的{{type}}。\n\n這個{{type}}似乎不包含分區表,因此您的設備可能無法識別或無法正確啓動。',
|
||||
largeDriveSize: '這是個很大的磁盤!請檢查並確認它不包含對您很重要的信息',
|
||||
systemDrive: '選擇系統盤很危險,因爲這將會刪除你的系統',
|
||||
sourceDrive: '源鏡像位於這個分區中',
|
||||
noSpace: '磁盤空間不足。 請插入另一個較大的磁盤並重試。',
|
||||
genericFlashError:
|
||||
'出了點問題。如果源鏡像曾被壓縮過,請檢查它是否已損壞。\n{{error}}',
|
||||
validation:
|
||||
'寫入已成功完成,但 Etcher 在從磁盤讀取鏡像時檢測到潛在的損壞問題。 \n\n請考慮將鏡像寫入其他磁盤。',
|
||||
openError: '打開 {{source}} 時出錯。\n\n錯誤信息: {{error}}',
|
||||
flashError: '燒錄 {{image}} {{targets}} 失敗。',
|
||||
unplug:
|
||||
'看起來 Etcher 失去了對磁盤的連接。 它是不是被意外拔掉了?\n\n有時這個錯誤是因爲讀卡器出了故障。',
|
||||
cannotWrite:
|
||||
'看起來 Etcher 無法寫入磁盤的這個位置。 此錯誤通常是由故障的磁盤、讀取器或端口引起的。 \n\n請使用其他磁盤、讀卡器或端口重試。',
|
||||
childWriterDied:
|
||||
'寫入進程意外崩潰。請再試一次,如果問題仍然存在,請聯繫 Etcher 團隊。',
|
||||
badProtocol: '僅支持 http:// 和 https:// 開頭的網址。',
|
||||
},
|
||||
target: {
|
||||
selectTarget: '選擇目標磁盤',
|
||||
plugTarget: '請插入目標磁盤',
|
||||
targets: '個目標',
|
||||
change: '更改',
|
||||
},
|
||||
menu: {
|
||||
edit: '編輯',
|
||||
view: '視圖',
|
||||
devTool: '打開開發者工具',
|
||||
window: '窗口',
|
||||
help: '幫助',
|
||||
pro: 'Etcher 專業版',
|
||||
website: 'Etcher 的官網',
|
||||
issue: '提交一個 issue',
|
||||
about: '關於 Etcher',
|
||||
hide: '隱藏 Etcher',
|
||||
hideOthers: '隱藏其它窗口',
|
||||
unhide: '取消隱藏',
|
||||
quit: '退出 Etcher',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: '使用鏡像網絡地址',
|
||||
auth: '驗證',
|
||||
username: '輸入用戶名',
|
||||
password: '輸入密碼',
|
||||
unsupportedProtocol: '不支持的協議',
|
||||
windowsImage: '這可能是 Windows 系統鏡像',
|
||||
partitionTable: '找不到分區表',
|
||||
errorOpen: '打開源鏡像時出錯',
|
||||
fromFile: '從文件燒錄',
|
||||
fromURL: '從在線地址燒錄',
|
||||
clone: '克隆磁盤',
|
||||
image: '鏡像信息',
|
||||
name: '名稱:',
|
||||
path: '路徑:',
|
||||
selectSource: '選擇源',
|
||||
plugSource: '請插入源磁盤',
|
||||
osImages: '系統鏡像格式',
|
||||
allFiles: '任何文件格式',
|
||||
enterValidURL: '請輸入一個正確的地址',
|
||||
},
|
||||
drives: {
|
||||
name: '名稱',
|
||||
size: '大小',
|
||||
location: '位置',
|
||||
find: '找到 {{length}} 個',
|
||||
select: '選定 {{select}}',
|
||||
showHidden: '顯示 {{num}} 個隱藏的磁盤',
|
||||
systemDriveDanger: '選擇系統盤很危險,因爲這將會刪除你的系統!',
|
||||
openInBrowser: 'Etcher 會在瀏覽器中打開 {{link}}',
|
||||
changeTarget: '改變目標',
|
||||
largeDriveWarning: '您即將擦除一個非常大的磁盤',
|
||||
largeDriveWarningMsg: '您確定所選磁盤不是存儲磁盤嗎?',
|
||||
systemDriveWarning: '您將要擦除系統盤',
|
||||
systemDriveWarningMsg: '您確定要燒錄到系統盤嗎?',
|
||||
},
|
||||
flash: {
|
||||
another: '燒錄另一目標',
|
||||
target: '目標',
|
||||
location: '位置',
|
||||
error: '錯誤',
|
||||
flash: '燒錄',
|
||||
flashNow: '現在燒錄!',
|
||||
skip: '跳過了驗證',
|
||||
moreInfo: '更多信息',
|
||||
speedTip:
|
||||
'通過將鏡像大小除以燒錄時間來計算速度。\n由於我們能夠跳過未使用的部分,因此具有EXT分區的磁盤鏡像燒錄速度更快。',
|
||||
speed: '速度:{{speed}} MB/秒',
|
||||
speedShort: '{{speed}} MB/秒',
|
||||
eta: '預計還需要:{{eta}}',
|
||||
failedTarget: '失敗的燒錄目標',
|
||||
failedRetry: '重試燒錄失敗目標',
|
||||
flashFailed: '燒錄失敗。',
|
||||
flashCompleted: '燒錄成功!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting: '匿名地向 balena.io 報告運行錯誤和使用統計',
|
||||
autoUpdate: '自動更新',
|
||||
settings: '軟件設置',
|
||||
systemInformation: '系統信息',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
12
lib/gui/app/index.dev.html
Normal file
12
lib/gui/app/index.dev.html
Normal 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>
|
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Etcher</title>
|
||||
<title>balenaEtcher</title>
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@@ -14,9 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { bytesToMegabytes } from '../../../shared/units';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
@@ -45,6 +46,8 @@ export function isFlashing(): boolean {
|
||||
* start a flash process.
|
||||
*/
|
||||
export function setFlashingFlag() {
|
||||
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
|
||||
electron.ipcRenderer.invoke('disable-screensaver');
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASHING_FLAG,
|
||||
data: {},
|
||||
@@ -66,6 +69,8 @@ export function unsetFlashingFlag(results: {
|
||||
type: Actions.UNSET_FLASHING_FLAG,
|
||||
data: results,
|
||||
});
|
||||
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
|
||||
electron.ipcRenderer.invoke('enable-screensaver');
|
||||
}
|
||||
|
||||
export function setDevicePaths(devicePaths: string[]) {
|
||||
@@ -79,12 +84,16 @@ export function addFailedDeviceError({
|
||||
device,
|
||||
error,
|
||||
}: {
|
||||
device: sdk.scanner.adapters.DrivelistDrive;
|
||||
device: DrivelistDrive;
|
||||
error: Error;
|
||||
}) {
|
||||
const failedDeviceErrorsMap = new Map(
|
||||
store.getState().toJS().failedDeviceErrors,
|
||||
);
|
||||
if (failedDeviceErrorsMap.has(device.device)) {
|
||||
// Only store the first error
|
||||
return;
|
||||
}
|
||||
failedDeviceErrorsMap.set(device.device, {
|
||||
description: device.description,
|
||||
device: device.device,
|
||||
|
@@ -15,40 +15,19 @@
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import {
|
||||
isSourceDrive,
|
||||
DrivelistDrive,
|
||||
isSourceDrive,
|
||||
} from '../../../shared/drive-constraints';
|
||||
import { getDrives } from './available-drives';
|
||||
import { getSelectedDrives } from './selection-state';
|
||||
import * as settings from './settings';
|
||||
import { DEFAULT_STATE, observe } from './store';
|
||||
import { observe, store } from './store';
|
||||
|
||||
const leds: Map<string, RGBLed> = new Map();
|
||||
|
||||
function setLeds(
|
||||
drivesPaths: Set<string>,
|
||||
colorOrAnimation: Color | AnimationFunction,
|
||||
frequency?: number,
|
||||
) {
|
||||
for (const path of drivesPaths) {
|
||||
const led = leds.get(path);
|
||||
if (led) {
|
||||
if (Array.isArray(colorOrAnimation)) {
|
||||
led.setStaticColor(colorOrAnimation);
|
||||
} else {
|
||||
led.setAnimation(colorOrAnimation, frequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const red: Color = [1, 0, 0];
|
||||
const green: Color = [0, 1, 0];
|
||||
const blue: Color = [0, 0, 1];
|
||||
const white: Color = [1, 1, 1];
|
||||
const black: Color = [0, 0, 0];
|
||||
const purple: Color = [0.5, 0, 0.5];
|
||||
const animator = new Animator([], 10);
|
||||
|
||||
function createAnimationFunction(
|
||||
intensityFunction: (t: number) => number,
|
||||
@@ -56,21 +35,39 @@ function createAnimationFunction(
|
||||
): AnimationFunction {
|
||||
return (t: number): Color => {
|
||||
const intensity = intensityFunction(t);
|
||||
return color.map((v) => v * intensity) as Color;
|
||||
return color.map((v: number) => v * intensity) as Color;
|
||||
};
|
||||
}
|
||||
|
||||
function blink(t: number) {
|
||||
return Math.floor(t / 1000) % 2;
|
||||
return Math.floor(t) % 2;
|
||||
}
|
||||
|
||||
function breathe(t: number) {
|
||||
return (1 + Math.sin(t / 1000)) / 2;
|
||||
function one(_t: number) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const breatheBlue = createAnimationFunction(breathe, blue);
|
||||
const blinkGreen = createAnimationFunction(blink, green);
|
||||
const blinkPurple = createAnimationFunction(blink, purple);
|
||||
type LEDColors = {
|
||||
green: Color;
|
||||
purple: Color;
|
||||
red: Color;
|
||||
blue: Color;
|
||||
white: Color;
|
||||
black: Color;
|
||||
};
|
||||
|
||||
type LEDAnimationFunctions = {
|
||||
blinkGreen: AnimationFunction;
|
||||
blinkPurple: AnimationFunction;
|
||||
staticRed: AnimationFunction;
|
||||
staticGreen: AnimationFunction;
|
||||
staticBlue: AnimationFunction;
|
||||
staticWhite: AnimationFunction;
|
||||
staticBlack: AnimationFunction;
|
||||
};
|
||||
|
||||
let ledColors: LEDColors;
|
||||
let ledAnimationFunctions: LEDAnimationFunctions;
|
||||
|
||||
interface LedsState {
|
||||
step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||
@@ -80,6 +77,17 @@ interface LedsState {
|
||||
failedDrives: string[];
|
||||
}
|
||||
|
||||
function setLeds(animation: AnimationFunction, drivesPaths: Set<string>) {
|
||||
const rgbLeds: RGBLed[] = [];
|
||||
for (const path of drivesPaths) {
|
||||
const led = leds.get(path);
|
||||
if (led) {
|
||||
rgbLeds.push(led);
|
||||
}
|
||||
}
|
||||
return { animation, rgbLeds };
|
||||
}
|
||||
|
||||
// Source slot (1st slot): behaves as a target unless it is chosen as source
|
||||
// No drive: black
|
||||
// Drive plugged: blue - on
|
||||
@@ -110,6 +118,7 @@ export function updateLeds({
|
||||
// Remove selected devices from plugged set
|
||||
for (const d of selectedOk) {
|
||||
plugged.delete(d);
|
||||
unplugged.delete(d);
|
||||
}
|
||||
|
||||
// Remove plugged devices from unplugged set
|
||||
@@ -122,74 +131,90 @@ export function updateLeds({
|
||||
selectedOk.delete(d);
|
||||
}
|
||||
|
||||
const mapping: Array<{
|
||||
animation: AnimationFunction;
|
||||
rgbLeds: RGBLed[];
|
||||
}> = [];
|
||||
// Handle source slot
|
||||
if (sourceDrive !== undefined) {
|
||||
if (unplugged.has(sourceDrive)) {
|
||||
unplugged.delete(sourceDrive);
|
||||
// TODO
|
||||
setLeds(new Set([sourceDrive]), breatheBlue, 2);
|
||||
} else if (plugged.has(sourceDrive)) {
|
||||
if (plugged.has(sourceDrive)) {
|
||||
plugged.delete(sourceDrive);
|
||||
setLeds(new Set([sourceDrive]), blue);
|
||||
mapping.push(
|
||||
setLeds(ledAnimationFunctions.staticBlue, new Set([sourceDrive])),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (step === 'main') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, white);
|
||||
setLeds(selectedFailed, white);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticWhite,
|
||||
new Set([...selectedOk, ...selectedFailed]),
|
||||
),
|
||||
);
|
||||
} else if (step === 'flashing') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, blinkPurple, 2);
|
||||
setLeds(selectedFailed, red);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.blinkPurple, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
} else if (step === 'verifying') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, blinkGreen, 2);
|
||||
setLeds(selectedFailed, red);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.blinkGreen, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
} else if (step === 'finish') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, green);
|
||||
setLeds(selectedFailed, red);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.staticGreen, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface DeviceFromState {
|
||||
devicePath?: string;
|
||||
device: string;
|
||||
animator.mapping = mapping;
|
||||
}
|
||||
|
||||
let ledsState: LedsState | undefined;
|
||||
|
||||
function stateObserver(state: typeof DEFAULT_STATE) {
|
||||
const s = state.toJS();
|
||||
function stateObserver() {
|
||||
const s = store.getState().toJS();
|
||||
let step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||
if (s.isFlashing) {
|
||||
step = s.flashState.type;
|
||||
} else {
|
||||
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
|
||||
}
|
||||
const availableDrives = s.availableDrives.filter(
|
||||
(d: DeviceFromState) => d.devicePath,
|
||||
const availableDrives = getDrives().filter(
|
||||
(d: DrivelistDrive) => d.devicePath,
|
||||
);
|
||||
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
|
||||
isSourceDrive(d, s.selection.image),
|
||||
)[0]?.devicePath;
|
||||
const availableDrivesPaths = availableDrives.map(
|
||||
(d: DeviceFromState) => d.devicePath,
|
||||
(d: DrivelistDrive) => d.devicePath,
|
||||
);
|
||||
let selectedDrivesPaths: string[];
|
||||
if (step === 'main') {
|
||||
selectedDrivesPaths = availableDrives
|
||||
.filter((d: DrivelistDrive) => s.selection.devices.includes(d.device))
|
||||
.map((d: DrivelistDrive) => d.devicePath);
|
||||
selectedDrivesPaths = getSelectedDrives()
|
||||
.filter((drive) => drive.devicePath !== null)
|
||||
.map((drive) => drive.devicePath) as string[];
|
||||
} else {
|
||||
selectedDrivesPaths = s.devicePaths;
|
||||
}
|
||||
const failedDevicePaths = s.failedDeviceErrors.map(
|
||||
([devicePath]: [string]) => devicePath,
|
||||
([, { devicePath }]: [string, { devicePath: string }]) => devicePath,
|
||||
);
|
||||
const newLedsState = {
|
||||
step,
|
||||
@@ -197,7 +222,7 @@ function stateObserver(state: typeof DEFAULT_STATE) {
|
||||
availableDrives: availableDrivesPaths,
|
||||
selectedDrives: selectedDrivesPaths,
|
||||
failedDrives: failedDevicePaths,
|
||||
};
|
||||
} as LedsState;
|
||||
if (!_.isEqual(newLedsState, ledsState)) {
|
||||
updateLeds(newLedsState);
|
||||
ledsState = newLedsState;
|
||||
@@ -220,6 +245,16 @@ export async function init(): Promise<void> {
|
||||
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
|
||||
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
|
||||
}
|
||||
ledColors = (await settings.get('ledColors')) || {};
|
||||
ledAnimationFunctions = {
|
||||
blinkGreen: createAnimationFunction(blink, ledColors['green']),
|
||||
blinkPurple: createAnimationFunction(blink, ledColors['purple']),
|
||||
staticRed: createAnimationFunction(one, ledColors['red']),
|
||||
staticGreen: createAnimationFunction(one, ledColors['green']),
|
||||
staticBlue: createAnimationFunction(one, ledColors['blue']),
|
||||
staticWhite: createAnimationFunction(one, ledColors['white']),
|
||||
staticBlack: createAnimationFunction(one, ledColors['black']),
|
||||
};
|
||||
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
|
||||
}
|
||||
}
|
||||
|
@@ -51,7 +51,7 @@ async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
||||
let contents = '{}';
|
||||
try {
|
||||
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// noop
|
||||
}
|
||||
try {
|
||||
@@ -77,8 +77,7 @@ export async function writeConfigFile(
|
||||
|
||||
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
|
||||
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
|
||||
desktopNotifications: true,
|
||||
autoBlockmapping: true,
|
||||
decompressFirst: true,
|
||||
@@ -105,7 +104,7 @@ export async function set(
|
||||
settings[key] = value;
|
||||
try {
|
||||
await writeConfigFileFn(CONFIG_PATH, settings);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
throw error;
|
||||
|
@@ -102,10 +102,9 @@ function validateMixpanelConfig(config: {
|
||||
* This function sends the debug message to product analytics services.
|
||||
*/
|
||||
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
|
||||
const {
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
} = store.getState().toJS();
|
||||
const { applicationSessionUuid, flashingWorkflowUuid } = store
|
||||
.getState()
|
||||
.toJS();
|
||||
resinCorvus.logEvent(message, {
|
||||
...data,
|
||||
sample: mixpanelSample,
|
||||
|
@@ -15,10 +15,15 @@
|
||||
*/
|
||||
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import {
|
||||
Adapter,
|
||||
BlockDeviceAdapter,
|
||||
UsbbootDeviceAdapter,
|
||||
} from 'etcher-sdk/build/scanner/adapters';
|
||||
import { geteuid, platform } from 'process';
|
||||
|
||||
const adapters: sdk.scanner.adapters.Adapter[] = [
|
||||
new sdk.scanner.adapters.BlockDeviceAdapter({
|
||||
const adapters: Adapter[] = [
|
||||
new BlockDeviceAdapter({
|
||||
includeSystemDrives: () => true,
|
||||
}),
|
||||
];
|
||||
@@ -26,14 +31,15 @@ const adapters: sdk.scanner.adapters.Adapter[] = [
|
||||
// Can't use permissions.isElevated() here as it returns a promise and we need to set
|
||||
// module.exports = scanner right now.
|
||||
if (platform !== 'linux' || geteuid() === 0) {
|
||||
adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter());
|
||||
adapters.push(new UsbbootDeviceAdapter());
|
||||
}
|
||||
|
||||
if (
|
||||
platform === 'win32' &&
|
||||
sdk.scanner.adapters.DriverlessDeviceAdapter !== undefined
|
||||
) {
|
||||
adapters.push(new sdk.scanner.adapters.DriverlessDeviceAdapter());
|
||||
if (platform === 'win32') {
|
||||
const {
|
||||
DriverlessDeviceAdapter: driverless,
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
} = require('etcher-sdk/build/scanner/adapters/driverless');
|
||||
adapters.push(new driverless());
|
||||
}
|
||||
|
||||
export const scanner = new sdk.scanner.Scanner(adapters);
|
||||
|
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as electron from 'electron';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import { Dictionary } from 'lodash';
|
||||
import * as ipc from 'node-ipc';
|
||||
@@ -25,6 +24,7 @@ import * as path from 'path';
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as permissions from '../../../shared/permissions';
|
||||
import { getAppPath } from '../../../shared/utils';
|
||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||
import * as flashState from '../models/flash-state';
|
||||
import * as selectionState from '../models/selection-state';
|
||||
@@ -93,11 +93,7 @@ function terminateServer() {
|
||||
}
|
||||
|
||||
function writerArgv(): string[] {
|
||||
let entryPoint = path.join(
|
||||
electron.remote.app.getAppPath(),
|
||||
'generated',
|
||||
'child-writer.js',
|
||||
);
|
||||
let entryPoint = path.join(getAppPath(), 'generated', 'child-writer.js');
|
||||
// AppImages run over FUSE, so the files inside the mount point
|
||||
// can only be accessed by the user that mounted the AppImage.
|
||||
// This means we can't re-spawn Etcher as root from the same
|
||||
@@ -151,11 +147,7 @@ async function performWrite(
|
||||
let cancelled = false;
|
||||
let skip = false;
|
||||
ipc.serve();
|
||||
const {
|
||||
unmountOnSuccess,
|
||||
autoBlockmapping,
|
||||
decompressFirst,
|
||||
} = await settings.getAll();
|
||||
const { autoBlockmapping, decompressFirst } = await settings.getAll();
|
||||
return await new Promise((resolve, reject) => {
|
||||
ipc.server.on('error', (error) => {
|
||||
terminateServer();
|
||||
@@ -174,7 +166,6 @@ async function performWrite(
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess,
|
||||
};
|
||||
|
||||
ipc.server.on('fail', ({ device, error }) => {
|
||||
@@ -211,7 +202,6 @@ async function performWrite(
|
||||
destinations: drives,
|
||||
SourceType: image.SourceType.name,
|
||||
autoBlockmapping,
|
||||
unmountOnSuccess,
|
||||
decompressFirst,
|
||||
});
|
||||
});
|
||||
@@ -228,7 +218,7 @@ async function performWrite(
|
||||
});
|
||||
flashResults.cancelled = cancelled || results.cancelled;
|
||||
flashResults.skip = skip;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// This happens when the child is killed using SIGKILL
|
||||
const SIGKILL_EXIT_CODE = 137;
|
||||
if (error.code === SIGKILL_EXIT_CODE) {
|
||||
@@ -278,7 +268,7 @@ export async function flash(
|
||||
throw new Error('There is already a flash in progress');
|
||||
}
|
||||
|
||||
flashState.setFlashingFlag();
|
||||
await flashState.setFlashingFlag();
|
||||
flashState.setDevicePaths(
|
||||
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
|
||||
);
|
||||
@@ -290,16 +280,18 @@ export async function flash(
|
||||
uuid: flashState.getFlashUuid(),
|
||||
status: 'started',
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
||||
};
|
||||
|
||||
analytics.logEvent('Flash', analyticsData);
|
||||
|
||||
try {
|
||||
const result = await write(image, drives, flashState.setProgressState);
|
||||
flashState.unsetFlashingFlag(result);
|
||||
} catch (error) {
|
||||
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
|
||||
await flashState.unsetFlashingFlag(result);
|
||||
} catch (error: any) {
|
||||
await flashState.unsetFlashingFlag({
|
||||
cancelled: false,
|
||||
errorCode: error.code,
|
||||
});
|
||||
windowProgress.clear();
|
||||
const { results = {} } = flashState.getFlashResults();
|
||||
const eventData = {
|
||||
@@ -345,7 +337,6 @@ export async function cancel(type: string) {
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
||||
status,
|
||||
};
|
||||
analytics.logEvent('Cancel', analyticsData);
|
||||
@@ -358,7 +349,7 @@ export async function cancel(type: string) {
|
||||
if (socket !== undefined) {
|
||||
ipc.server.emit(socket, status);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export interface FlashState {
|
||||
active: number;
|
||||
@@ -34,36 +35,45 @@ export function fromFlashState({
|
||||
position?: string;
|
||||
} {
|
||||
if (type === undefined) {
|
||||
return { status: 'Starting...' };
|
||||
return { status: i18next.t('progress.starting') };
|
||||
} else if (type === 'decompressing') {
|
||||
if (percentage == null) {
|
||||
return { status: 'Decompressing...' };
|
||||
return { status: i18next.t('progress.decompressing') };
|
||||
} else {
|
||||
return { position: `${percentage}%`, status: 'Decompressing...' };
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.decompressing'),
|
||||
};
|
||||
}
|
||||
} else if (type === 'flashing') {
|
||||
if (percentage != null) {
|
||||
if (percentage < 100) {
|
||||
return { position: `${percentage}%`, status: 'Flashing...' };
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.flashing'),
|
||||
};
|
||||
} else {
|
||||
return { status: 'Finishing...' };
|
||||
return { status: i18next.t('progress.finishing') };
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: 'Flashing...',
|
||||
status: i18next.t('progress.flashing'),
|
||||
position: `${position ? prettyBytes(position) : ''}`,
|
||||
};
|
||||
}
|
||||
} else if (type === 'verifying') {
|
||||
if (percentage == null) {
|
||||
return { status: 'Validating...' };
|
||||
return { status: i18next.t('progress.verifying') };
|
||||
} else if (percentage < 100) {
|
||||
return { position: `${percentage}%`, status: 'Validating...' };
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.verifying'),
|
||||
};
|
||||
} else {
|
||||
return { status: 'Finishing...' };
|
||||
return { status: i18next.t('progress.finishing') };
|
||||
}
|
||||
}
|
||||
return { status: 'Failed' };
|
||||
return { status: i18next.t('progress.failing') };
|
||||
}
|
||||
|
||||
export function titleFromFlashState(
|
||||
|
@@ -20,6 +20,7 @@ import * as _ from 'lodash';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as settings from '../../../gui/app/models/settings';
|
||||
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
async function mountSourceDrive() {
|
||||
// sourceDrivePath is the name of the link in /dev/disk/by-path
|
||||
@@ -27,7 +28,7 @@ async function mountSourceDrive() {
|
||||
if (sourceDrivePath) {
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -53,11 +54,11 @@ export async function selectImage(): Promise<string | undefined> {
|
||||
properties: ['openFile', 'treatPackageAsDirectory'],
|
||||
filters: [
|
||||
{
|
||||
name: 'OS Images',
|
||||
name: i18next.t('source.osImages'),
|
||||
extensions: SUPPORTED_EXTENSIONS,
|
||||
},
|
||||
{
|
||||
name: 'All',
|
||||
name: i18next.t('source.allFiles'),
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
@@ -79,8 +80,8 @@ export async function showWarning(options: {
|
||||
description: string;
|
||||
}): Promise<boolean> {
|
||||
_.defaults(options, {
|
||||
confirmationLabel: 'OK',
|
||||
rejectionLabel: 'Cancel',
|
||||
confirmationLabel: i18next.t('ok'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
});
|
||||
|
||||
const BUTTONS = [options.confirmationLabel, options.rejectionLabel];
|
||||
@@ -98,7 +99,7 @@ export async function showWarning(options: {
|
||||
buttons: BUTTONS,
|
||||
defaultId: BUTTON_REJECTION_INDEX,
|
||||
cancelId: BUTTON_REJECTION_INDEX,
|
||||
title: 'Attention',
|
||||
title: i18next.t('attention'),
|
||||
message: options.title,
|
||||
detail: options.description,
|
||||
},
|
||||
|
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
||||
import { readFile } from 'fs';
|
||||
import { chain, trim } from 'lodash';
|
||||
import { platform } from 'os';
|
||||
@@ -22,8 +23,6 @@ import { join } from 'path';
|
||||
import { env } from 'process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { withTmpFile } from '../../../shared/tmp';
|
||||
|
||||
const readFileAsync = promisify(readFile);
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -41,11 +40,11 @@ async function getWmicNetworkDrivesOutput(): Promise<string> {
|
||||
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
|
||||
const options = {
|
||||
// Close the file once it's created
|
||||
discardDescriptor: true,
|
||||
keepOpen: false,
|
||||
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
|
||||
prefix: 'tmp',
|
||||
};
|
||||
return withTmpFile(options, async (path) => {
|
||||
return withTmpFile(options, async ({ path }) => {
|
||||
const command = [
|
||||
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
|
||||
'path',
|
||||
|
@@ -37,6 +37,7 @@ import {
|
||||
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const COMPLETED_PERCENTAGE = 100;
|
||||
const SPEED_PRECISION = 2;
|
||||
@@ -117,7 +118,7 @@ async function flashImageToDrive(
|
||||
}
|
||||
goToSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
notifyFailure(iconPath, basename, drives);
|
||||
let errorMessage = getErrorMessageFromCode(error.code);
|
||||
if (!errorMessage) {
|
||||
@@ -293,9 +294,17 @@ export class FlashStep extends React.PureComponent<
|
||||
color="#7e8085"
|
||||
width="100%"
|
||||
>
|
||||
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
|
||||
<Txt>
|
||||
{i18next.t('flash.speedShort', {
|
||||
speed: this.props.speed.toFixed(SPEED_PRECISION),
|
||||
})}
|
||||
</Txt>
|
||||
{!_.isNil(this.props.eta) && (
|
||||
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
|
||||
<Txt>
|
||||
{i18next.t('flash.eta', {
|
||||
eta: formatSeconds(this.props.eta),
|
||||
})}
|
||||
</Txt>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
@@ -276,9 +276,9 @@ export class MainPage extends React.Component<
|
||||
style={{
|
||||
// Allow window to be dragged from header
|
||||
// @ts-ignore
|
||||
'-webkit-app-region': 'drag',
|
||||
WebkitAppRegion: 'drag',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<Flex width="100%" />
|
||||
@@ -304,7 +304,7 @@ export class MainPage extends React.Component<
|
||||
onClick={() => this.setState({ hideSettings: false })}
|
||||
style={{
|
||||
// Make touch events click instead of dragging
|
||||
'-webkit-app-region': 'no-drag',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
}}
|
||||
/>
|
||||
{!settings.getSync('disableExternalLinks') && (
|
||||
@@ -319,7 +319,7 @@ export class MainPage extends React.Component<
|
||||
tabIndex={6}
|
||||
style={{
|
||||
// Make touch events click instead of dragging
|
||||
'-webkit-app-region': 'no-drag',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
15
lib/gui/app/renderer.ts
Normal file
15
lib/gui/app/renderer.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// @ts-nocheck
|
||||
import { main } from './app';
|
||||
import './i18n';
|
||||
import { langParser } from './i18n';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
ipcRenderer.send('change-lng', langParser());
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./app', () => {
|
||||
main();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
@@ -142,7 +142,7 @@ export const Modal = styled(({ style, children, ...props }) => {
|
||||
{...props}
|
||||
>
|
||||
<ScrollableFlex flexDirection="column" width="100%" height="90%">
|
||||
{...children}
|
||||
{children.length ? children.map((c: any) => <>{c}</>) : children}
|
||||
</ScrollableFlex>
|
||||
</ModalBase>
|
||||
);
|
||||
|
@@ -21,18 +21,22 @@ import { platform } from 'os';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import './app/i18n';
|
||||
|
||||
import { packageType, version } from '../../package.json';
|
||||
import * as EXIT_CODES from '../shared/exit-codes';
|
||||
import { delay, getConfig } from '../shared/utils';
|
||||
import * as settings from './app/models/settings';
|
||||
import { logException } from './app/modules/analytics';
|
||||
import { buildWindowMenu } from './menu';
|
||||
import * as i18n from 'i18next';
|
||||
|
||||
const customProtocol = 'etcher';
|
||||
const scheme = `${customProtocol}://`;
|
||||
const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
|
||||
const packageUpdatable = updatablePackageTypes.includes(packageType);
|
||||
let packageUpdated = false;
|
||||
let mainWindow: any = null;
|
||||
|
||||
async function checkForUpdates(interval: number) {
|
||||
// We use a while loop instead of a setInterval to preserve
|
||||
@@ -43,7 +47,7 @@ async function checkForUpdates(interval: number) {
|
||||
const release = await autoUpdater.checkForUpdates();
|
||||
const isOutdated =
|
||||
semver.compare(release.updateInfo.version, version) > 0;
|
||||
const shouldUpdate = release.updateInfo.stagingPercentage || 0 > 0;
|
||||
const shouldUpdate = release.updateInfo.stagingPercentage !== 0; // undefinded (default) means 100%
|
||||
if (shouldUpdate && isOutdated) {
|
||||
await autoUpdater.downloadUpdate();
|
||||
packageUpdated = true;
|
||||
@@ -97,6 +101,7 @@ const sourceSelectorReady = new Promise((resolve) => {
|
||||
async function selectImageURL(url?: string) {
|
||||
// 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests
|
||||
if (url !== undefined && url !== 'data:,') {
|
||||
url = url.replace(/\/$/, ''); // on windows the url ends with an extra slash
|
||||
url = url.startsWith(scheme) ? url.slice(scheme.length) : url;
|
||||
await sourceSelectorReady;
|
||||
electron.BrowserWindow.getAllWindows().forEach((window) => {
|
||||
@@ -129,7 +134,7 @@ async function createMainWindow() {
|
||||
if (fullscreen) {
|
||||
({ width, height } = electron.screen.getPrimaryDisplay().bounds);
|
||||
}
|
||||
const mainWindow = new electron.BrowserWindow({
|
||||
mainWindow = new electron.BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
frame: !fullscreen,
|
||||
@@ -147,6 +152,7 @@ async function createMainWindow() {
|
||||
webPreferences: {
|
||||
backgroundThrottling: false,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
webviewTag: true,
|
||||
zoomFactor: width / defaultWidth,
|
||||
enableRemoteModule: true,
|
||||
@@ -155,7 +161,6 @@ async function createMainWindow() {
|
||||
|
||||
electron.app.setAsDefaultProtocolClient(customProtocol);
|
||||
|
||||
buildWindowMenu(mainWindow);
|
||||
mainWindow.setFullScreen(true);
|
||||
|
||||
// Prevent flash of white when starting the application
|
||||
@@ -238,6 +243,17 @@ async function main(): Promise<void> {
|
||||
await selectImageURL(await getCommandLineURL(argv));
|
||||
});
|
||||
await selectImageURL(await getCommandLineURL(process.argv));
|
||||
|
||||
electron.ipcMain.on('change-lng', function (event, args) {
|
||||
i18n.changeLanguage(args, () => {
|
||||
console.log('Language changed to: ' + args);
|
||||
});
|
||||
if (mainWindow != null) {
|
||||
buildWindowMenu(mainWindow);
|
||||
} else {
|
||||
console.log('Build menu failed. ');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,8 @@
|
||||
import * as electron from 'electron';
|
||||
import { displayName } from '../../package.json';
|
||||
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
/**
|
||||
* @summary Builds a native application menu for a given window
|
||||
*/
|
||||
@@ -42,12 +44,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
const menuTemplate: electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
role: 'editMenu',
|
||||
label: i18next.t('menu.edit'),
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
label: i18next.t('menu.view'),
|
||||
submenu: [
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
label: i18next.t('menu.devTool'),
|
||||
accelerator:
|
||||
process.platform === 'darwin' ? 'Command+Alt+I' : 'Control+Shift+I',
|
||||
click: toggleDevTools,
|
||||
@@ -56,12 +59,14 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
},
|
||||
{
|
||||
role: 'windowMenu',
|
||||
label: i18next.t('menu.window'),
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
label: i18next.t('menu.help'),
|
||||
submenu: [
|
||||
{
|
||||
label: 'Etcher Pro',
|
||||
label: i18next.t('menu.pro'),
|
||||
click() {
|
||||
electron.shell.openExternal(
|
||||
'https://etcher.io/pro?utm_source=etcher_menu&ref=etcher_menu',
|
||||
@@ -69,13 +74,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Etcher Website',
|
||||
label: i18next.t('menu.website'),
|
||||
click() {
|
||||
electron.shell.openExternal('https://etcher.io?ref=etcher_menu');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Report an issue',
|
||||
label: i18next.t('menu.issue'),
|
||||
click() {
|
||||
electron.shell.openExternal(
|
||||
'https://github.com/balena-io/etcher/issues',
|
||||
@@ -92,25 +97,29 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
submenu: [
|
||||
{
|
||||
role: 'about' as const,
|
||||
label: 'About Etcher',
|
||||
label: i18next.t('menu.about'),
|
||||
},
|
||||
{
|
||||
type: 'separator' as const,
|
||||
},
|
||||
{
|
||||
role: 'hide' as const,
|
||||
label: i18next.t('menu.hide'),
|
||||
},
|
||||
{
|
||||
role: 'hideOthers' as const,
|
||||
label: i18next.t('menu.hideOthers'),
|
||||
},
|
||||
{
|
||||
role: 'unhide' as const,
|
||||
label: i18next.t('menu.unhide'),
|
||||
},
|
||||
{
|
||||
type: 'separator' as const,
|
||||
},
|
||||
{
|
||||
role: 'quit' as const,
|
||||
label: i18next.t('menu.quit'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@@ -27,6 +27,7 @@ import {
|
||||
OnProgressFunction,
|
||||
OnFailFunction,
|
||||
decompressThenFlash,
|
||||
DECOMPRESSED_IMAGE_PREFIX,
|
||||
} from 'etcher-sdk/build/multi-write';
|
||||
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
||||
import * as ipc from 'node-ipc';
|
||||
@@ -34,8 +35,10 @@ import { totalmem } from 'os';
|
||||
|
||||
import { toJSON } from '../../shared/errors';
|
||||
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
|
||||
import { delay } from '../../shared/utils';
|
||||
import { delay, isJson } from '../../shared/utils';
|
||||
import { SourceMetadata } from '../app/components/source-selector/source-selector';
|
||||
import axios from 'axios';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID as string;
|
||||
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
||||
@@ -68,7 +71,7 @@ function log(message: string) {
|
||||
*/
|
||||
async function terminate(exitCode: number) {
|
||||
ipc.disconnect(IPC_SERVER_ID);
|
||||
await cleanupTmpFiles(Date.now());
|
||||
await cleanupTmpFiles(Date.now(), DECOMPRESSED_IMAGE_PREFIX);
|
||||
process.nextTick(() => {
|
||||
process.exit(exitCode || SUCCESS);
|
||||
});
|
||||
@@ -167,10 +170,10 @@ async function writeAndValidate({
|
||||
interface WriteOptions {
|
||||
image: SourceMetadata;
|
||||
destinations: DrivelistDrive[];
|
||||
unmountOnSuccess: boolean;
|
||||
autoBlockmapping: boolean;
|
||||
decompressFirst: boolean;
|
||||
SourceType: string;
|
||||
httpRequest?: any;
|
||||
}
|
||||
|
||||
ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
@@ -257,13 +260,12 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
const imagePath = options.image.path;
|
||||
log(`Image: ${imagePath}`);
|
||||
log(`Devices: ${destinations.join(', ')}`);
|
||||
log(`Umount on success: ${options.unmountOnSuccess}`);
|
||||
log(`Auto blockmapping: ${options.autoBlockmapping}`);
|
||||
log(`Decompress first: ${options.decompressFirst}`);
|
||||
const dests = options.destinations.map((destination) => {
|
||||
return new BlockDevice({
|
||||
drive: destination,
|
||||
unmountOnSuccess: options.unmountOnSuccess,
|
||||
unmountOnSuccess: true,
|
||||
write: true,
|
||||
direct: true,
|
||||
});
|
||||
@@ -282,7 +284,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
path: imagePath,
|
||||
});
|
||||
} else {
|
||||
source = new Http({ url: imagePath, avoidRandomAccess: true });
|
||||
const decodedImagePath = decodeURIComponent(imagePath);
|
||||
if (isJson(decodedImagePath)) {
|
||||
const imagePathObject = JSON.parse(decodedImagePath);
|
||||
source = new Http({
|
||||
url: imagePathObject.url,
|
||||
avoidRandomAccess: true,
|
||||
axiosInstance: axios.create(_.omit(imagePathObject, ['url'])),
|
||||
auth: options.image.auth,
|
||||
});
|
||||
} else {
|
||||
source = new Http({
|
||||
url: imagePath,
|
||||
avoidRandomAccess: true,
|
||||
auth: options.image.auth,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const results = await writeAndValidate({
|
||||
@@ -301,7 +318,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
ipc.of[IPC_SERVER_ID].emit('done', { results });
|
||||
await delay(DISCONNECT_DELAY);
|
||||
await terminate(exitCode);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
exitCode = GENERAL_ERROR;
|
||||
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
|
||||
}
|
||||
|
21
lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
Executable file
21
lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env osascript -l JavaScript
|
||||
|
||||
ObjC.import('stdlib')
|
||||
|
||||
const app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
const result = app.displayDialog('balenaEtcher 需要来自管理员的权限才能烧录镜像到磁盘。\n\n输入您的密码以允许此操作。', {
|
||||
defaultAnswer: '',
|
||||
withIcon: 'caution',
|
||||
buttons: ['取消', '好'],
|
||||
defaultButton: '好',
|
||||
hiddenAnswer: true,
|
||||
})
|
||||
|
||||
if (result.buttonReturned === '好') {
|
||||
result.textReturned
|
||||
} else {
|
||||
$.exit(255)
|
||||
}
|
||||
|
@@ -15,11 +15,12 @@
|
||||
*/
|
||||
|
||||
import { execFile } from 'child_process';
|
||||
import { app, remote } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { env } from 'process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { getAppPath } from '../utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||
@@ -29,6 +30,9 @@ export async function sudo(
|
||||
command: string,
|
||||
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
|
||||
try {
|
||||
let lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
lang = lang.substr(0, 2);
|
||||
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
'sudo',
|
||||
['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`],
|
||||
@@ -37,9 +41,9 @@ export async function sudo(
|
||||
env: {
|
||||
PATH: env.PATH,
|
||||
SUDO_ASKPASS: join(
|
||||
(app || remote.app).getAppPath(),
|
||||
getAppPath(),
|
||||
__dirname,
|
||||
'sudo-askpass.osascript.js',
|
||||
'sudo-askpass.osascript-' + lang + '.js',
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -49,7 +53,7 @@ export async function sudo(
|
||||
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
|
||||
stderr,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code === 1) {
|
||||
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
|
||||
return { cancelled: true };
|
||||
|
@@ -104,24 +104,19 @@ export function isDriveLargeEnough(
|
||||
return driveSize >= (image.size || UNKNOWN_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is disabled (i.e. not ready for selection)
|
||||
*/
|
||||
export function isDriveDisabled(drive: DrivelistDrive): boolean {
|
||||
return drive.disabled || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is valid, i.e. large enough for an image
|
||||
*/
|
||||
export function isDriveValid(
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
write: boolean = true,
|
||||
): boolean {
|
||||
return (
|
||||
isDriveLargeEnough(drive, image) &&
|
||||
!isSourceDrive(drive, image as SourceMetadata) &&
|
||||
!isDriveDisabled(drive)
|
||||
!write ||
|
||||
(!drive.disabled &&
|
||||
isDriveLargeEnough(drive, image) &&
|
||||
!isSourceDrive(drive, image as SourceMetadata))
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -17,16 +17,16 @@
|
||||
import { Dictionary } from 'lodash';
|
||||
import { outdent } from 'outdent';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import '../gui/app/i18n';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export const progress: Dictionary<(quantity: number) => string> = {
|
||||
successful: (quantity: number) => {
|
||||
const plural = quantity === 1 ? '' : 's';
|
||||
return `Successful target${plural}`;
|
||||
return i18next.t('message.flashSucceed', { count: quantity });
|
||||
},
|
||||
|
||||
failed: (quantity: number) => {
|
||||
const plural = quantity === 1 ? '' : 's';
|
||||
return `Failed target${plural}`;
|
||||
return i18next.t('message.flashFail', { count: quantity });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -38,132 +38,121 @@ export const info = {
|
||||
) => {
|
||||
const targets = [];
|
||||
if (failed + successful === 1) {
|
||||
targets.push(`to ${drive.description} (${drive.displayName})`);
|
||||
targets.push(
|
||||
i18next.t('message.toDrive', {
|
||||
description: drive.description,
|
||||
name: drive.displayName,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
if (successful) {
|
||||
const plural = successful === 1 ? '' : 's';
|
||||
targets.push(`to ${successful} target${plural}`);
|
||||
targets.push(
|
||||
i18next.t('message.toTarget', {
|
||||
count: successful,
|
||||
num: successful,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (failed) {
|
||||
const plural = failed === 1 ? '' : 's';
|
||||
targets.push(`and failed to be flashed to ${failed} target${plural}`);
|
||||
targets.push(
|
||||
i18next.t('message.andFailTarget', { count: failed, num: failed }),
|
||||
);
|
||||
}
|
||||
}
|
||||
return `${imageBasename} was successfully flashed ${targets.join(' ')}`;
|
||||
return i18next.t('message.succeedTo', {
|
||||
name: imageBasename,
|
||||
target: targets.join(' '),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const compatibility = {
|
||||
sizeNotRecommended: () => {
|
||||
return 'Not recommended';
|
||||
return i18next.t('message.sizeNotRecommended');
|
||||
},
|
||||
|
||||
tooSmall: () => {
|
||||
return 'Too small';
|
||||
return i18next.t('message.tooSmall');
|
||||
},
|
||||
|
||||
locked: () => {
|
||||
return 'Locked';
|
||||
return i18next.t('message.locked');
|
||||
},
|
||||
|
||||
system: () => {
|
||||
return 'System drive';
|
||||
return i18next.t('message.system');
|
||||
},
|
||||
|
||||
containsImage: () => {
|
||||
return 'Source drive';
|
||||
return i18next.t('message.containsImage');
|
||||
},
|
||||
|
||||
// The drive is large and therefore likely not a medium you want to write to.
|
||||
largeDrive: () => {
|
||||
return 'Large drive';
|
||||
return i18next.t('message.largeDrive');
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const warning = {
|
||||
unrecommendedDriveSize: (
|
||||
image: { recommendedDriveSize: number },
|
||||
drive: { device: string; size: number },
|
||||
) => {
|
||||
tooSmall: (source: { size: number }, target: { size: number }) => {
|
||||
return outdent({ newline: ' ' })`
|
||||
This image recommends a ${prettyBytes(image.recommendedDriveSize)}
|
||||
drive, however ${drive.device} is only ${prettyBytes(drive.size)}.
|
||||
${i18next.t('message.sourceLarger', {
|
||||
byte: prettyBytes(source.size - target.size),
|
||||
})}
|
||||
`;
|
||||
},
|
||||
|
||||
exitWhileFlashing: () => {
|
||||
return [
|
||||
'You are currently flashing a drive.',
|
||||
'Closing Etcher may leave your drive in an unusable state.',
|
||||
].join(' ');
|
||||
return i18next.t('message.exitWhileFlashing');
|
||||
},
|
||||
|
||||
looksLikeWindowsImage: () => {
|
||||
return [
|
||||
'It looks like you are trying to burn a Windows image.\n\n',
|
||||
'Unlike other images, Windows images require special processing to be made bootable.',
|
||||
'We suggest you use a tool specially designed for this purpose, such as',
|
||||
'<a href="https://rufus.akeo.ie">Rufus</a> (Windows),',
|
||||
'<a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux),',
|
||||
'or Boot Camp Assistant (macOS).',
|
||||
].join(' ');
|
||||
return i18next.t('message.looksLikeWindowsImage');
|
||||
},
|
||||
|
||||
missingPartitionTable: () => {
|
||||
return [
|
||||
'It looks like this is not a bootable image.\n\n',
|
||||
'The image does not appear to contain a partition table,',
|
||||
'and might not be recognized or bootable by your device.',
|
||||
].join(' ');
|
||||
return i18next.t('message.missingPartitionTable', {
|
||||
type: i18next.t('message.image'),
|
||||
});
|
||||
},
|
||||
|
||||
driveMissingPartitionTable: () => {
|
||||
return outdent({ newline: ' ' })`
|
||||
It looks like this is not a bootable drive.
|
||||
The drive does not appear to contain a partition table,
|
||||
and might not be recognized or bootable by your device.
|
||||
`;
|
||||
return i18next.t('message.missingPartitionTable', {
|
||||
type: i18next.t('message.drive'),
|
||||
});
|
||||
},
|
||||
|
||||
largeDriveSize: () => {
|
||||
return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.';
|
||||
return i18next.t('message.largeDriveSize');
|
||||
},
|
||||
|
||||
systemDrive: () => {
|
||||
return 'Selecting your system drive is dangerous and will erase your drive!';
|
||||
return i18next.t('message.systemDrive');
|
||||
},
|
||||
|
||||
sourceDrive: () => {
|
||||
return 'Contains the image you chose to flash';
|
||||
return i18next.t('message.sourceDrive');
|
||||
},
|
||||
};
|
||||
|
||||
export const error = {
|
||||
notEnoughSpaceInDrive: () => {
|
||||
return [
|
||||
'Not enough space on the drive.',
|
||||
'Please insert larger one and try again.',
|
||||
].join(' ');
|
||||
return i18next.t('message.noSpace');
|
||||
},
|
||||
|
||||
genericFlashError: (err: Error) => {
|
||||
return `Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n${err.message}`;
|
||||
return i18next.t('message.genericFlashError', { error: err.message });
|
||||
},
|
||||
|
||||
validation: () => {
|
||||
return [
|
||||
'The write has been completed successfully but Etcher detected potential',
|
||||
'corruption issues when reading the image back from the drive.',
|
||||
'\n\nPlease consider writing the image to a different drive.',
|
||||
].join(' ');
|
||||
return i18next.t('message.validation');
|
||||
},
|
||||
|
||||
openSource: (sourceName: string, errorMessage: string) => {
|
||||
return outdent`
|
||||
Something went wrong while opening ${sourceName}
|
||||
|
||||
Error: ${errorMessage}
|
||||
`;
|
||||
return i18next.t('message.openError', {
|
||||
source: sourceName,
|
||||
error: errorMessage,
|
||||
});
|
||||
},
|
||||
|
||||
flashFailure: (
|
||||
@@ -172,35 +161,33 @@ export const error = {
|
||||
) => {
|
||||
const target =
|
||||
drives.length === 1
|
||||
? `${drives[0].description} (${drives[0].displayName})`
|
||||
: `${drives.length} targets`;
|
||||
return `Something went wrong while writing ${imageBasename} to ${target}.`;
|
||||
? i18next.t('message.toDrive', {
|
||||
description: drives[0].description,
|
||||
name: drives[0].displayName,
|
||||
})
|
||||
: i18next.t('message.toTarget', {
|
||||
count: drives.length,
|
||||
num: drives.length,
|
||||
});
|
||||
return i18next.t('message.flashError', {
|
||||
image: imageBasename,
|
||||
targets: target,
|
||||
});
|
||||
},
|
||||
|
||||
driveUnplugged: () => {
|
||||
return [
|
||||
'Looks like Etcher lost access to the drive.',
|
||||
'Did it get unplugged accidentally?',
|
||||
"\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
|
||||
].join(' ');
|
||||
return i18next.t('message.unplug');
|
||||
},
|
||||
|
||||
inputOutput: () => {
|
||||
return [
|
||||
'Looks like Etcher is not able to write to this location of the drive.',
|
||||
'This error is usually caused by a faulty drive, reader, or port.',
|
||||
'\n\nPlease try again with another drive, reader, or port.',
|
||||
].join(' ');
|
||||
return i18next.t('message.cannotWrite');
|
||||
},
|
||||
|
||||
childWriterDied: () => {
|
||||
return [
|
||||
'The writer process ended unexpectedly.',
|
||||
'Please try again, and contact the Etcher team if the problem persists.',
|
||||
].join(' ');
|
||||
return i18next.t('message.childWriterDied');
|
||||
},
|
||||
|
||||
unsupportedProtocol: () => {
|
||||
return 'Only http:// and https:// URLs are supported.';
|
||||
return i18next.t('message.badProtocol');
|
||||
},
|
||||
};
|
||||
|
@@ -15,30 +15,32 @@
|
||||
*/
|
||||
|
||||
import * as childProcess from 'child_process';
|
||||
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as semver from 'semver';
|
||||
import * as sudoPrompt from 'sudo-prompt';
|
||||
import * as sudoPrompt from '@balena/sudo-prompt';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
|
||||
import * as errors from './errors';
|
||||
import { withTmpFile } from './tmp';
|
||||
|
||||
const execAsync = promisify(childProcess.exec);
|
||||
const execFileAsync = promisify(childProcess.execFile);
|
||||
|
||||
type Std = string | Buffer | undefined;
|
||||
|
||||
function sudoExecAsync(
|
||||
cmd: string,
|
||||
options: { name: string },
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
): Promise<{ stdout: Std; stderr: Std }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
sudoPrompt.exec(
|
||||
cmd,
|
||||
options,
|
||||
(error: Error | null, stdout: string, stderr: string) => {
|
||||
if (error != null) {
|
||||
(error: Error | undefined, stdout: Std, stderr: Std) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
@@ -60,7 +62,7 @@ export async function isElevated(): Promise<boolean> {
|
||||
// See http://stackoverflow.com/a/28268802
|
||||
try {
|
||||
await execAsync('fltmc');
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code === os.constants.errno.EPERM) {
|
||||
return false;
|
||||
}
|
||||
@@ -146,7 +148,7 @@ async function elevateScriptCatalina(
|
||||
try {
|
||||
const { cancelled } = await catalinaSudo(cmd);
|
||||
return { cancelled };
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
throw errors.createError({ title: error.stderr });
|
||||
}
|
||||
}
|
||||
@@ -172,10 +174,11 @@ export async function elevateCommand(
|
||||
);
|
||||
return await withTmpFile(
|
||||
{
|
||||
keepOpen: false,
|
||||
prefix: 'balena-etcher-electron-',
|
||||
postfix: '.cmd',
|
||||
},
|
||||
async (path) => {
|
||||
async ({ path }) => {
|
||||
await fs.writeFile(path, launchScript);
|
||||
if (isWindows) {
|
||||
return elevateScriptWindows(path, options.applicationName);
|
||||
@@ -189,7 +192,7 @@ export async function elevateCommand(
|
||||
}
|
||||
try {
|
||||
return await elevateScriptUnix(path, options.applicationName);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// We're hardcoding internal error messages declared by `sudo-prompt`.
|
||||
// There doesn't seem to be a better way to handle these errors, so
|
||||
// for now, we should make sure we double check if the error messages
|
||||
|
@@ -1,27 +0,0 @@
|
||||
import * as tmp from 'tmp';
|
||||
|
||||
function tmpFileAsync(
|
||||
options: tmp.FileOptions,
|
||||
): Promise<{ path: string; cleanup: () => void }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
tmp.file(options, (error, path, _fd, cleanup) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ path, cleanup });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function withTmpFile<T>(
|
||||
options: tmp.FileOptions,
|
||||
fn: (path: string) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const { path, cleanup } = await tmpFileAsync(options);
|
||||
try {
|
||||
return await fn(path);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { app, remote } from 'electron';
|
||||
import { Dictionary } from 'lodash';
|
||||
|
||||
import * as errors from './errors';
|
||||
@@ -47,3 +48,25 @@ export async function delay(duration: number): Promise<void> {
|
||||
setTimeout(resolve, duration);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppPath(): string {
|
||||
return (
|
||||
(app || 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 : '',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isJson(jsonString: string) {
|
||||
try {
|
||||
JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
22705
npm-shrinkwrap.json → package-lock.json
generated
22705
npm-shrinkwrap.json → package-lock.json
generated
File diff suppressed because it is too large
Load Diff
175
package.json
175
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "balena-etcher",
|
||||
"private": true,
|
||||
"displayName": "balenaEtcher",
|
||||
"version": "1.5.112",
|
||||
"version": "1.13.3",
|
||||
"packageType": "local",
|
||||
"main": "generated/etcher.js",
|
||||
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
|
||||
@@ -13,22 +13,26 @@
|
||||
"url": "git@github.com:balena-io/etcher.git"
|
||||
},
|
||||
"scripts": {
|
||||
"lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
|
||||
"build": "npm run webpack",
|
||||
"flowzone-preinstall-linux": "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",
|
||||
"flowzone-preinstall-macos": "true",
|
||||
"flowzone-preinstall-windows": "true",
|
||||
"flowzone-preinstall": "npm run flowzone-preinstall-linux",
|
||||
"lint-css": "prettier --write lib/**/*.css",
|
||||
"lint-spell": "codespell --dictionary - --dictionary dictionary.txt --skip *.ttf *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension lib tests docs Makefile *.md LICENSE",
|
||||
"lint": "npm run lint-ts && npm run lint-css && npm run lint-spell",
|
||||
"test-spectron": "mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts tests/spectron/runner.spec.ts",
|
||||
"test-gui": "electron-mocha --recursive --reporter spec --require ts-node/register --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 --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
|
||||
"test": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && npm run sanity-checks",
|
||||
"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 .",
|
||||
"postshrinkwrap": "ts-node ./scripts/clean-shrinkwrap.ts",
|
||||
"webpack": "webpack",
|
||||
"watch": "webpack --watch",
|
||||
"concourse-build-electron": "npm run webpack",
|
||||
"concourse-test": "npx npm@6.14.5 test",
|
||||
"concourse-test-electron": "npx npm@6.14.5 test"
|
||||
"test-macos": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && npm run sanity-checks",
|
||||
"test-gui": "electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
|
||||
"test-linux": "npm run lint && xvfb-run --auto-servernum npm run test-gui && xvfb-run --auto-servernum npm run test-shared && xvfb-run --auto-servernum npm run test-spectron && npm run sanity-checks",
|
||||
"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-spectron": "mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts tests/spectron/runner.spec.ts",
|
||||
"test-windows": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && 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": {
|
||||
"hooks": {
|
||||
@@ -45,72 +49,83 @@
|
||||
},
|
||||
"author": "Balena Inc. <hello@etcher.io>",
|
||||
"license": "Apache-2.0",
|
||||
"platformSpecificDependencies": [
|
||||
"fsevents",
|
||||
"winusb-driver-generator"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@balena/lint": "^5.0.4",
|
||||
"@fortawesome/fontawesome-free": "^5.13.1",
|
||||
"@svgr/webpack": "^5.4.0",
|
||||
"@types/chai": "^4.2.7",
|
||||
"@types/copy-webpack-plugin": "^6.0.0",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/mocha": "^8.0.3",
|
||||
"@types/node": "^12.12.39",
|
||||
"@types/node-ipc": "^9.1.2",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/semver": "^7.1.0",
|
||||
"@types/sinon": "^9.0.0",
|
||||
"@types/terser-webpack-plugin": "^4.1.0",
|
||||
"@types/tmp": "^0.2.0",
|
||||
"@types/webpack-node-externals": "^2.5.0",
|
||||
"chai": "^4.2.0",
|
||||
"copy-webpack-plugin": "^6.0.1",
|
||||
"css-loader": "^4.2.1",
|
||||
"d3": "^4.13.0",
|
||||
"debug": "^4.2.0",
|
||||
"electron": "9.3.3",
|
||||
"electron-builder": "^22.9.1",
|
||||
"electron-mocha": "^9.3.2",
|
||||
"electron-notarize": "^1.0.0",
|
||||
"electron-rebuild": "^2.3.2",
|
||||
"electron-updater": "^4.3.5",
|
||||
"etcher-sdk": "^5.1.10",
|
||||
"file-loader": "^6.0.0",
|
||||
"husky": "^4.2.5",
|
||||
"immutable": "^3.8.1",
|
||||
"lint-staged": "^10.2.2",
|
||||
"lodash": "^4.17.10",
|
||||
"mini-css-extract-plugin": "^0.10.0",
|
||||
"mocha": "^8.0.1",
|
||||
"native-addon-loader": "^2.0.1",
|
||||
"node-ipc": "^9.1.1",
|
||||
"omit-deep-lodash": "1.1.4",
|
||||
"outdent": "^0.7.1",
|
||||
"path-is-inside": "^1.0.2",
|
||||
"pretty-bytes": "^5.3.0",
|
||||
"react": "^16.8.5",
|
||||
"react-dom": "^16.8.5",
|
||||
"redux": "^4.0.5",
|
||||
"rendition": "^18.8.3",
|
||||
"resin-corvus": "^2.0.5",
|
||||
"semver": "^7.3.2",
|
||||
"simple-progress-webpack-plugin": "^1.1.2",
|
||||
"sinon": "^9.0.2",
|
||||
"spectron": "^11.0.0",
|
||||
"string-replace-loader": "^2.3.0",
|
||||
"styled-components": "^5.1.0",
|
||||
"sudo-prompt": "github:zvin/sudo-prompt#7cdede2f0da28fbcc2db48402d7d935f3a825c91",
|
||||
"sys-class-rgb-led": "^2.1.1",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^8.0.0",
|
||||
"ts-node": "^9.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^4.1.2",
|
||||
"uuid": "^8.1.0",
|
||||
"webpack": "^4.40.2",
|
||||
"webpack-cli": "^3.3.9"
|
||||
"@balena/lint": "5.4.2",
|
||||
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@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": "8.2.3",
|
||||
"@types/node": "14.18.34",
|
||||
"@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",
|
||||
"aws4-axios": "2.4.9",
|
||||
"chai": "4.3.7",
|
||||
"copy-webpack-plugin": "7.0.0",
|
||||
"css-loader": "5.2.7",
|
||||
"d3": "4.13.0",
|
||||
"debug": "4.3.4",
|
||||
"electron": "^13.5.0",
|
||||
"electron-builder": "^23.0.9",
|
||||
"electron-mocha": "9.3.3",
|
||||
"electron-notarize": "1.2.2",
|
||||
"electron-rebuild": "3.2.9",
|
||||
"electron-updater": "5.3.0",
|
||||
"esbuild-loader": "2.20.0",
|
||||
"etcher-sdk": "^7.4.7",
|
||||
"file-loader": "6.2.0",
|
||||
"husky": "4.3.8",
|
||||
"i18next": "21.10.0",
|
||||
"immutable": "3.8.2",
|
||||
"lint-staged": "10.5.4",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"mocha": "8.4.0",
|
||||
"native-addon-loader": "2.0.1",
|
||||
"node-ipc": "9.2.1",
|
||||
"omit-deep-lodash": "1.1.7",
|
||||
"outdent": "0.8.0",
|
||||
"path-is-inside": "1.0.2",
|
||||
"pnp-webpack-plugin": "1.7.0",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5",
|
||||
"react-i18next": "11.18.6",
|
||||
"redux": "4.2.0",
|
||||
"rendition": "19.3.2",
|
||||
"resin-corvus": "2.0.5",
|
||||
"semver": "7.3.8",
|
||||
"simple-progress-webpack-plugin": "1.1.2",
|
||||
"sinon": "9.2.4",
|
||||
"spectron": "15.0.0",
|
||||
"string-replace-loader": "3.1.0",
|
||||
"style-loader": "2.0.0",
|
||||
"styled-components": "5.3.6",
|
||||
"sys-class-rgb-led": "3.0.1",
|
||||
"terser-webpack-plugin": "5.3.6",
|
||||
"ts-loader": "8.4.0",
|
||||
"ts-node": "9.1.1",
|
||||
"tslib": "2.4.1",
|
||||
"typescript": "4.4.4",
|
||||
"url-loader": "4.1.1",
|
||||
"uuid": "8.3.2",
|
||||
"webpack": "5.75.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-dev-server": "4.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14 < 16"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2023-01-11T14:30:46.007Z"
|
||||
}
|
||||
}
|
||||
|
13
repo.yml
13
repo.yml
@@ -1,17 +1,18 @@
|
||||
---
|
||||
type: electron
|
||||
release: github
|
||||
publishMetadata: true
|
||||
sentry:
|
||||
org: balenaetcher
|
||||
team: resinio
|
||||
type: electron
|
||||
org: balenaetcher
|
||||
team: resinio
|
||||
type: electron
|
||||
triggerNotification:
|
||||
version: 1.5.81
|
||||
stagingPercentage: 100
|
||||
version: 1.7.9
|
||||
stagingPercentage: 100
|
||||
upstream:
|
||||
- repo: etcher-sdk
|
||||
url: https://github.com/balena-io-modules/etcher-sdk
|
||||
module: 'etcher-sdk'
|
||||
module: etcher-sdk
|
||||
- repo: sys-class-rgb-led
|
||||
url: https://github.com/balena-io-modules/sys-class-rgb-led
|
||||
module: sys-class-rgb-led
|
||||
|
@@ -1,3 +1,2 @@
|
||||
codespell==1.12.0
|
||||
awscli==1.11.87
|
||||
shyaml==0.5.0
|
||||
awscli==1.27.28
|
||||
shyaml==0.6.2
|
||||
|
@@ -29,11 +29,15 @@ 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) {
|
||||
} catch (error: any) {
|
||||
console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
Submodule scripts/resin updated: 02c8c7ca1f...8dfa21cfc2
BIN
secrets/APPLE_SIGNING.p12.secret
Normal file
BIN
secrets/APPLE_SIGNING.p12.secret
Normal file
Binary file not shown.
BIN
secrets/APPLE_SIGNING_PASSWORD.txt.secret
Normal file
BIN
secrets/APPLE_SIGNING_PASSWORD.txt.secret
Normal file
Binary file not shown.
BIN
secrets/WINDOWS_SIGNING.pfx.secret
Normal file
BIN
secrets/WINDOWS_SIGNING.pfx.secret
Normal file
Binary file not shown.
BIN
secrets/WINDOWS_SIGNING_PASSWORD.txt.secret
Normal file
BIN
secrets/WINDOWS_SIGNING_PASSWORD.txt.secret
Normal file
Binary file not shown.
BIN
secrets/XCODE_APP_LOADER_PASSWORD.txt.secret
Normal file
BIN
secrets/XCODE_APP_LOADER_PASSWORD.txt.secret
Normal file
Binary file not shown.
@@ -573,7 +573,8 @@ describe('Model: flashState', function () {
|
||||
});
|
||||
|
||||
describe('.getFlashUuid()', function () {
|
||||
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
const UUID_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
|
||||
it('should be initially undefined', function () {
|
||||
expect(flashState.getFlashUuid()).to.be.undefined;
|
||||
|
@@ -23,7 +23,7 @@ import * as settings from '../../../lib/gui/app/models/settings';
|
||||
async function checkError(promise: Promise<any>, fn: (err: Error) => any) {
|
||||
try {
|
||||
await promise;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
await fn(error);
|
||||
return;
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ describe('Browser: imageWriter', () => {
|
||||
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||
]);
|
||||
assert.fail('Writing twice should fail');
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
expect(error.message).to.equal(
|
||||
'There is already a flash in progress',
|
||||
);
|
||||
@@ -133,7 +133,7 @@ describe('Browser: imageWriter', () => {
|
||||
});
|
||||
try {
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('write error');
|
||||
}
|
||||
|
@@ -16,7 +16,6 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import * as settings from '../../../lib/gui/app/models/settings';
|
||||
import * as progressStatus from '../../../lib/gui/app/modules/progress-status';
|
||||
|
||||
describe('Browser: progressStatus', function () {
|
||||
@@ -30,8 +29,6 @@ describe('Browser: progressStatus', function () {
|
||||
eta: 15,
|
||||
speed: 100000000000000,
|
||||
};
|
||||
|
||||
settings.set('unmountOnSuccess', true);
|
||||
});
|
||||
|
||||
it('should report 0% if percentage == 0 but speed != 0', function () {
|
||||
@@ -40,22 +37,14 @@ describe('Browser: progressStatus', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 0, flashing, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 0, flashing', function () {
|
||||
this.state.speed = 0;
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'0% Flashing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 0, flashing, !unmountOnSuccess', function () {
|
||||
this.state.speed = 0;
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'0% Flashing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 0, verifying, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 0, verifying', function () {
|
||||
this.state.speed = 0;
|
||||
this.state.type = 'verifying';
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
@@ -63,31 +52,14 @@ describe('Browser: progressStatus', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 0, verifying, !unmountOnSuccess', function () {
|
||||
this.state.speed = 0;
|
||||
this.state.type = 'verifying';
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'0% Validating...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 50, flashing, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 50, flashing', function () {
|
||||
this.state.percentage = 50;
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'50% Flashing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 50, flashing, !unmountOnSuccess', function () {
|
||||
this.state.percentage = 50;
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'50% Flashing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 50, verifying, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 50, verifying', function () {
|
||||
this.state.percentage = 50;
|
||||
this.state.type = 'verifying';
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
@@ -95,31 +67,14 @@ describe('Browser: progressStatus', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 50, verifying, !unmountOnSuccess', function () {
|
||||
this.state.percentage = 50;
|
||||
this.state.type = 'verifying';
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'50% Validating...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 100, flashing, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 100, flashing', function () {
|
||||
this.state.percentage = 100;
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'Finishing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 100, flashing, !unmountOnSuccess', function () {
|
||||
this.state.percentage = 100;
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'Finishing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 100, verifying, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 100, verifying', function () {
|
||||
this.state.percentage = 100;
|
||||
this.state.type = 'verifying';
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
@@ -127,9 +82,8 @@ describe('Browser: progressStatus', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 100, validatinf, !unmountOnSuccess', function () {
|
||||
it('should handle percentage == 100, validating', function () {
|
||||
this.state.percentage = 100;
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'Finishing...',
|
||||
);
|
||||
|
@@ -514,40 +514,6 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isDriveDisabled()', function () {
|
||||
it('should return true if the drive is disabled', function () {
|
||||
const result = constraints.isDriveDisabled(({
|
||||
device: '/dev/disk1',
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
disabled: true,
|
||||
} as unknown) as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if the drive is not disabled', function () {
|
||||
const result = constraints.isDriveDisabled(({
|
||||
device: '/dev/disk1',
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
disabled: false,
|
||||
} as unknown) as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if "disabled" is undefined', function () {
|
||||
const result = constraints.isDriveDisabled({
|
||||
device: '/dev/disk1',
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isDriveSizeRecommended()', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
@@ -1192,7 +1158,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
'/dev/disk6',
|
||||
];
|
||||
const drives = [
|
||||
({
|
||||
{
|
||||
device: drivePaths[0],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
@@ -1200,8 +1166,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [{ path: __dirname }],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
} as unknown as constraints.DrivelistDrive,
|
||||
{
|
||||
device: drivePaths[1],
|
||||
description: 'My Other Drive',
|
||||
size: 123456789,
|
||||
@@ -1209,8 +1175,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: true,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
} as unknown as constraints.DrivelistDrive,
|
||||
{
|
||||
device: drivePaths[2],
|
||||
description: 'My Drive',
|
||||
size: 1234567,
|
||||
@@ -1218,8 +1184,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
} as unknown as constraints.DrivelistDrive,
|
||||
{
|
||||
device: drivePaths[3],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
@@ -1227,8 +1193,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: true,
|
||||
isReadOnly: false,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
} as unknown as constraints.DrivelistDrive,
|
||||
{
|
||||
device: drivePaths[4],
|
||||
description: 'My Drive',
|
||||
size: 128000000001,
|
||||
@@ -1236,8 +1202,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
} as unknown as constraints.DrivelistDrive,
|
||||
{
|
||||
device: drivePaths[5],
|
||||
description: 'My Drive',
|
||||
size: 12345678,
|
||||
@@ -1245,8 +1211,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
} as unknown as constraints.DrivelistDrive,
|
||||
{
|
||||
device: drivePaths[6],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
@@ -1254,7 +1220,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
} as unknown as constraints.DrivelistDrive,
|
||||
];
|
||||
|
||||
const image: SourceMetadata = {
|
||||
|
@@ -30,9 +30,8 @@ describe('Shared: SupportedFormats', function () {
|
||||
],
|
||||
(imagePath) => {
|
||||
it(`should return true if filename is ${imagePath}`, function () {
|
||||
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(
|
||||
imagePath,
|
||||
);
|
||||
const looksLikeWindowsImage =
|
||||
supportedFormats.looksLikeWindowsImage(imagePath);
|
||||
expect(looksLikeWindowsImage).to.be.true;
|
||||
});
|
||||
},
|
||||
@@ -45,9 +44,8 @@ describe('Shared: SupportedFormats', function () {
|
||||
],
|
||||
(imagePath) => {
|
||||
it(`should return false if filename is ${imagePath}`, function () {
|
||||
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(
|
||||
imagePath,
|
||||
);
|
||||
const looksLikeWindowsImage =
|
||||
supportedFormats.looksLikeWindowsImage(imagePath);
|
||||
expect(looksLikeWindowsImage).to.be.false;
|
||||
});
|
||||
},
|
||||
|
@@ -15,46 +15,52 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { platform } from 'os';
|
||||
import { Application } from 'spectron';
|
||||
import * as electronPath from 'electron';
|
||||
|
||||
describe('Spectron', function () {
|
||||
// Mainly for CI jobs
|
||||
this.timeout(40000);
|
||||
// TODO: spectron fails to start on the CI with:
|
||||
// Error: Failed to create session.
|
||||
// unknown error: Chrome failed to start: exited abnormally
|
||||
if (platform() !== 'darwin') {
|
||||
describe('Spectron', function () {
|
||||
// Mainly for CI jobs
|
||||
this.timeout(40000);
|
||||
|
||||
const app = new Application({
|
||||
path: (electronPath as unknown) as string,
|
||||
args: ['--no-sandbox', '.'],
|
||||
});
|
||||
|
||||
before('app:start', async () => {
|
||||
await app.start();
|
||||
});
|
||||
|
||||
after('app:stop', async () => {
|
||||
if (app && app.isRunning()) {
|
||||
await app.stop();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Browser Window', () => {
|
||||
it('should open a browser window', async () => {
|
||||
// We can't use `isVisible()` here as it won't work inside
|
||||
// a Windows Docker container, but we can approximate it
|
||||
// with these set of checks:
|
||||
const bounds = await app.browserWindow.getBounds();
|
||||
expect(bounds.height).to.be.above(0);
|
||||
expect(bounds.width).to.be.above(0);
|
||||
expect(await app.browserWindow.isMinimized()).to.be.false;
|
||||
expect(
|
||||
(await app.browserWindow.isVisible()) ||
|
||||
(await app.browserWindow.isFocused()),
|
||||
).to.be.true;
|
||||
const app = new Application({
|
||||
path: electronPath as unknown as string,
|
||||
args: ['--no-sandbox', '.'],
|
||||
});
|
||||
|
||||
it('should set a proper title', async () => {
|
||||
// @ts-ignore (SpectronClient.getTitle exists)
|
||||
return expect(await app.client.getTitle()).to.equal('Etcher');
|
||||
before('app:start', async () => {
|
||||
await app.start();
|
||||
});
|
||||
|
||||
after('app:stop', async () => {
|
||||
if (app && app.isRunning()) {
|
||||
await app.stop();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Browser Window', () => {
|
||||
it('should open a browser window', async () => {
|
||||
// We can't use `isVisible()` here as it won't work inside
|
||||
// a Windows Docker container, but we can approximate it
|
||||
// with these set of checks:
|
||||
const bounds = await app.browserWindow.getBounds();
|
||||
expect(bounds.height).to.be.above(0);
|
||||
expect(bounds.width).to.be.above(0);
|
||||
expect(await app.browserWindow.isMinimized()).to.be.false;
|
||||
expect(
|
||||
(await app.browserWindow.isVisible()) ||
|
||||
(await app.browserWindow.isFocused()),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should set a proper title', async () => {
|
||||
// @ts-ignore (SpectronClient.getTitle exists)
|
||||
return expect(await app.client.getTitle()).to.equal('balenaEtcher');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -1,12 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "es2019",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "esnext"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react",
|
||||
"pretty": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2019",
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,16 @@
|
||||
"jsx": "react",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||
"importHelpers": true,
|
||||
"allowSyntheticDefaultImports": 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",
|
||||
|
1
typings/pnp-webpack-plugin/index.d.ts
vendored
Normal file
1
typings/pnp-webpack-plugin/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'pnp-webpack-plugin';
|
2
typings/sudo-prompt/index.d.ts
vendored
2
typings/sudo-prompt/index.d.ts
vendored
@@ -1 +1 @@
|
||||
declare module 'sudo-prompt';
|
||||
declare module '@balena/sudo-prompt';
|
||||
|
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
|
||||
import * as CopyPlugin from 'copy-webpack-plugin';
|
||||
import { readdirSync } from 'fs';
|
||||
import { readdirSync, existsSync } from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import * as os from 'os';
|
||||
import outdent from 'outdent';
|
||||
import * as path from 'path';
|
||||
@@ -25,6 +24,9 @@ import { env } from 'process';
|
||||
import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin';
|
||||
import * as TerserPlugin from 'terser-webpack-plugin';
|
||||
import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
|
||||
import * as PnpWebpackPlugin from 'pnp-webpack-plugin';
|
||||
|
||||
import * as tsconfigRaw from './tsconfig.webpack.json';
|
||||
|
||||
/**
|
||||
* Don't webpack package.json as mixpanel & sentry tokens
|
||||
@@ -32,8 +34,7 @@ import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
|
||||
*/
|
||||
function externalPackageJson(packageJsonPath: string) {
|
||||
return (
|
||||
_context: string,
|
||||
request: string,
|
||||
{ request }: { context: string; request: string },
|
||||
callback: (error?: Error | null, result?: string) => void,
|
||||
) => {
|
||||
if (_.endsWith(request, 'package.json')) {
|
||||
@@ -50,8 +51,7 @@ function platformSpecificModule(
|
||||
) {
|
||||
// Resolves module on platform, otherwise resolves the replacement
|
||||
return (
|
||||
_context: string,
|
||||
request: string,
|
||||
{ request }: { context: string; request: string },
|
||||
callback: (error?: Error, result?: string, type?: string) => void,
|
||||
) => {
|
||||
if (request === module && os.platform() !== platform) {
|
||||
@@ -70,16 +70,87 @@ function renameNodeModules(resourcePath: string) {
|
||||
path
|
||||
.relative(__dirname, resourcePath)
|
||||
.replace('node_modules', 'modules')
|
||||
// use the same name on all architectures so electron-builder can build a universal dmg on mac
|
||||
.replace(LZMA_BINDINGS_FOLDER, LZMA_BINDINGS_FOLDER_RENAMED)
|
||||
// file-loader expects posix paths, even on Windows
|
||||
.replace(/\\/g, '/')
|
||||
);
|
||||
}
|
||||
|
||||
function findExt2fsFolder(): string {
|
||||
const ext2fs = 'node_modules/ext2fs';
|
||||
const biFsExt2fs = 'node_modules/balena-image-fs/node_modules/ext2fs';
|
||||
|
||||
if (existsSync(ext2fs)) {
|
||||
return ext2fs;
|
||||
} else if (existsSync(biFsExt2fs)) {
|
||||
return biFsExt2fs;
|
||||
} else {
|
||||
throw Error('ext2fs not found');
|
||||
}
|
||||
}
|
||||
|
||||
function makeExt2FsRegex(): RegExp {
|
||||
const folder = findExt2fsFolder();
|
||||
const libpath = '/lib/libext2fs\\.js$';
|
||||
|
||||
return new RegExp(folder.concat(libpath));
|
||||
}
|
||||
|
||||
function findUsbPrebuild(): string[] {
|
||||
const usbPrebuildsFolder = path.join('node_modules', 'usb', 'prebuilds');
|
||||
const prebuildFolders = readdirSync(usbPrebuildsFolder);
|
||||
let bindingFile: string | undefined = 'node.napi.node';
|
||||
const platformFolder = prebuildFolders.find(
|
||||
(f) => f.startsWith(os.platform()) && f.indexOf(os.arch()) > -1,
|
||||
);
|
||||
if (platformFolder === undefined) {
|
||||
throw new Error(
|
||||
'Could not find usb prebuild. Should try fallback to node-gyp and use /build/Release instead of /prebuilds',
|
||||
);
|
||||
}
|
||||
|
||||
const bindingFiles = readdirSync(
|
||||
path.join(usbPrebuildsFolder, platformFolder),
|
||||
);
|
||||
|
||||
if (!bindingFiles.length) {
|
||||
throw new Error('Could not find usb prebuild for platform');
|
||||
}
|
||||
|
||||
if (bindingFiles.length === 1) {
|
||||
bindingFile = bindingFiles[0];
|
||||
}
|
||||
|
||||
// armv6 vs v7 in linux-arm and
|
||||
// glibc vs musl in linux-x64
|
||||
if (bindingFiles.length > 1) {
|
||||
bindingFile = bindingFiles.find((file) => {
|
||||
if (bindingFiles.indexOf('arm') > -1) {
|
||||
const process = require('process');
|
||||
return file.indexOf(process.config.variables.arm_version) > -1;
|
||||
} else {
|
||||
return file.indexOf('glibc') > -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (bindingFile === undefined) {
|
||||
throw new Error('Could not find usb prebuild for platform');
|
||||
}
|
||||
|
||||
return [platformFolder, bindingFile];
|
||||
}
|
||||
|
||||
const [USB_BINDINGS_FOLDER, USB_BINDINGS_FILE] = findUsbPrebuild();
|
||||
|
||||
function findLzmaNativeBindingsFolder(): string {
|
||||
const files = readdirSync(path.join('node_modules', 'lzma-native'));
|
||||
const files = readdirSync(
|
||||
path.join('node_modules', 'lzma-native', 'prebuilds'),
|
||||
);
|
||||
const bindingsFolder = files.find(
|
||||
(f) =>
|
||||
f.startsWith('binding-') &&
|
||||
f.startsWith(os.platform()) &&
|
||||
f.endsWith(env.npm_config_target_arch || os.arch()),
|
||||
);
|
||||
if (bindingsFolder === undefined) {
|
||||
@@ -89,6 +160,7 @@ function findLzmaNativeBindingsFolder(): string {
|
||||
}
|
||||
|
||||
const LZMA_BINDINGS_FOLDER = findLzmaNativeBindingsFolder();
|
||||
const LZMA_BINDINGS_FOLDER_RENAMED = 'binding';
|
||||
|
||||
interface ReplacementRule {
|
||||
search: string;
|
||||
@@ -119,7 +191,15 @@ function fetchWasm(...where: string[]) {
|
||||
} catch {
|
||||
}
|
||||
function appPath() {
|
||||
return Path.isAbsolute(__dirname) ? __dirname : Path.join(electron.remote.app.getAppPath(), 'generated');
|
||||
return Path.isAbsolute(__dirname) ?
|
||||
__dirname :
|
||||
Path.join(
|
||||
// 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.
|
||||
electron.remote.app.getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
|
||||
'generated'
|
||||
);
|
||||
}
|
||||
scriptDirectory = Path.join(appPath(), 'modules', ${whereStr}, '/');
|
||||
`;
|
||||
@@ -128,16 +208,17 @@ function fetchWasm(...where: string[]) {
|
||||
const commonConfig = {
|
||||
mode: 'production',
|
||||
optimization: {
|
||||
moduleIds: 'natural',
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
compress: false,
|
||||
mangle: false,
|
||||
output: {
|
||||
beautify: true,
|
||||
format: {
|
||||
comments: false,
|
||||
ecma: 2018,
|
||||
ecma: 2020,
|
||||
},
|
||||
},
|
||||
extractComments: false,
|
||||
@@ -148,7 +229,12 @@ const commonConfig = {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: 'css-loader',
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||
loader: 'file-loader',
|
||||
options: { name: renameNodeModules },
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
@@ -158,9 +244,11 @@ const commonConfig = {
|
||||
test: /\.tsx?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
loader: 'esbuild-loader',
|
||||
options: {
|
||||
configFile: 'tsconfig.webpack.json',
|
||||
loader: 'tsx',
|
||||
target: 'es2021',
|
||||
tsconfigRaw,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -191,13 +279,8 @@ const commonConfig = {
|
||||
/node_modules\/lzma-native\/index\.js$/,
|
||||
// remove node-pre-gyp magic from lzma-native
|
||||
{
|
||||
search: 'require(binding_path)',
|
||||
replace: () => {
|
||||
return `require('./${path.posix.join(
|
||||
LZMA_BINDINGS_FOLDER,
|
||||
'lzma_native.node',
|
||||
)}')`;
|
||||
},
|
||||
search: `require('node-gyp-build')(__dirname);`,
|
||||
replace: `require('./prebuilds/${LZMA_BINDINGS_FOLDER}/electron.napi.node')`,
|
||||
},
|
||||
// use regular stream module instead of readable-stream
|
||||
{
|
||||
@@ -206,9 +289,9 @@ const commonConfig = {
|
||||
},
|
||||
),
|
||||
// remove node-pre-gyp magic from usb
|
||||
replace(/node_modules\/@balena.io\/usb\/usb\.js$/, {
|
||||
search: 'require(binding_path)',
|
||||
replace: "require('./build/Release/usb_bindings.node')",
|
||||
replace(/node_modules\/usb\/dist\/usb\/bindings\.js$/, {
|
||||
search: `require('node-gyp-build')(path_1.join(__dirname, '..', '..'));`,
|
||||
replace: `require('../../prebuilds/${USB_BINDINGS_FOLDER}/${USB_BINDINGS_FILE}')`,
|
||||
}),
|
||||
// remove bindings magic from mountutils
|
||||
replace(/node_modules\/mountutils\/index\.js$/, {
|
||||
@@ -241,14 +324,26 @@ const commonConfig = {
|
||||
"return await readFile(Path.join(__dirname, '..', 'blobs', filename));",
|
||||
replace: outdent`
|
||||
const { app, remote } = require('electron');
|
||||
return await readFile(Path.join((app || remote.app).getAppPath(), 'generated', __dirname.replace('node_modules', 'modules'), '..', 'blobs', filename));
|
||||
return await readFile(
|
||||
Path.join(
|
||||
// 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.
|
||||
(app || remote.app).getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
|
||||
'generated',
|
||||
__dirname.replace('node_modules', 'modules'),
|
||||
'..',
|
||||
'blobs',
|
||||
filename
|
||||
)
|
||||
);
|
||||
`,
|
||||
}),
|
||||
// Use the libext2fs.wasm file in the generated folder
|
||||
// The way to find the app directory depends on whether we run in the renderer or in the child-writer
|
||||
// We use __dirname in the child-writer and electron.remote.app.getAppPath() in the renderer
|
||||
replace(/node_modules\/ext2fs\/lib\/libext2fs\.js$/, {
|
||||
search: 'scriptDirectory=__dirname+"/"',
|
||||
replace(makeExt2FsRegex(), {
|
||||
search: 'scriptDirectory = __dirname + "/";',
|
||||
replace: fetchWasm('ext2fs', 'lib'),
|
||||
}),
|
||||
// Same for node-crc-utils
|
||||
@@ -272,16 +367,20 @@ const commonConfig = {
|
||||
extensions: ['.node', '.js', '.json', '.ts', '.tsx'],
|
||||
},
|
||||
plugins: [
|
||||
PnpWebpackPlugin,
|
||||
new SimpleProgressWebpackPlugin({
|
||||
format: process.env.WEBPACK_PROGRESS || 'verbose',
|
||||
}),
|
||||
// Force axios to use http.js, not xhr.js as we need stream support
|
||||
// (it's package.json file replaces http with xhr for browser targets).
|
||||
// (its package.json file replaces http with xhr for browser targets).
|
||||
new NormalModuleReplacementPlugin(
|
||||
slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/),
|
||||
'./http.js',
|
||||
),
|
||||
],
|
||||
resolveLoader: {
|
||||
plugins: [PnpWebpackPlugin.moduleLoader(module)],
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'generated'),
|
||||
filename: '[name].js',
|
||||
@@ -306,7 +405,7 @@ const guiConfigCopyPatterns = [
|
||||
to: 'modules/node-raspberrypi-usbboot/blobs',
|
||||
},
|
||||
{
|
||||
from: 'node_modules/ext2fs/lib/libext2fs.wasm',
|
||||
from: `${findExt2fsFolder()}/lib/libext2fs.wasm`,
|
||||
to: 'modules/ext2fs/lib/libext2fs.wasm',
|
||||
},
|
||||
{
|
||||
@@ -318,8 +417,8 @@ const guiConfigCopyPatterns = [
|
||||
if (os.platform() === 'win32') {
|
||||
// liblzma.dll is required on Windows for lzma-native
|
||||
guiConfigCopyPatterns.push({
|
||||
from: `node_modules/lzma-native/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
|
||||
to: `modules/lzma-native/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
|
||||
from: `node_modules/lzma-native/prebuilds/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
|
||||
to: `modules/lzma-native/prebuilds/${LZMA_BINDINGS_FOLDER_RENAMED}/liblzma.dll`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -331,10 +430,19 @@ const guiConfig = {
|
||||
__filename: true,
|
||||
},
|
||||
entry: {
|
||||
gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.ts'),
|
||||
gui: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
|
||||
},
|
||||
// entry: 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
|
||||
new BannerPlugin({
|
||||
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
|
||||
@@ -373,41 +481,4 @@ const childWriterConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const cssConfig = {
|
||||
mode: 'production',
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: { name: renameNodeModules },
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({ filename: '[name].css' }),
|
||||
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' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
entry: {
|
||||
index: path.join(__dirname, 'lib', 'gui', 'app', 'css', 'main.css'),
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'generated'),
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = [cssConfig, guiConfig, etcherConfig, childWriterConfig];
|
||||
export default [guiConfig, etcherConfig, childWriterConfig];
|
||||
|
24
webpack.dev.config.ts
Normal file
24
webpack.dev.config.ts
Normal 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];
|
Reference in New Issue
Block a user