Compare commits

..

291 Commits

Author SHA1 Message Date
balenaCI
7de99003ca v1.10.0 2022-11-10 20:54:14 +00:00
bulldozer-balena[bot]
e09bdd734b Merge pull request #3871 from balena-io/test
testing renovate
2022-11-10 20:52:37 +00:00
builder555
306e087ec6 testing renovate
Change-Type: minor
2022-11-10 15:12:39 -05:00
balenaCI
c6b0178a87 v1.9.0 2022-11-08 21:37:26 +00:00
bulldozer-balena[bot]
4e581ea1ce Merge pull request #3861 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.5
2022-11-08 21:36:02 +00:00
Renovate Bot
26dc2d19e5 Update dependency awscli to 1.27.5
Update awscli to 1.27.5

Update awscli from 1.11.87 to 1.27.5

Change-type: minor
2022-11-08 20:50:05 +00:00
balenaCI
b99282acfb v1.8.17 2022-11-08 20:38:29 +00:00
bulldozer-balena[bot]
4e48724d0c Merge pull request #3860 from balena-io/renovate/react-dom-16.x
Update dependency @types/react-dom to 16.9.17
2022-11-08 20:36:50 +00:00
Renovate Bot
448ce141d5 Update dependency @types/react-dom to 16.9.17
Update @types/react-dom to 16.9.17

Update @types/react-dom from 16.8.4 to 16.9.17

Change-type: patch
2022-11-08 19:50:12 +00:00
balenaCI
695f287190 v1.8.16 2022-11-08 19:39:44 +00:00
bulldozer-balena[bot]
4de3271e15 Merge pull request #3858 from balena-io/renovate/react-16.x
Update dependency @types/react to 16.14.34
2022-11-08 19:38:16 +00:00
Renovate Bot
77b33b127d Update dependency @types/react to 16.14.34
Update @types/react to 16.14.34

Update @types/react from 16.8.5 to 16.14.34

Change-type: patch
2022-11-08 18:37:18 +00:00
balenaCI
9cd13ba381 v1.8.15 2022-11-08 18:21:06 +00:00
bulldozer-balena[bot]
9df23c8a3f Merge pull request #3859 from balena-io/ab77/operational
CI: generalise artefact handling
2022-11-08 18:19:29 +00:00
ab77
e3618b939e CI: generalise artefact handling
* on PR syncs, delete draft releases on Linux runners only
* delete draft releases when unmerged PRs are closed

Change-type: patch
2022-11-08 09:41:03 -08:00
balenaCI
6a39f5869a v1.8.14 2022-11-08 13:57:20 +00:00
bulldozer-balena[bot]
fd472efadc Merge pull request #3857 from balena-io/renovate/node-14.x
Update dependency @types/node to 14.18.33
2022-11-08 13:56:11 +00:00
Renovate Bot
7e2c2eae63 Update dependency @types/node to 14.18.33
Update @types/node to 14.18.33

Update @types/node from 14.14.41 to 14.18.33

Change-type: patch
2022-11-08 13:01:36 +00:00
balenaCI
5266571ca4 v1.8.13 2022-11-08 12:39:29 +00:00
bulldozer-balena[bot]
797868c474 Merge pull request #3856 from balena-io/renovate/copy-webpack-plugin-6.x
Update dependency @types/copy-webpack-plugin to 6.4.3
2022-11-08 12:38:11 +00:00
Renovate Bot
2c2a5c7c2b Update dependency @types/copy-webpack-plugin to 6.4.3
Update @types/copy-webpack-plugin to 6.4.3

Update @types/copy-webpack-plugin from 6.0.0 to 6.4.3

Change-type: patch
2022-11-08 11:47:28 +00:00
balenaCI
9e536d5337 v1.8.12 2022-11-08 11:33:03 +00:00
bulldozer-balena[bot]
860e680dd9 Merge pull request #3855 from balena-io/renovate/font-awesome
Update dependency @fortawesome/fontawesome-free to 5.15.4
2022-11-08 11:31:46 +00:00
Renovate Bot
7bb52aa170 Update dependency @fortawesome/fontawesome-free to 5.15.4
Update @fortawesome/fontawesome-free to 5.15.4

Update @fortawesome/fontawesome-free from 5.13.1 to 5.15.4

Change-type: patch
2022-11-08 10:50:37 +00:00
balenaCI
1c370f9100 v1.8.11 2022-11-08 10:38:49 +00:00
bulldozer-balena[bot]
ec7c772d0b Merge pull request #3854 from balena-io/renovate/balena-lint-5.x
Update dependency @balena/lint to 5.4.2
2022-11-08 10:37:18 +00:00
Renovate Bot
cc0285a77d Update dependency @balena/lint to 5.4.2
Update @balena/lint to 5.4.2

Update @balena/lint from 5.3.0 to 5.4.2

Change-type: patch
2022-11-08 09:46:20 +00:00
balenaCI
256d3550d1 v1.8.10 2022-11-08 09:35:05 +00:00
bulldozer-balena[bot]
db3a5f3b0a Merge pull request #3852 from balena-io/renovate/sys-class-rgb-led-3.x
Update dependency sys-class-rgb-led to 3.0.1
2022-11-08 09:33:38 +00:00
Renovate Bot
0e58edf113 Update dependency sys-class-rgb-led to 3.0.1
Update sys-class-rgb-led to 3.0.1

Update sys-class-rgb-led from 3.0.0 to 3.0.1

Change-type: patch
2022-11-08 08:54:54 +00:00
balenaCI
db136926a9 v1.8.9 2022-11-08 08:40:23 +00:00
bulldozer-balena[bot]
d84e7211be Merge pull request #3851 from balena-io/renovate/semver-7.x
Update dependency semver to 7.3.8
2022-11-08 08:38:44 +00:00
Renovate Bot
8357cc19d2 Update dependency semver to 7.3.8
Update semver to 7.3.8

Update semver from 7.3.2 to 7.3.8

Change-type: patch
2022-11-08 07:45:37 +00:00
balenaCI
2752b9fa95 v1.8.8 2022-11-08 07:33:39 +00:00
bulldozer-balena[bot]
0214be4953 Merge pull request #3850 from balena-io/renovate/omit-deep-lodash-1.x
Update dependency omit-deep-lodash to 1.1.7
2022-11-08 07:32:29 +00:00
Renovate Bot
a4f944e795 Update dependency omit-deep-lodash to 1.1.7
Update omit-deep-lodash to 1.1.7

Update omit-deep-lodash from 1.1.4 to 1.1.7

Change-type: patch
2022-11-08 06:35:41 +00:00
balenaCI
cd2ebf15fc v1.8.7 2022-11-08 06:19:14 +00:00
bulldozer-balena[bot]
7a7ea374e9 Merge pull request #3849 from balena-io/renovate/immutable-3.x
Update dependency immutable to 3.8.2
2022-11-08 06:17:03 +00:00
Renovate Bot
330df325f9 Update dependency immutable to 3.8.2
Update immutable to 3.8.2

Update immutable from 3.8.1 to 3.8.2

Change-type: patch
2022-11-08 05:22:49 +00:00
balenaCI
2fc0882b2e v1.8.6 2022-11-08 05:18:30 +00:00
bulldozer-balena[bot]
4dd779e010 Merge pull request #3847 from balena-io/renovate/electron-rebuild-3.x
Update dependency electron-rebuild to 3.2.9
2022-11-08 05:17:09 +00:00
Renovate Bot
3dc54405fe Update dependency electron-rebuild to 3.2.9
Update electron-rebuild to 3.2.9

Update electron-rebuild from 3.2.5 to 3.2.9

Change-type: patch
2022-11-08 04:39:11 +00:00
balenaCI
3f1aa5bac3 v1.8.5 2022-11-08 04:22:24 +00:00
bulldozer-balena[bot]
8f52fdb900 Merge pull request #3846 from balena-io/renovate/electron-mocha-9.x
Update dependency electron-mocha to 9.3.3
2022-11-08 04:21:09 +00:00
Renovate Bot
1b93891ed8 Update dependency electron-mocha to 9.3.3
Update electron-mocha to 9.3.3

Update electron-mocha from 9.3.2 to 9.3.3

Change-type: patch
2022-11-08 03:23:58 +00:00
balenaCI
33adc8ecf8 v1.8.4 2022-11-08 02:41:38 +00:00
bulldozer-balena[bot]
0455f7ea58 Merge pull request #3845 from balena-io/renovate/webpack-node-externals-2.x
Update dependency @types/webpack-node-externals to 2.5.3
2022-11-08 02:39:47 +00:00
Renovate Bot
ea5a167f4f Update dependency @types/webpack-node-externals to 2.5.3
Update @types/webpack-node-externals to 2.5.3

Update @types/webpack-node-externals from 2.5.0 to 2.5.3

Change-type: patch
2022-11-08 01:45:44 +00:00
balenaCI
8a1c4a4cc8 v1.8.3 2022-11-08 01:36:35 +00:00
bulldozer-balena[bot]
bd8bc81713 Merge pull request #3844 from balena-io/renovate/tmp-0.x
Update dependency @types/tmp to 0.2.3
2022-11-08 01:35:19 +00:00
Renovate Bot
98a5ddf58a Update dependency @types/tmp to 0.2.3
Update @types/tmp to 0.2.3

Update @types/tmp from 0.2.0 to 0.2.3

Change-type: patch
2022-11-08 00:34:31 +00:00
balenaCI
6223dbc541 v1.8.2 2022-11-08 00:19:28 +00:00
bulldozer-balena[bot]
7c56621c57 Merge pull request #3843 from balena-io/ab77/operational
Generate release notes with git
2022-11-08 00:18:08 +00:00
ab77
a61aa8e2be Generate release notes with git
Change-type: patch
2022-11-07 15:39:26 -08:00
balenaCI
7df4f9615b v1.8.1 2022-11-07 23:35:28 +00:00
bulldozer-balena[bot]
5742452fdf Merge pull request #3842 from balena-io/renovate/mime-types-2.x
Update dependency @types/mime-types to 2.1.1
2022-11-07 23:34:08 +00:00
Renovate Bot
fe09f9f862 Update dependency @types/mime-types to 2.1.1
Update @types/mime-types to 2.1.1

Update @types/mime-types from 2.1.0 to 2.1.1

Change-type: patch
2022-11-07 22:36:54 +00:00
balenaCI
3a4687ea0f v1.8.0 2022-11-07 22:27:05 +00:00
bulldozer-balena[bot]
db6490fb1b Merge pull request #3840 from balena-io/renovate/scripts-resin-digest
Update scripts/resin digest to 652fdd4
2022-11-07 22:25:48 +00:00
Renovate Bot
1642297101 Update scripts/resin digest to 652fdd4
Update scripts/resin to

Update scripts/resin from  to

Change-type: minor
2022-11-07 21:46:35 +00:00
balenaCI
5ecd223cfc v1.7.15 2022-11-07 21:36:38 +00:00
bulldozer-balena[bot]
306e40fd7b Merge pull request #3838 from balena-io/ab77/operational
Build targets individually
2022-11-07 21:34:46 +00:00
ab77
b58249b9c8 Build targets individually
Change-type: patch
2022-11-07 12:57:51 -08:00
balenaCI
b23b4b34d0 v1.7.14 2022-11-07 20:17:55 +00:00
bulldozer-balena[bot]
73bc921713 Merge pull request #3833 from balena-io/renovate/npm-lodash-vulnerability
Update dependency lodash to 4.17.21 [SECURITY]
2022-11-07 20:16:20 +00:00
Renovate Bot
f356e4c303 Update dependency lodash to 4.17.21 [SECURITY]
Update lodash to 4.17.21

Update lodash from 4.17.10 to 4.17.21

Change-type: patch
2022-11-07 19:31:58 +00:00
balenaCI
9888167f2e v1.7.13 2022-11-07 19:17:08 +00:00
bulldozer-balena[bot]
4561690478 Merge pull request #3837 from balena-io/ab77/operational
Update release notes on finalize
2022-11-07 19:15:38 +00:00
ab77
576113febf Update release notes on finalize
Change-type: patch
2022-11-07 10:33:38 -08:00
balenaCI
cc139bf750 v1.7.12 2022-11-07 18:32:20 +00:00
bulldozer-balena[bot]
ae91958c06 Merge pull request #3836 from balena-io/ab77/operational
Avoid duplicate releases
2022-11-07 18:30:56 +00:00
ab77
33dea6267f Avoid duplicate releases
Change-type: patch
2022-11-07 09:47:19 -08:00
balenaCI
c9a8bca96f v1.7.11 2022-11-07 17:32:27 +00:00
bulldozer-balena[bot]
8af376e608 Merge pull request #3832 from balena-io/ab77/operational
Only run finalize on Linux runners
2022-11-07 17:30:49 +00:00
ab77
9ab307df4f Only run finalize on Linux runners
Change-type: patch
2022-11-07 08:39:52 -08:00
balenaCI
e8a716f8bb v1.7.10 2022-11-07 15:59:42 +00:00
bulldozer-balena[bot]
a40e64f6cd Merge pull request #3819 from balena-io/ab77/operational
Switch to Flowzone
2022-11-07 15:58:11 +00:00
ab77
2e53feb38c Switch to Flowzone
Change-type: patch
2022-11-07 07:11:18 -08:00
Balena CI
5945ab1f50 v1.7.9 2022-04-22 16:47:29 +03:00
bulldozer-balena[bot]
59d67220d4 Merge pull request #3747 from balena-io/next-release
patch: deb afterinstall and readme updates
2022-04-22 13:45:50 +00:00
mcraa
61610ded84 patch: update allowed extensions to include deb afterinstall in build 2022-04-22 15:08:04 +02:00
Peter Makra
c87a132f40 patch: add update notification 2022-04-22 14:44:36 +02:00
Andrew Scheller
350d4de32b patch: fix usb-device-boot link in README
Change-type: patch
2022-03-31 17:46:08 +02:00
Ken Bannister
f5f9025d6d Fix application directory for Debian postinst script
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2022-03-31 17:45:10 +02:00
Balena CI
549d744d04 v1.7.8 2022-03-18 19:22:12 +02:00
bulldozer-balena[bot]
6194460dc2 Merge pull request #3723 from balena-io/v1.7.8-draft
small ui updates
2022-03-18 17:20:11 +00:00
Peter Makra
8370f638b4 patch: complete suse uninstall readme
Change-Type: patch
2022-03-11 00:40:20 +01:00
Peter Makra
ac34c51125 patch: completed suse instructions
Change-Type: patch
2022-03-11 00:33:29 +01:00
Peter Makra
b241470fe1 patch: order rpm instrictions
Change-Type: patch
2022-03-10 17:00:43 +01:00
Peter Makra
179697040c Merge branch 'v1.7.8-draft' of github.com:balena-io/etcher into v1.7.8-draft 2022-03-10 16:37:01 +01:00
Peter Makra
335766ed12 patch: enabled update notification for version 1.7.8
Change-Type: patch
2022-03-10 16:36:24 +01:00
Peter Makra
4c5d052a71 patch: updated title to balenaEtcher
fixes #3592

Change-Type: patch
2022-03-10 16:35:03 +01:00
Peter Makra
86423342a8 patch: cleanup and organize readme
Change-Type: patch
2022-03-10 16:27:38 +01:00
Peter Makra
d8b41552e3 patch: extend cloudsmith attribution in readme
Change-Type: patch
2022-03-10 16:08:43 +01:00
Logicer
11c65fb392 Update macOS Icon to Big Sur Style
Change-type: patch
2022-03-10 15:55:27 +01:00
Balena CI
bed126506f v1.7.7 2022-02-22 11:25:43 +02:00
bulldozer-balena[bot]
f6aeb52b16 Merge pull request #3711 from balena-io/fix-auto-update
patch: Fix auto update
2022-02-22 09:23:16 +00:00
Peter Makra
a5201942b8 patch: clarified update check 2022-02-22 09:50:34 +01:00
Peter Makra
c1f7164273 patch: autoupdate stagingPercentage check, include default
Change-Type: patch
2022-02-21 21:09:49 +01:00
Balena CI
6774bf784c v1.7.6 2022-02-21 18:00:46 +02:00
bulldozer-balena[bot]
56ec8b4eac Merge pull request #3699 from balena-io/update-issue-template
patch: add requirements and help to issue template
2022-02-21 15:59:05 +00:00
Peter Makra
35868509af patch: version number notification 2022-02-21 15:21:58 +01:00
Peter Makra
3ab6749f49 Merge branch 'master' into update-issue-template 2022-02-21 14:33:16 +01:00
Balena CI
7a012a92bc v1.7.5 2022-02-21 15:19:52 +02:00
bulldozer-balena[bot]
aba01825a0 Merge pull request #3700 from flec/fix-basic-auth
patch: fix flashing from URL when using basic auth
2022-02-21 13:17:31 +00:00
Peter Makra
907a3308de updated branch with 1.7.4 2022-02-21 13:53:16 +01:00
Peter Makra
4366bb372f patch: fixed typos in template 2022-02-21 13:49:18 +01:00
Marco Füllemann
a6f6cd4a19 patch: fix flashing from URL when using basic auth 2022-02-21 13:36:40 +01:00
Balena CI
03ee428039 v1.7.4 2022-02-21 13:44:44 +02:00
bulldozer-balena[bot]
8d652d064d Merge pull request #3704 from balena-io/update-electron-12.2.3
patch: updated electron to 12.2.3
2022-02-21 11:42:46 +00:00
Peter Makra
28adc34239 patch: set version update notification 1.7.3 2022-02-17 14:15:46 +01:00
Peter Makra
120e9bf42f Merge branch 'update-electron-12.2.3' of github.com:balena-io/etcher into update-electron-12.2.3 2022-02-17 13:47:02 +01:00
Peter Makra
59f54e194b patch: updated electron to 12.2.3
Change-Type: patch
2022-02-17 13:46:35 +01:00
Peter Makra
c4834e61a7 patch: updated electron to 12.2.3
Change-Type: patch
2022-02-17 13:37:17 +01:00
mcraa
e4d02bc561 patch: add requirements and help to issue template 2022-02-08 08:19:55 +00:00
mcraa
b9e54e39f7 patch: add requirements and help to issue template 2022-02-08 09:19:55 +01:00
Balena CI
f3c32eac65 v1.7.3 2021-12-29 17:09:42 +02:00
bulldozer-balena[bot]
9a303ab344 Merge pull request #3667 from balena-io/fix-message-of.null
patch: fix mesage of null
2021-12-29 15:06:59 +00:00
Peter Makra
9c1b55bebc patch: fix mesage of null
Change-Type: patch
2021-12-29 15:23:21 +01:00
Balena CI
30ae4bbd86 v1.7.2 2021-12-21 20:38:54 +02:00
bulldozer-balena[bot]
c6126a980a Merge pull request #3661 from balena-io/fix-windows-open-from-web
patch: fixed open from browser on windows
2021-12-21 18:36:22 +00:00
Peter Makra
ef90d048ca patch: fixed open from browser on windows
Change-type: patch
Signed-off-by: Peter Makra <peter@balena.io>
2021-12-21 17:47:10 +01:00
Balena CI
b938132038 v1.7.1 2021-11-29 11:05:10 +02:00
bulldozer-balena[bot]
3cb2e78fe7 Merge pull request #3630 from balena-io/improve-webpack-build
patch: Improve webpack build time
2021-11-29 09:02:29 +00:00
Lorenzo Alberto Maria Ambrosi
ea9875ddf0 patch: Revert back to electron-rebuild
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-22 12:24:26 +01:00
Lorenzo Alberto Maria Ambrosi
65dacd2ff2 patch: Disallow TS in JS
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-16 20:08:11 +01:00
Lorenzo Alberto Maria Ambrosi
a190818827 patch: Remove esInterop TS flag
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 14:39:33 +01:00
Lorenzo Alberto Maria Ambrosi
98e33b619b patch: Use @balena/sudo-prompt
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 14:39:07 +01:00
Lorenzo Alberto Maria Ambrosi
685ed715ac patch: Update rpiboot guide link
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 11:47:11 +01:00
Lorenzo Alberto Maria Ambrosi
3cf3c4b398 patch: Improve webpack build time
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 10:45:00 +01:00
Balena CI
1c2ef4b1d4 v1.7.0 2021-11-09 18:09:26 +02:00
bulldozer-balena[bot]
d22fc91585 Merge pull request #3628 from balena-io/device-info-draft
Device info draft
2021-11-09 16:07:20 +00:00
Lorenzo Alberto Maria Ambrosi
0a28af5c35 patch: Add missing @types/react@16.8.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-09 14:10:18 +01:00
Lorenzo Alberto Maria Ambrosi
0c1e5b88ef patch: Use npm ci in Makefile
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-09 13:53:04 +01:00
Lorenzo Alberto Maria Ambrosi
790201be90 patch: Add draft info boxes for system information
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-27 10:41:56 +02:00
Lorenzo Alberto Maria Ambrosi
d8d379f05e patch: Remove electron-rebuild package
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:42:46 +02:00
Lorenzo Alberto Maria Ambrosi
b5e9701048 patch: Make electron a dev. dependency
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:40:37 +02:00
Lorenzo Alberto Maria Ambrosi
292f86d6f5 patch: Remove electron-rebuild package
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:40:15 +02:00
Lorenzo Alberto Maria Ambrosi
76ca9934c8 patch: Use exact modules versions
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 11:24:32 +02:00
Lorenzo Alberto Maria Ambrosi
37b826ee4e patch: Update etcher-sdk from v6.2.5 to v6.3.0
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:27:32 +02:00
JSReds
1e1bd3c508 Fix write step for Http file process
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-10-13 17:22:57 +02:00
Lorenzo Alberto Maria Ambrosi
00e8f11913 patch: Fix linting errors
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:21:48 +02:00
Lorenzo Alberto Maria Ambrosi
a3c24a26a0 minor: Refactor dependencies installation to avoid custom scripts
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:16:11 +02:00
Lorenzo Alberto Maria Ambrosi
4232928ad8 patch: Fix LEDs init error
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-30 13:05:47 +02:00
Balena CI
b165fb78da v1.6.0 2021-09-24 20:12:18 +03:00
bulldozer-balena[bot]
e9f6c5ead9 Merge pull request #3599 from balena-io/led-color-settings
Led color settings
2021-09-24 17:10:00 +00:00
Marco Füllemann
b2d0c1c9dd add support for basic auth when downloading images from URL
When selecting "Flash from URL" the user can optionally provide a username and password for basic authentication. The authentication input fields are collapsed by default. When the authentication input fields are collapsed after entering values the values are cleared to ensure that the user sees all parameter passed to the server.

Change-Type: minor
Changelog-Entry: Add support for basic auth when downloading images from URL.
2021-09-17 11:16:29 +02:00
Lorenzo Alberto Maria Ambrosi
14d91400a4 patch: Update etcher-sdk from v6.2.1 to v6.2.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-17 11:11:36 +02:00
David Gaspar
d0114aece7 Update Makefile to Apple M1 info
Expanding host architecture detection.

Change-type: patch
2021-09-07 16:56:10 +02:00
Lorenzo Alberto Maria Ambrosi
dff2df4aab Add LED settings for potentially different hardware
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-07 15:20:56 +02:00
Balena CI
13159f93ee v1.5.122 2021-09-02 17:48:21 +03:00
bulldozer-balena[bot]
3ece1fd841 Merge pull request #3590 from balena-io/various-fixes
Various fixes
2021-09-02 14:46:26 +00:00
Lorenzo Alberto Maria Ambrosi
f46963b6b3 Restore image file selection LED-drive pathing
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-02 14:16:18 +02:00
Lorenzo Alberto Maria Ambrosi
b97f4e0031 Update scripts submodule
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-31 16:40:48 +02:00
Lorenzo Alberto Maria Ambrosi
e2d233d74b Change LEDs colours
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-26 20:15:49 +02:00
Lorenzo Alberto Maria Ambrosi
a7ca2e527b Restore windows images warning
Change-type: patch
Changelog-entry: Windows images now show the proper warning again
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-26 20:15:48 +02:00
Mohamed Salah
396a053c0a Fix Update and install with DNF instructions
Change-type: patch
2021-08-26 20:15:48 +02:00
JSReds
d1a3f1cb88 Add possibile authorization as a query param
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-08-26 20:15:48 +02:00
Xtraim
9f96558cdd update the windows part
I choose to add this part because, after the clean the usb stick
could stay in a raw state without creating the new partions,
activating and formatting.
Thanks

Change-type: patch
2021-08-20 08:31:49 +02:00
thambu1710
b3bc589d70 Update SUPPORT.md
Change-type: patch
2021-08-20 08:31:45 +02:00
Seth Falco
18d2c28110 replace make webpack with npm run webpack
Change-type: patch
2021-08-20 08:31:38 +02:00
JSReds
b272ef296d Add loader on image select
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-08-20 08:31:27 +02:00
Zane Hitchcox
32ca28a3a9 add pnp-webpack-plugin
Change-type: patch
2021-08-20 08:31:15 +02:00
Lorenzo Alberto Maria Ambrosi
4d5e5a3b0b Remove redundant codespell dependency/tests
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-20 08:28:48 +02:00
Balena CI
8b3f37102d v1.5.121 2021-07-14 19:55:16 +03:00
bulldozer-balena[bot]
4b74253631 Merge pull request #3489 from balena-io/direct-select-drive
patch: Select drive on list interaction rather than modal closing
2021-07-14 16:52:42 +00:00
Vipul Gupta
a81b552b95 patch: Delete Codeowners 2021-07-02 13:59:11 +02:00
Lorenzo Alberto Maria Ambrosi
53f53c0f75 Add source maps for devtools
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-25 17:28:17 +02:00
Lorenzo Alberto Maria Ambrosi
fdaf5c69d6 Clone submodules when initializing modules
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-24 14:52:22 +02:00
Lorenzo Alberto Maria Ambrosi
061afca5d3 patch: Select drive on list interaction rather than modal closing
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-24 14:52:22 +02:00
Balena CI
ccb08a48f1 v1.5.120 2021-05-11 19:56:49 +03:00
bulldozer-balena[bot]
a8f3d45b12 Merge pull request #3514 from balena-io/add-cloudsmith-reference
Update README to reference Cloudsmith
2021-05-11 16:49:15 +00:00
Lorenzo Alberto Maria Ambrosi
7e333caaf9 Update README to reference Cloudsmith
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-11 17:59:17 +02:00
Balena CI
70229e8684 v1.5.119 2021-05-01 11:45:32 +03:00
bulldozer-balena[bot]
261700389b Merge pull request #3500 from balena-io/new-deb-rpm-ppa
Update readme for new PPA provider
2021-05-01 08:43:36 +00:00
Lorenzo Alberto Maria Ambrosi
250aed2eb1 Update readme for new PPA provider
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-04-30 23:30:08 +02:00
Balena CI
ed1f008fe2 v1.5.118 2021-04-29 11:47:01 +03:00
bulldozer-balena[bot]
e9ce270dab Merge pull request #3495 from balena-io/etcher-dev-server
Etcher dev server
2021-04-29 08:45:03 +00:00
Zane Hitchcox
1ee110bc95 patch: development environment
Add webpack dev server and hot module reloading to get live changes and reloads without reloading the whole electron app.

This patch also runs the development environment in development mode, which is much, much faster on builds and rebuilds.
2021-04-26 21:17:32 -04:00
Zane Hitchcox
33dd07c675 patch: watch files for electron 2021-04-20 22:30:05 -04:00
Balena CI
39ccbbeeda v1.5.117 2021-04-06 14:44:31 +03:00
bulldozer-balena[bot]
55d2400ac7 Merge pull request #3432 from balena-io/electron-11
Electron 11
2021-04-06 11:42:19 +00:00
Alexis Svinartchouk
0bdea5c54c Rename mac releases (keep old naming)
Change-type: patch
2021-04-02 15:52:33 +02:00
Alexis Svinartchouk
3be372d49f Disable spectron tests on macOS
Change-type: patch
2021-04-01 15:48:39 +02:00
Alexis Svinartchouk
d0c66b2c48 Update electron to v12.0.2
Change-type: patch
2021-04-01 12:13:34 +02:00
Alexis Svinartchouk
65082c4790 Update etcher-sdk from 6.1.1 to 6.2.1
Update etcher-sdk from 6.1.1 to 6.2.1

Change-type: patch
2021-03-29 14:11:44 +02:00
Alexis Svinartchouk
e87ed9beed Fix getAppPath() returning an asar file on macOS
Change-type: patch
2021-03-23 17:53:54 +01:00
Andrew Scheller
bc5563d9c2 Grammar fix
"flash directly" sounds odd

Change-type: patch
2021-03-23 14:32:30 +01:00
vlad doster
ad83ab5dcc (docs) update README.md
- fix spelling
- emphasize notes
- add link
- fix macOS to account for new homebrew API

Change-type: patch
2021-03-23 12:00:40 +01:00
Andrew Scheller
0dc1cf9701 Update copyright year in electron-builder.yml
Change-type: patch
2021-03-23 11:55:43 +01:00
Andrew Scheller
11489c6538 Update copyright year in .resinci.json
Change-type: patch
2021-03-23 11:55:29 +01:00
Dugan Chen
2619d4bc86 Separate the Yum and DNF instructions.
Change-type: patch
2021-03-23 11:55:04 +01:00
Alexis Svinartchouk
3730efd350 Set msvs_version to 2019 when rebuilding
Change-type: patch
2021-03-22 17:23:43 +01:00
Alexis Svinartchouk
6ece32c546 Use moduleIds: 'natural' in webpack config to keep js files in arm64 and x64 mac builds identical
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
fd9996a3cc Update electron-builder to 22.10.5
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
f06cc89152 Update spectron to v13
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
c1d7ab3fa9 Update dependencies, use aws4-axios@2.2.1 to avoid adding more dependiencies
Also filter out dmg-license dependencies from the shrinkwrap file
aws4-axios@2.3.0 brings in react-native, see aws/aws-sdk-js-v3#1797

Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
b206483c7c Update scripts to build universal mac dmgs on the ci
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
c3eb8c7b56 Fix beforeBuild.js script to also work on mac
Change-type: patch
2021-03-15 19:26:49 +01:00
Alexis Svinartchouk
0849d4f435 Support building universal dmgs (x64 and arm64) for mac
Change-type: patch
2021-03-15 19:26:49 +01:00
Alexis Svinartchouk
1dba3ae19b Update electron-builder to 22.10.4
Change-type: patch
2021-02-16 15:49:18 +01:00
Alexis Svinartchouk
f33f2e3771 Fix titlebar z-index
Change-type: patch
2021-02-16 15:49:18 +01:00
Alexis Svinartchouk
e56aaed973 Explicitly set contextIsolation to false
Change-type: patch
2021-02-16 15:49:12 +01:00
Alexis Svinartchouk
a4659f038e Update electron from 9.4.1 to 11.2.3
Change-type: patch
2021-02-10 17:51:56 +01:00
Alexis Svinartchouk
cd462818da Update etcher-sdk from 6.1.0 to 6.1.1
Update etcher-sdk from 6.1.0 to 6.1.1

Change-type: patch
2021-02-10 17:50:47 +01:00
Balena CI
37769efbed v1.5.116 2021-02-03 17:56:40 +02:00
bulldozer-balena[bot]
0f70c4bbce Merge pull request #3414 from balena-io/116
116
2021-02-03 15:54:29 +00:00
Alexis Svinartchouk
48b5e8b9d9 Only cleanup temporary decompressed files in child-writer
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
1f138f0ecc Add .versionbot/CHANGELOG.yml
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
73f67e99ca Stop using node-tmp, use withTmpFile from etcher-sdk instead
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
9114da2445 Update etcher-sdk from 5.2.2 to 6.1.0
Update etcher-sdk from 5.2.2 to 6.1.0

Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
554bbcc780 Revert "Change some border colors to have higher contrast"
This reverts commit 8c4edaabba.

Change-type: patch
2021-02-01 19:44:37 +01:00
Alexis Svinartchouk
4db2289cfd Update electron to v9.4.1
Change-type: patch
2021-02-01 19:44:37 +01:00
Alexis Svinartchouk
c15b56bc23 Update etcher-sdk from 5.2.1 to 5.2.2
Update etcher-sdk from 5.2.1 to 5.2.2

Change-type: patch
2021-01-19 18:44:19 +01:00
Balena CI
9f52dda6ae v1.5.115 2021-01-18 14:09:10 +02:00
bulldozer-balena[bot]
fadcefb11a Merge pull request #3413 from balena-io/115
Update etcher-sdk from 5.1.12 to 5.2.1
2021-01-18 12:07:11 +00:00
Alexis Svinartchouk
361c32913c Update etcher-sdk from 5.1.12 to 5.2.1
Update etcher-sdk from 5.1.12 to 5.2.1

Change-type: patch
2021-01-18 10:46:59 +01:00
Balena CI
5c2042198e v1.5.114 2021-01-15 14:30:49 +02:00
bulldozer-balena[bot]
99df53098c Merge pull request #3394 from balena-io/114
114
2021-01-15 12:28:31 +00:00
Alexis Svinartchouk
aa563c87bd Remove libappindicator1 debian dependency
Changelog-entry: Remove libappindicator1 debian dependency
Change-type: patch
2021-01-12 15:22:43 +01:00
Alexis Svinartchouk
1188888956 Update etcher-sdk from 5.1.11 to 5.1.12
Update etcher-sdk from 5.1.11 to 5.1.12

Change-type: patch
2021-01-12 15:22:43 +01:00
Alexis Svinartchouk
f9d7991dc8 Update rendition from 18.8.3 to 19.2.0
Update rendition from 18.8.3 to 19.2.0

Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
53954e81fd Update dependencies
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
f82996bfd1 Update @balena/lint to 5.3.0
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
b74069eb41 Update webpack to v5
Changelog-entry: Update webpack to v5
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
e8c7591751 Fix typo in webpack.config.ts comment
Change-type: patch
2021-01-12 15:22:42 +01:00
Aaron Shaw
3521b61a81 docs: fix quote marks
Fix quote mark styling

Change-type: patch
Signed-off-by: Aaron Shaw <aaron@balena.io>
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
93db90c725 Disable screensaver while flashing (on balena-electron-env)
Change-type: patch
2021-01-12 15:22:42 +01:00
Balena CI
1dc56aed14 v1.5.113 2020-12-17 16:23:23 +02:00
bulldozer-balena[bot]
d814202424 Merge pull request #3377 from balena-io/113
113
2020-12-17 14:20:57 +00:00
Alexis Svinartchouk
c54856a616 Only store the first error for each target
Changelog-entry: Show the first error for each drive (not the last)
Change-type: patch
2020-12-16 12:33:17 +01:00
Alexis Svinartchouk
fc45df270a Fix red leds not showing for failed devices
Change-type: patch
2020-12-14 18:59:40 +01:00
Aaron Shaw
3cde2faed0 docs: add documentation links
add documentation and faq links

Change-Type: patch
Closes: https://github.com/balena-io/etcher/issues/3191
Signed-off-by: Aaron Shaw <aaron@balena.io>
2020-12-14 18:59:40 +01:00
Aaron Shaw
b4b8c89aad docs: update macOS version
Update macOS version as latest version of Electron is 10.10 compatible only (Yosemite)

Change-Type: patch
Signed-off-by: Aaron Shaw <aaron@balena.io>
2020-12-14 18:59:40 +01:00
Alexis Svinartchouk
36d05724c0 Improve hover message when the drive is too small
Changelog-entry: Improve hover message when the drive is too small
Change-type: patch
2020-12-14 18:59:40 +01:00
Alexis Svinartchouk
b1e4e681d1 Update electron to v9.4.0
Changelog-entry: Update electron to v9.4.0
Change-type: patch
2020-12-14 18:59:40 +01:00
Giovanni Garufi
3987078c11 Update npm to v6.14.8
Change-type: patch
2020-12-11 17:51:50 +01:00
Alexis Svinartchouk
de0010eb72 Update rgb leds colors
Change-type: patch
2020-12-10 17:18:54 +01:00
Alexis Svinartchouk
1f94f44b18 Remove unmountOnSuccess setting
Changelog-entry: Remove unmountOnSuccess setting
Change-type: patch
2020-12-10 15:36:19 +01:00
Alexis Svinartchouk
fe0b45cae6 Only show auto-updates setting on supported targets
Change-type: patch
2020-12-10 15:35:37 +01:00
Alexis Svinartchouk
c32e485f27 Remove dead code in settings modal
Change-type: patch
2020-12-10 14:05:08 +01:00
Alexis Svinartchouk
409b78fc21 Fix effective flashing speed calculation for compressed images
Changelog-entry: Fix effective flashing speed calculation for compressed images
Change-type: patch
2020-12-08 17:14:49 +01:00
bulldozer-balena[bot]
2f08142f5a Merge pull request #3379 from balena-io/high-contrast-lines
Change some border colors to have higher contrast
2020-12-08 13:54:20 +00:00
Lorenzo Alberto Maria Ambrosi
8c4edaabba Change some border colors to have higher contrast
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-12-08 14:37:24 +01:00
Alexis Svinartchouk
05497ce85c Update etcher-sdk from 5.1.10 to 5.1.11
Update etcher-sdk from 5.1.10 to 5.1.11

Changelog-entry: Update etcher-sdk from 5.1.10 to 5.1.11
Change-type: patch
2020-12-07 19:31:41 +01:00
Alexis Svinartchouk
d3df2fe57e Update sys-class-rgb-led from 2.1.1 to 3.0.0
Update sys-class-rgb-led from 2.1.1 to 3.0.0

Changelog-entry: Update sys-class-rgb-led from 2.1.1 to 3.0.0
Change-type: patch
2020-12-04 14:11:51 +01:00
Balena CI
a0f07082f2 v1.5.112 2020-12-03 17:19:22 +02:00
bulldozer-balena[bot]
b7efa8e1f0 Merge pull request #3362 from balena-io/112
112
2020-12-03 15:17:28 +00:00
Alexis Svinartchouk
3647457bb5 Add rendition and sys-class-rgb-led to repo.yml
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
2e5a39dcd8 Update sys-class-rgb-led from 2.1.0 to 2.1.1
Update sys-class-rgb-led from 2.1.0 to 2.1.1

Changelog-entry: Update sys-class-rgb-led from 2.1.0 to 2.1.1
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
edabacfb3a Fix spectron test to work on Windows in all cases
Change-type: none
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
f46176fd10 Fix layout when the featured project is not showing
Changelog-entry: Fix layout when the featured project is not showing
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
2158e20380 Improve flashing error handling
Changelog-entry: Improve flashing error handling
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
fa593e33d1 Update repo.yml to enable nested changelogs
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
50730bd3df Fix imports in child-writer.ts
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
4e68955981 Target commit instead of branch name for sudo-prompt
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
3c0084d012 Fix modal content height on Windows
Change-type: patch
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
8bd11a01ae Update etcher-sdk from 5.1.5 to 5.1.10
Update etcher-sdk from 5.1.5 to 5.1.10

Changelog-entry: Update etcher-sdk from 5.1.5 to 5.1.10
Change-type: patch
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
da3a22d0f6 Set useContentSize to true so the size is the same on all platforms
Changelog-entry: Set useContentSize to true so the size is the same on all platforms
Change-type: patch
2020-11-24 17:10:17 +01:00
Balena CI
e708212d41 v1.5.111 2020-11-23 19:54:50 +02:00
bulldozer-balena[bot]
a5ceba8435 Merge pull request #3345 from balena-io/111
111
2020-11-23 17:52:38 +00:00
Alexis Svinartchouk
446e8e1253 Update bl
Change-type: patch
2020-11-20 20:10:35 +01:00
Alexis Svinartchouk
c69b2fa053 Warn when the source drive has no partition table
Changelog-entry: Warn when the source drive has no partition table
Change-type: patch
2020-11-20 15:29:03 +01:00
Alexis Svinartchouk
0597c0e908 Update etcher-sdk to 5.1.5
Change-type: patch
2020-11-20 14:49:41 +01:00
Alexis Svinartchouk
af2b6bc8ca Update typescript to 4.1.2
Change-type: patch
2020-11-20 14:45:44 +01:00
Alexis Svinartchouk
a2c7a542df Use a different icon when no source drive is available
Changelog-entry: Use a different icon when no source drive is available
Change-type: patch
2020-11-20 14:45:18 +01:00
Alexis Svinartchouk
e37ae2743f Update etcher-sdk to 5.1.3
Change-type: patch
2020-11-17 11:33:36 +01:00
Alexis Svinartchouk
644d955f08 Prevent opening more than one file selector
Change-type: patch
2020-11-16 16:14:36 +01:00
Alexis Svinartchouk
e7b4f09021 Allow selecting a locked SD card as the source drive
Changelog-entry: Allow selecting a locked SD card as the source drive
Change-type: patch
2020-11-16 14:16:38 +01:00
Alexis Svinartchouk
1e0a6a3129 Removed disableExplicitDriveSelection setting, use autoSelectAllDrives instead
Change-type: patch
2020-11-13 20:23:07 +01:00
Alexis Svinartchouk
ef3b8915d8 Update etcher-sdk to 5.1.2
Change-type: patch
2020-11-13 18:30:26 +01:00
Alexis Svinartchouk
e58cfd89c5 Add successBannerURL setting
Change-type: patch
2020-11-11 13:31:04 +01:00
Alexis Svinartchouk
1c52379ee3 Add drivesOrder setting
Change-type: patch
2020-11-11 13:30:54 +01:00
Alexis Svinartchouk
e2c2b40690 Remove "Validate write on success" setting
Validation is always enabled, press the "skip" button to skip it.

Changelog-entry: Remove "Validate write on success" setting. Validation is always enabled, press the "skip" button to skip it.
Change-type: patch
2020-11-11 13:30:26 +01:00
Alexis Svinartchouk
bddb89e4a1 Update electron to v9.3.3
Changelog-entry: Update electron to v9.3.3
Change-type: patch
2020-11-11 13:30:18 +01:00
Alexis Svinartchouk
560ed91e2e Update etcher-sdk to 5.1.1, use WASM ext2fs module
Changelog-entry: Update etcher-sdk to 5.1.1, use WASM ext2fs module
Change-type: patch
2020-11-11 13:29:44 +01:00
Balena CI
1f8f7ad7f8 v1.5.110 2020-11-05 13:56:29 +02:00
bulldozer-balena[bot]
a2a0f2ef41 Merge pull request #3325 from balena-io/new-success-screen-2
New success screen 2
2020-11-05 11:54:37 +00:00
Lorenzo Alberto Maria Ambrosi
40e5fb2287 Add primary colors to default flow
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-04 14:01:56 +01:00
Lorenzo Alberto Maria Ambrosi
6c49c71b3f Remove console.log in tests
Change-type: patch
Changelog-entry: Remove console.log in tests
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
deb3db0fff Add more typings & refactor code accordingly
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
4872fa3d6e Fix URL not being selected with custom protocol
Change-type: patch
Changelog-entry: Fix URL not being selected with custom protocol
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
640a7409ee Add dash on table when selecting only some rows
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
a7637ad8d4 Fix settings spacing
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
31409c61ca Use drive-selector's table for flash errors table
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:46 +01:00
Lorenzo Alberto Maria Ambrosi
e74dc9eb60 Update rendition to v18.8.3
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:01:11 +01:00
Lorenzo Alberto Maria Ambrosi
06997fdf29 Fix zoomFactor in webviews
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:01:11 +01:00
Lorenzo Alberto Maria Ambrosi
611e659626 Add retry button to the errors modal in success screen
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:01:07 +01:00
Lorenzo Alberto Maria Ambrosi
e484ae9837 Cleanup after child-process is terminated
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-22 14:08:44 +02:00
Lorenzo Alberto Maria Ambrosi
7e7ca9524e Add skip function to validation
Change-type: patch
Changelog-entry: Add skip function to validation
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-22 14:08:44 +02:00
Lorenzo Alberto Maria Ambrosi
db09b7440d Rework success screen
Change-type: patch
Changelog-entry: Rework success screen
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-22 14:07:20 +02:00
94 changed files with 23896 additions and 8815 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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 -->

38
.github/actions/always/action.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: cleanup
# 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
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
# delete draft releases if the pull request is closed without merging
- name: Delete draft release
if: |
runner.os == 'Linux' &&
github.event_name == 'pull_request' &&
github.event.pull_request.merged == false &&
github.event.action == 'closed'
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
gh release delete --yes '${{ github.event.pull_request.head.ref }}' || true
env:
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}

54
.github/actions/finalize/action.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
---
name: publish GitHub 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
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
- name: Get release version
if: runner.os == 'Linux'
id: get_release
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
# https://docs.github.com/en/rest/releases
- name: Finalize GitHub release
if: runner.os == 'Linux'
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
previous_tag="$(git tag --sort=-version:refname | head -n 2 | tail -n 1)"
release_notes="$(git log ${previous_tag}..HEAD --pretty=reference)"
gh release edit '${{ github.event.pull_request.head.ref }}' \
--notes "${release_notes}" \
--title 'v${{ steps.get_release.outputs.version }}' \
--tag 'v${{ steps.get_release.outputs.version }}' \
--prerelease=false \
--draft=false
release_id="$(gh api "/repos/${{ github.repository }}/releases/tags/v${{ steps.get_release.outputs.version }}" \
-H 'Accept: application/vnd.github+json' | jq -r .id)"
gh api --method PATCH "/repos/${{ github.repository }}/releases/${release_id}" \
-H 'Accept: application/vnd.github+json' \
-F make_latest="true"
env:
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}

237
.github/actions/publish/action.yml vendored Normal file
View File

@@ -0,0 +1,237 @@
---
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.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\" {} \;
# https://github.com/softprops/action-gh-release#-customizing
- name: Create draft GitHub (pre)release
uses: softprops/action-gh-release@v1
with:
# use PR branch name for draft releases
name: ${{ github.event.pull_request.head.ref }}
tag_name: ${{ github.event.pull_request.head.ref }}
draft: true
prerelease: true
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
files: |
dist/*.AppImage
dist/*.blockmap
dist/*.deb
dist/*.dmg
dist/*.exe
dist/*.rpm
dist/*.zip
dist/latest*.yml
- 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

71
.github/actions/test/action.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
---
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:
- name: Delete previous draft release
if: runner.os == 'Linux'
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
gh release delete --yes '${{ github.event.pull_request.head.ref }}' || true
env:
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
# 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

16
.github/workflows/flowzone.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Flowzone
on:
pull_request:
types: [opened, synchronize, closed]
branches:
- "main"
- "master"
jobs:
flowzone:
name: Flowzone
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
secrets: inherit
with:
tests_run_on: '["ubuntu-latest","macos-latest","windows-2019"]'

6
.gitignore vendored
View File

@@ -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

Binary file not shown.

Binary file not shown.

BIN
.gitsecret/keys/trustdb.gpg Normal file

Binary file not shown.

View 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
.nvmrc Normal file
View File

@@ -0,0 +1 @@
14

View File

@@ -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"
]
}
}
}
}

12037
.versionbot/CHANGELOG.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,948 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
# 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)
* Add rendition and sys-class-rgb-led to repo.yml [Alexis Svinartchouk]
<details>
<summary> Update sys-class-rgb-led from 2.1.0 to 2.1.1 [Alexis Svinartchouk] </summary>
> ## sys-class-rgb-led-2.1.1
> ### (2020-12-01)
>
> * Replace resin-lint with @balena/lint [Alexis Svinartchouk]
> * Update typescript to v4.1.2 [Alexis Svinartchouk]
> * Add versionbot changelog [Alexis Svinartchouk]
>
</details>
* Fix layout when the featured project is not showing [Alexis Svinartchouk]
* Improve flashing error handling [Alexis Svinartchouk]
* Fix modal content height on Windows [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 5.1.5 to 5.1.10 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.1.10
> ### (2020-12-02)
>
>
> <details>
> <summary> Update balena-image-fs from 7.0.5 to 7.0.6 [Alexis Svinartchouk] </summary>
>
>> ### balena-image-fs-7.0.6
>> #### (2020-12-02)
>>
>>
>> <details>
>> <summary> Update ext2fs from 3.0.4 to 3.0.5 [Alexis Svinartchouk] </summary>
>>
>>> #### node-ext2fs-3.0.5
>>> ##### (2020-12-02)
>>>
>>> * Fix reading and discarding with offsets > 32 bits [Alexis Svinartchouk]
>>>
>> </details>
>>
>>
> </details>
>
>
> ## etcher-sdk-5.1.9
> ### (2020-12-01)
>
> * Add repo.yml file [Alexis Svinartchouk]
> * Update @balena/udif from 1.1.0 to 1.1.1 [Alexis Svinartchouk]
>
> <details>
> <summary> Update zip-part-stream from 1.0.2 to 1.0.3 [Alexis Svinartchouk] </summary>
>
>> ### zip-part-stream-1.0.3
>> #### (2020-11-30)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update node-raspberrypi-usbboot from 0.2.9 to 0.2.10 [Alexis Svinartchouk] </summary>
>
>> ### node-raspberrypi-usbboot-0.2.10
>> #### (2020-11-30)
>>
>> * Update typescript to v4.1.2 [Alexis Svinartchouk]
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update mountutils from 1.3.19 to 1.3.20 [Alexis Svinartchouk] </summary>
>
>> ### mountutils-1.3.20
>> #### (2020-11-30)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update gzip-stream from 1.1.1 to 1.1.2 [Alexis Svinartchouk] </summary>
>
>> ### gzip-stream-1.1.2
>> #### (2020-11-30)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update drivelist from 9.2.1 to 9.2.2 [Alexis Svinartchouk] </summary>
>
>> ### drivelist-9.2.2
>> #### (2020-11-30)
>>
>> * Update typescript to v4.1.2 [Alexis Svinartchouk]
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update blockmap from 4.0.2 to 4.0.3 [Alexis Svinartchouk] </summary>
>
>> ### blockmap-4.0.3
>> #### (2020-11-30)
>>
>> * Update typescript to v4.1.2 [Alexis Svinartchouk]
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update partitioninfo from 6.0.1 to 6.0.2 [Alexis Svinartchouk] </summary>
>
>> ### partitioninfo-6.0.2
>> #### (2020-11-27)
>>
>>
>> <details>
>> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
>>
>>> #### file-disk-8.0.1
>>> ##### (2020-11-26)
>>>
>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>
>> </details>
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
>
>> ### file-disk-8.0.1
>> #### (2020-11-26)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
>> ### file-disk-8.0.1
>> #### (2020-11-26)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update balena-image-fs from 7.0.4 to 7.0.5 [Alexis Svinartchouk] </summary>
>
>> ### balena-image-fs-7.0.5
>> #### (2020-11-27)
>>
>>
>> <details>
>> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
>>
>>> #### file-disk-8.0.1
>>> ##### (2020-11-26)
>>>
>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>
>> </details>
>>
>>
>> <details>
>> <summary> Update ext2fs from 3.0.3 to 3.0.4 [Alexis Svinartchouk] </summary>
>>
>>> #### node-ext2fs-3.0.4
>>> ##### (2020-11-26)
>>>
>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>
>> </details>
>>
>>
>> <details>
>> <summary> Update partitioninfo from 6.0.1 to 6.0.2 [Alexis Svinartchouk] </summary>
>>
>>> #### partitioninfo-6.0.2
>>> ##### (2020-11-27)
>>>
>>>
>>> <details>
>>> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
>>>
>>>> ##### file-disk-8.0.1
>>>> ###### (2020-11-26)
>>>>
>>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>>
>>> </details>
>>>
>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>
>> </details>
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> ## etcher-sdk-5.1.8
> ### (2020-11-26)
>
> * Add versionbot changelog [Alexis Svinartchouk]
>
> ## etcher-sdk-5.1.7
> ### (2020-11-25)
>
> * Don't start opening drives in advance to avoid unhandled rejections [Alexis Svinartchouk]
> * Update generated docs [Alexis Svinartchouk]
>
> ## etcher-sdk-5.1.6
> ### (2020-11-24)
>
> * Do not unmount source drives [Alexis Svinartchouk]
> * Factorize retrying transient errors [Alexis Svinartchouk]
> * Retry opening files & block devices on transient errors [Alexis Svinartchouk]
> * Update generated docs [Alexis Svinartchouk]
>
</details>
* Set useContentSize to true so the size is the same on all platforms [Alexis Svinartchouk]
# v1.5.111
## (2020-11-23)
* Warn when the source drive has no partition table [Alexis Svinartchouk]
* Use a different icon when no source drive is available [Alexis Svinartchouk]
* Allow selecting a locked SD card as the source drive [Alexis Svinartchouk]
* Remove "Validate write on success" setting. Validation is always enabled, press the "skip" button to skip it. [Alexis Svinartchouk]
* Update electron to v9.3.3 [Alexis Svinartchouk]
* Update etcher-sdk to 5.1.1, use WASM ext2fs module [Alexis Svinartchouk]
# v1.5.110
## (2020-11-04)
* Remove console.log in tests [Lorenzo Alberto Maria Ambrosi]
* Fix URL not being selected with custom protocol [Lorenzo Alberto Maria Ambrosi]
* Add skip function to validation [Lorenzo Alberto Maria Ambrosi]
* Rework success screen [Lorenzo Alberto Maria Ambrosi]
# v1.5.109
## (2020-09-14)

View File

@@ -1,2 +0,0 @@
* @thundron @zvin @jviotti
/scripts @nazrhom

4
FAQ.md
View File

@@ -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).

View File

@@ -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:

175
README.md
View File

@@ -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).
[![Current Release](https://img.shields.io/github/release/balena-io/etcher.svg?style=flat-square)](https://balena.io/etcher)
[![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)](https://github.com/balena-io/etcher/blob/master/LICENSE)
[![Dependency status](https://img.shields.io/david/balena-io/etcher.svg?style=flat-square)](https://david-dm.org/balena-io/etcher)
[![Balena.io Forums](https://img.shields.io/discourse/https/forums.balena.io/topics.svg?style=flat-square&label=balena.io%20forums)](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
> [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge)](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,20 +170,20 @@ yay -S balena-etcher
yay -R balena-etcher
```
#### Brew Cask (macOS)
#### Brew (macOS)
Note that the Etcher Cask has to be updated manually to point to new versions,
**Note**: Etcher 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
brew install balenaetcher
```
##### Uninstall
```sh
brew cask uninstall balenaetcher
brew uninstall balenaetcher
```
#### Chocolatey (Windows)
@@ -168,20 +203,20 @@ 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

View File

@@ -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
View 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

View File

@@ -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
})
}

Binary file not shown.

View File

@@ -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,
},
},
);
}
}

View File

@@ -91,7 +91,7 @@ make electron-develop
```sh
# Build the GUI
make webpack
npm run webpack
# Start Electron
npm start
```

View File

@@ -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

View File

@@ -1,11 +1,9 @@
# https://www.electron.build/configuration/configuration
appId: io.balena.etcher
copyright: Copyright 2016-2020 Balena Ltd
copyright: Copyright 2016-2021 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
@@ -16,6 +14,9 @@ mac:
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 +33,10 @@ dmg:
height: 405
win:
icon: assets/icon.ico
target:
- zip
- nsis
- portable
nsis:
oneClick: true
runAfterFinish: true
@@ -44,17 +49,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 +99,7 @@ deb:
- libxss1
- libxtst6
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
afterInstall: "./after-install.tpl"
rpm:
depends:
- util-linux

View File

@@ -23,17 +23,12 @@ import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json';
import {
DrivelistDrive,
isDriveValid,
isSourceDrive,
} from '../../shared/drive-constraints';
import { DrivelistDrive, isSourceDrive } from '../../shared/drive-constraints';
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, selectDrive } from './models/selection-state';
import { deselectImage, getImage } from './models/selection-state';
import * as settings from './models/settings';
import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics';
@@ -42,6 +37,7 @@ 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';
window.addEventListener(
'unhandledrejection',
@@ -220,8 +216,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`
@@ -251,14 +246,6 @@ async function addDrive(drive: Drive) {
const drives = getDrives();
drives[preparedDrive.device] = preparedDrive;
setDrives(drives);
if (
(await settings.get('autoSelectAllDrives')) &&
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isDriveValid(drive.drive, getImage())
) {
selectDrive(drive.device);
}
}
function removeDrive(drive: Drive) {
@@ -346,13 +333,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'),
@@ -368,5 +361,3 @@ async function main() {
},
);
}
main();

View File

@@ -42,8 +42,8 @@ import {
Table,
} from '../../styled-components';
import DriveSVGIcon from '../../../assets/tgt.svg';
import { SourceMetadata } from '../source-selector/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis';
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number;
@@ -137,15 +137,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 {
@@ -166,12 +169,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(),
@@ -198,7 +203,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>
);
}
@@ -258,7 +265,8 @@ 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)
);
}
@@ -301,9 +309,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);
}
}
@@ -311,6 +319,7 @@ export class DriveSelector extends React.Component<
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
drive,
this.state.image,
this.props.write,
).slice(0, 2);
return (
// the column render fn expects a single Element
@@ -345,16 +354,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();
@@ -380,8 +379,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;
@@ -405,7 +404,7 @@ export class DriveSelector extends React.Component<
</Flex>
}
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={cancel}
cancel={() => cancel(this.originalList)}
done={() => done(selectedList)}
action={`Select (${selectedList.length})`}
primaryButtonProps={{
@@ -422,7 +421,7 @@ export class DriveSelector extends React.Component<
alignItems="center"
width="100%"
>
<DriveSVGIcon width="40px" height="90px" />
{this.props.emptyListIcon}
<b>{this.props.emptyListLabel}</b>
</Flex>
) : (
@@ -445,14 +444,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),
});
@@ -464,6 +483,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,
);
@@ -512,7 +534,7 @@ 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: {} });

View File

@@ -20,6 +20,7 @@ import { v4 as uuidV4 } from 'uuid';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings';
import { Actions, store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { FlashAnother } from '../flash-another/flash-another';
@@ -39,24 +40,27 @@ function restart(goToMain: () => void) {
goToMain();
}
async function getSuccessBannerURL() {
return (
(await settings.get('successBannerURL')) ??
'https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true'
);
}
function FinishPage({ goToMain }: { goToMain: () => void }) {
const [webviewShowing, setWebviewShowing] = React.useState(false);
const [successBannerURL, setSuccessBannerURL] = React.useState('');
(async () => {
setSuccessBannerURL(await getSuccessBannerURL());
})();
const flashResults = flashState.getFlashResults();
let errors: FlashError[] = flashResults.results?.errors;
if (errors === undefined) {
errors = (store.getState().toJS().failedDevicePaths || []).map(
([, error]: [string, FlashError]) => ({
...error,
}),
);
}
const {
averageSpeed,
blockmappedSize,
bytesWritten,
failed,
size,
} = flashState.getFlashState();
const errors: FlashError[] = (
store.getState().toJS().failedDeviceErrors || []
).map(([, error]: [string, FlashError]) => ({
...error,
}));
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
flashState.getFlashState();
const {
skip,
results = {
@@ -85,7 +89,7 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
}}
>
<FlashResults
image={selectionState.getImageName()}
image={selectionState.getImage()?.name}
results={results}
skip={skip}
errors={errors}
@@ -99,18 +103,20 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
}}
/>
</Flex>
<SafeWebview
src="https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true"
onWebviewShow={setWebviewShowing}
style={{
display: webviewShowing ? 'flex' : 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
{successBannerURL.length && (
<SafeWebview
src={successBannerURL}
onWebviewShow={setWebviewShowing}
style={{
display: webviewShowing ? 'flex' : 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
</Flex>
);
}

View File

@@ -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';
@@ -27,49 +26,47 @@ import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units';
import FlashSvg from '../../../assets/flash.svg';
import { getDrives } from '../../models/available-drives';
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';
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
[data-display='table-head'],
[data-display='table-body'] {
[data-display='table-cell'] {
&:first-child {
width: 30%;
}
&&& [data-display='table-head'],
&&& [data-display='table-body'] {
> [data-display='table-row'] {
> [data-display='table-cell'] {
&:first-child {
width: 30%;
}
&:nth-child(2) {
width: 20%;
}
&:nth-child(2) {
width: 20%;
}
&:last-child {
width: 50%;
&:last-child {
width: 50%;
}
}
}
}
`;
const DoneIcon = (props: {
skipped: boolean;
color: string;
allFailed: boolean;
someFailed: boolean;
}) => {
const { allFailed, someFailed } = props;
const someOrAllFailed = allFailed || someFailed;
const svgProps = {
width: '24px',
fill: someOrAllFailed ? '#c6c8c9' : '#1ac135',
width: '28px',
fill: props.color,
style: {
width: '28px',
height: '28px',
marginTop: '-25px',
marginLeft: '13px',
zIndex: 1,
color: someOrAllFailed ? '#c6c8c9' : '#1ac135',
},
};
return allFailed && !props.skipped ? (
return props.allFailed && !props.skipped ? (
<TimesCircleSvg {...svgProps} />
) : (
<CheckCircleSvg {...svgProps} />
@@ -106,6 +103,19 @@ const columns: Array<TableColumn<FlashError>> = [
},
];
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 = '',
@@ -119,22 +129,18 @@ export function FlashResults({
errors: FlashError[];
skip: boolean;
results: {
bytesWritten: number;
sourceMetadata: {
size: number;
blockmappedSize: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
devices: { failed: number; successful: number };
};
} & FlexProps) {
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = results.devices.successful === 0;
const effectiveSpeed = _.round(
bytesToMegabytes(
results.sourceMetadata.size /
(results.bytesWritten / results.averageFlashingSpeed),
),
const allFailed = !skip && results.devices.successful === 0;
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
1,
);
return (
@@ -151,42 +157,41 @@ export function FlashResults({
<DoneIcon
skipped={skip}
allFailed={allFailed}
someFailed={results.devices.failed !== 0}
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
/>
<Txt>{middleEllipsis(image, 24)}</Txt>
</Flex>
<Txt fontSize={24} color="#fff" mb="17px">
Flash Complete!
Flash {allFailed ? 'Failed' : 'Complete'}!
</Txt>
{skip ? <Flex color="#7e8085">Validation has been skipped</Flex> : null}
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
</Flex>
<Flex flexDirection="column" color="#7e8085">
{Object.entries(results.devices).map(([type, quantity]) => {
const failedTargets = type === 'failed';
return quantity ? (
<Flex alignItems="center">
<CircleSvg
width="14px"
fill={type === 'failed' ? '#ff4444' : '#1ac135'}
color={failedTargets ? '#ff4444' : '#1ac135'}
/>
<Txt ml="10px" color="#fff">
{quantity}
</Txt>
<Txt
ml="10px"
tooltip={failedTargets ? formattedErrors(errors) : undefined}
>
{progress[type](quantity)}
</Txt>
{failedTargets && (
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
more info
</Link>
)}
</Flex>
) : null;
})}
{results.devices.successful !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#1ac135" />
<Txt ml="10px" color="#fff">
{results.devices.successful}
</Txt>
<Txt ml="10px">
{progress.successful(results.devices.successful)}
</Txt>
</Flex>
) : null}
{errors.length !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#ff4444" />
<Txt ml="10px" color="#fff">
{errors.length}
</Txt>
<Txt ml="10px" tooltip={formattedErrors(errors)}>
{progress.failed(errors.length)}
</Txt>
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
more info
</Link>
</Flex>
) : null}
{!allFailed && (
<Txt
fontSize="10px"
@@ -218,12 +223,15 @@ export function FlashResults({
done={() => {
setShowErrorsInfo(false);
resetState();
selection
.getSelectedDrives()
.filter((drive) =>
errors.every((error) => error.device !== drive.device),
getDrives()
.map((drive) => {
selection.deselectDrive(drive.device);
return drive.device;
})
.filter((driveDevice) =>
errors.some((error) => error.device === driveDevice),
)
.forEach((drive) => selection.deselectDrive(drive.device));
.forEach((driveDevice) => selection.selectDrive(driveDevice));
goToMain();
}}
>

View File

@@ -18,12 +18,12 @@ import * as React from 'react';
import { Flex, Button, ProgressBar, Txt } from 'rendition';
import { default as styled } from 'styled-components';
import { fromFlashState, FlashState } from '../../modules/progress-status';
import { fromFlashState } from '../../modules/progress-status';
import { StepButton } from '../../styled-components';
const FlashProgressBar = styled(ProgressBar)`
> div {
width: 220px;
width: 100%;
height: 12px;
color: white !important;
text-shadow: none !important;
@@ -33,7 +33,7 @@ const FlashProgressBar = styled(ProgressBar)`
}
}
width: 220px;
width: 100%;
height: 12px;
margin-bottom: 6px;
border-radius: 14px;
@@ -44,7 +44,7 @@ const FlashProgressBar = styled(ProgressBar)`
`;
interface ProgressButtonProps {
type: FlashState['type'];
type: 'decompressing' | 'flashing' | 'verifying';
active: boolean;
percentage: number;
position: number;
@@ -58,8 +58,6 @@ const colors = {
decompressing: '#00aeef',
flashing: '#da60ff',
verifying: '#1ac135',
downloading: '#00aeef',
default: '#00aeef',
} as const;
const CancelButton = styled(({ type, onClick, ...props }) => {
@@ -80,7 +78,6 @@ const CancelButton = styled(({ type, onClick, ...props }) => {
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
public render() {
const type = this.props.type || 'default';
const percentage = this.props.percentage;
const warning = this.props.warning;
const { status, position } = fromFlashState({
@@ -88,6 +85,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
percentage,
position: this.props.position,
});
const type = this.props.type || 'default';
if (this.props.active) {
return (
<>

View File

@@ -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 = {};

View File

@@ -15,6 +15,7 @@
*/
import * as electron from 'electron';
import * as _ from 'lodash';
import * as React from 'react';
import * as packageJSON from '../../../../../package.json';
@@ -93,8 +94,8 @@ export class SafeWebview extends React.PureComponent<
);
this.entryHref = url.href;
// Events steal 'this'
this.didFailLoad = this.didFailLoad.bind(this);
this.didGetResponseDetails = this.didGetResponseDetails.bind(this);
this.didFailLoad = _.bind(this.didFailLoad, this);
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
// Make a persistent electron session for the webview
this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, {
// Disable the cache for the session such that new content shows up when refreshing

View File

@@ -16,9 +16,8 @@
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';
@@ -26,50 +25,39 @@ 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();
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',
},
{
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: 'validateWriteOnSuccess',
label: 'Validate write on success',
},
{
];
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
list.push({
name: 'updatesEnabled',
label: 'Auto-updates enabled',
hide: ['rpm', 'deb'].includes(packageType),
},
];
});
}
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(() => {
@@ -90,25 +78,14 @@ 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 (
@@ -122,18 +99,24 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
>
<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}>System Information</Txt>
<InfoBox label="UUID" value={UUID.substr(0, 7)} />
</Flex>
)}
<Flex
mt={18}
alignItems="center"

View File

@@ -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';
@@ -25,7 +27,16 @@ import { GPTPartition, MBRPartition } from 'partitioninfo';
import * as path from 'path';
import * as prettyBytes from 'pretty-bytes';
import * as React from 'react';
import { Flex, ButtonProps, Modal as SmallModal, Txt } from 'rendition';
import {
Flex,
ButtonProps,
Modal as SmallModal,
Txt,
Card as BaseCard,
Input,
Spinner,
Link,
} from 'rendition';
import styled from 'styled-components';
import * as errors from '../../../../shared/errors';
@@ -40,21 +51,65 @@ import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drive
import {
ChangeButton,
DetailsText,
Modal,
StepButton,
StepNameButton,
ScrollableFlex,
} from '../../styled-components';
import { colors } from '../../theme';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import URLSelector from '../url-selector/url-selector';
import { SVGIcon } from '../svg-icon/svg-icon';
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';
const recentUrlImagesKey = 'recentUrlImages';
function normalizeRecentUrlImages(urls: any[]): URL[] {
if (!Array.isArray(urls)) {
urls = [];
}
urls = urls
.map((url) => {
try {
return new URL(url);
} catch (error: any) {
// Invalid URL, skip
}
})
.filter((url) => url !== undefined);
urls = _.uniqBy(urls, (url) => url.href);
return urls.slice(urls.length - 5);
}
function getRecentUrlImages(): URL[] {
let urls = [];
try {
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
} catch {
// noop
}
return normalizeRecentUrlImages(urls);
}
function setRecentUrlImages(urls: URL[]) {
const normalized = normalizeRecentUrlImages(urls.map((url: URL) => url.href));
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
}
const isURL = (imagePath: string) =>
imagePath.startsWith('https://') || imagePath.startsWith('http://');
const Card = styled(BaseCard)`
hr {
margin: 5px 0;
}
`;
// TODO move these styles to rendition
const ModalText = styled.p`
a {
@@ -67,10 +122,11 @@ const ModalText = styled.p`
`;
function getState() {
const image = selectionState.getImage();
return {
hasImage: selectionState.hasImage(),
imageName: selectionState.getImageName(),
imageSize: selectionState.getImageSize(),
imageName: image?.name,
imageSize: image?.size,
};
}
@@ -78,6 +134,132 @@ function isString(value: any): value is string {
return typeof value === 'string';
}
const URLSelector = ({
done,
cancel,
}: {
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();
setRecentImages(recentUrlImages);
};
fetchRecentUrlImages();
}, []);
return (
<Modal
cancel={cancel}
primaryButtonProps={{
disabled: loading || !imageURL,
}}
action={loading ? <Spinner /> : 'OK'}
done={async () => {
setLoading(true);
const urlStrings = recentImages.map((url: URL) => url.href);
const normalizedRecentUrls = normalizeRecentUrlImages([
...urlStrings,
imageURL,
]);
setRecentUrlImages(normalizedRecentUrls);
const auth = username ? { username, password } : undefined;
await done(imageURL, auth);
}}
>
<Flex flexDirection="column">
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
<Txt mb="10px" fontSize="24px">
Use Image URL
</Txt>
<Input
value={imageURL}
placeholder="Enter a valid URL"
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}>Authentication</Txt>
</Flex>
</Link>
{showBasicAuth && (
<React.Fragment>
<Input
mb={15}
value={username}
placeholder="Enter username"
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setUsername(evt.target.value)
}
/>
<Input
value={password}
placeholder="Enter password"
type="password"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setPassword(evt.target.value)
}
/>
</React.Fragment>
)}
</Flex>
{recentImages.length > 0 && (
<Flex flexDirection="column" height="78.6%">
<Txt fontSize={18}>Recent</Txt>
<ScrollableFlex flexDirection="column">
<Card
p="10px 15px"
rows={recentImages
.map((recent) => (
<Txt
key={recent.href}
onClick={() => {
setImageURL(recent.href);
}}
style={{
overflowWrap: 'break-word',
}}
>
{recent.pathname.split('/').pop()} - {recent.href}
</Txt>
))
.reverse()}
/>
</ScrollableFlex>
</Flex>
)}
</Flex>
</Modal>
);
};
interface Flow {
icon?: JSX.Element;
onClick: (evt: React.MouseEvent) => void;
@@ -133,6 +315,7 @@ export interface SourceMetadata extends sourceDestination.Metadata {
drive?: DrivelistDrive;
extension?: string;
archiveExtension?: string;
auth?: Authentication;
}
interface SourceSelectorProps {
@@ -148,6 +331,13 @@ interface SourceSelectorState {
showURLSelector: boolean;
showDriveSelector: boolean;
defaultFlowActive: boolean;
imageSelectorOpen: boolean;
imageLoading: boolean;
}
interface Authentication {
username: string;
password: string;
}
export class SourceSelector extends React.Component<
@@ -165,6 +355,8 @@ export class SourceSelector extends React.Component<
showURLSelector: false,
showDriveSelector: false,
defaultFlowActive: true,
imageSelectorOpen: false,
imageLoading: false,
};
// Bind `this` since it's used in an event's callback
@@ -185,25 +377,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() {
@@ -217,6 +436,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 {
@@ -228,7 +448,10 @@ 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',
selected,
@@ -246,7 +469,7 @@ export class SourceSelector extends React.Component<
},
});
}
source = await this.createSource(selected, SourceType);
source = await this.createSource(selected, SourceType, auth);
if (cancelled) {
return;
@@ -263,7 +486,7 @@ 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: {
@@ -272,7 +495,7 @@ export class SourceSelector extends React.Component<
},
});
}
} catch (error) {
} catch (error: any) {
this.handleError(
'Error opening source',
sourcePath,
@@ -282,11 +505,20 @@ export class SourceSelector extends React.Component<
} finally {
try {
await source.close();
} catch (error) {
} catch (error: any) {
// Noop
}
}
} else {
if (selected.partitionTableType === null) {
analytics.logEvent('Missing partition table', { selected });
this.setState({
warning: {
message: messages.warning.driveMissingPartitionTable(),
title: 'Missing partition table',
},
});
}
metadata = {
path: selected.device,
displayName: selected.displayName,
@@ -298,6 +530,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
@@ -352,6 +585,7 @@ export class SourceSelector extends React.Component<
private async openImageSelector() {
analytics.logEvent('Open image selector');
this.setState({ imageSelectorOpen: true });
try {
const imagePath = await osDialog.selectImage();
@@ -362,8 +596,10 @@ 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 });
}
}
@@ -402,7 +638,7 @@ export class SourceSelector extends React.Component<
private showSelectedImageDetails() {
analytics.logEvent('Show selected image tooltip', {
imagePath: selectionState.getImagePath(),
imagePath: selectionState.getImage()?.path,
});
this.setState({
@@ -414,10 +650,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);
@@ -455,16 +702,18 @@ 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}
@@ -473,13 +722,14 @@ export class SourceSelector extends React.Component<
Remove
</ChangeButton>
)}
{!_.isNil(imageSize) && (
{!_.isNil(imageSize) && !imageLoading && (
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
)}
</>
) : (
<>
<FlowSelector
disabled={this.state.imageSelectorOpen}
primary={this.state.defaultFlowActive}
key="Flash from file"
flow={{
@@ -516,6 +766,9 @@ 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" />{' '}
@@ -564,7 +817,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) {
@@ -574,6 +827,7 @@ export class SourceSelector extends React.Component<
({ promise, cancel: cancelURLSelection } = this.selectSource(
imageURL,
sourceDestination.Http,
auth,
));
await promise;
}
@@ -586,24 +840,35 @@ export class SourceSelector extends React.Component<
{showDriveSelector && (
<DriveSelector
write={false}
multipleSelection={false}
titleLabel="Select source"
emptyListLabel="Plug a source"
cancel={() => {
this.setState({
showDriveSelector: false,
});
}}
done={async (drives: DrivelistDrive[]) => {
if (drives.length) {
await this.selectSource(
drives[0],
sourceDestination.BlockDevice,
);
emptyListLabel="Plug a source drive"
emptyListIcon={<SrcSvg width="40px" />}
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,
});
}}
/>
)}

View File

@@ -24,7 +24,7 @@ import {
} from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes';
import { getSelectedDrives } from '../../models/selection-state';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import {
ChangeButton,
DetailsText,
@@ -80,9 +80,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
if (targets.length === 1) {
const target = targets[0];
const warnings = getDriveImageCompatibilityStatuses(target).map(
getDriveWarning,
);
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
@@ -106,9 +108,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
if (targets.length > 1) {
const targetsTemplate = [];
for (const target of targets) {
const warnings = getDriveImageCompatibilityStatuses(target).map(
getDriveWarning,
);
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
targetsTemplate.push(
<DetailsText
key={target.device}

View File

@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { scanner } from 'etcher-sdk';
import * as React from 'react';
import { Flex, Txt } from 'rendition';
@@ -28,14 +27,16 @@ import {
getSelectedDrives,
deselectDrive,
selectDrive,
deselectAllDrives,
} from '../../models/selection-state';
import * as settings from '../../models/settings';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
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';
export const getDriveListLabel = () => {
return getSelectedDrives()
@@ -45,12 +46,7 @@ export const getDriveListLabel = () => {
.join('\n');
};
const shouldShowDrivesButton = () => {
return !settings.getSync('disableExplicitDriveSelection');
};
const getDriveSelectionStateSlice = () => ({
showDrivesButton: shouldShowDrivesButton(),
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
@@ -59,13 +55,14 @@ const getDriveSelectionStateSlice = () => ({
export const TargetSelectorModal = (
props: Omit<
DriveSelectorProps,
'titleLabel' | 'emptyListLabel' | 'multipleSelection'
'titleLabel' | 'emptyListLabel' | 'multipleSelection' | 'emptyListIcon'
>,
) => (
<DriveSelector
multipleSelection={true}
titleLabel="Select target"
emptyListLabel="Plug a target drive"
emptyListIcon={<TgtSvg width="40px" />}
showWarnings={true}
selectedList={getSelectedDrives()}
updateSelectedList={getSelectedDrives}
@@ -73,9 +70,7 @@ export const TargetSelectorModal = (
/>
);
export const selectAllTargets = (
modalTargets: scanner.adapters.DrivelistDrive[],
) => {
export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
const selectedDrivesFromState = getSelectedDrives();
const deselected = selectedDrivesFromState.filter(
(drive) =>
@@ -114,13 +109,11 @@ export const TargetSelector = ({
flashing,
}: TargetSelectorProps) => {
// TODO: inject these from redux-connector
const [
{ showDrivesButton, driveListLabel, targets },
setStateSlice,
] = React.useState(getDriveSelectionStateSlice());
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
false,
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
getDriveSelectionStateSlice(),
);
const [showTargetSelectorModal, setShowTargetSelectorModal] =
React.useState(false);
React.useEffect(() => {
return observe(() => {
@@ -141,7 +134,7 @@ export const TargetSelector = ({
<TargetSelectorButton
disabled={disabled}
show={!hasDrive && showDrivesButton}
show={!hasDrive}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
@@ -168,11 +161,31 @@ export const TargetSelector = ({
{showTargetSelectorModal && (
<TargetSelectorModal
cancel={() => setShowTargetSelectorModal(false)}
done={(modalTargets) => {
selectAllTargets(modalTargets);
write={true}
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>

View File

@@ -1,167 +0,0 @@
import { uniqBy } from 'lodash';
import * as React from 'react';
import Checkbox from 'rendition/dist_esm5/components/Checkbox';
import { Flex } from 'rendition/dist_esm5/components/Flex';
import Input from 'rendition/dist_esm5/components/Input';
import Link from 'rendition/dist_esm5/components/Link';
import RadioButton from 'rendition/dist_esm5/components/RadioButton';
import Txt from 'rendition/dist_esm5/components/Txt';
import * as settings from '../../models/settings';
import { Modal, ScrollableFlex } from '../../styled-components';
import { openDialog } from '../../os/dialog';
import { startEllipsis } from '../../utils/start-ellipsis';
const RECENT_URL_IMAGES_KEY = 'recentUrlImages';
const SAVE_IMAGE_AFTER_FLASH_KEY = 'saveUrlImage';
const SAVE_IMAGE_AFTER_FLASH_PATH_KEY = 'saveUrlImageTo';
function normalizeRecentUrlImages(urls: any[]): URL[] {
if (!Array.isArray(urls)) {
urls = [];
}
urls = urls
.map((url) => {
try {
return new URL(url);
} catch (error) {
// Invalid URL, skip
}
})
.filter((url) => url !== undefined);
urls = uniqBy(urls, (url) => url.href);
return urls.slice(-5);
}
function getRecentUrlImages(): URL[] {
let urls = [];
try {
urls = JSON.parse(localStorage.getItem(RECENT_URL_IMAGES_KEY) || '[]');
} catch {
// noop
}
return normalizeRecentUrlImages(urls);
}
function setRecentUrlImages(urls: string[]) {
localStorage.setItem(RECENT_URL_IMAGES_KEY, JSON.stringify(urls));
}
export const URLSelector = ({
done,
cancel,
}: {
done: (imageURL: string) => void;
cancel: () => void;
}) => {
const [imageURL, setImageURL] = React.useState('');
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
const [loading, setLoading] = React.useState(false);
const [saveImage, setSaveImage] = React.useState(false);
const [saveImagePath, setSaveImagePath] = React.useState('');
React.useEffect(() => {
const fetchRecentUrlImages = async () => {
const recentUrlImages: URL[] = await getRecentUrlImages();
setRecentImages(recentUrlImages);
};
const getSaveImageSettings = async () => {
const saveUrlImage: boolean = await settings.get(
SAVE_IMAGE_AFTER_FLASH_KEY,
);
const saveUrlImageToPath: string = await settings.get(
SAVE_IMAGE_AFTER_FLASH_PATH_KEY,
);
setSaveImage(saveUrlImage);
setSaveImagePath(saveUrlImageToPath);
};
fetchRecentUrlImages();
getSaveImageSettings();
}, []);
return (
<Modal
title="Use Image URL"
cancel={cancel}
primaryButtonProps={{
className: loading || !imageURL ? 'disabled' : '',
}}
done={async () => {
setLoading(true);
const urlStrings = recentImages
.map((url: URL) => url.href)
.concat(imageURL);
setRecentUrlImages(urlStrings);
await done(imageURL);
}}
>
<Flex flexDirection="column">
<Flex mb="16px" width="100%" height="auto" flexDirection="column">
<Input
value={imageURL}
placeholder="Enter a valid URL"
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setImageURL(evt.target.value)
}
/>
<Flex alignItems="flex-end">
<Checkbox
mt="16px"
checked={saveImage}
onChange={(evt) => {
const value = evt.target.checked;
setSaveImage(value);
settings
.set(SAVE_IMAGE_AFTER_FLASH_KEY, value)
.then(() => setSaveImage(value));
}}
label={<>Save file to:&nbsp;</>}
/>
<Link
disabled={!saveImage}
onClick={async () => {
if (saveImage) {
const folder = await openDialog('openDirectory');
if (folder) {
await settings.set(SAVE_IMAGE_AFTER_FLASH_PATH_KEY, folder);
setSaveImagePath(folder);
}
}
}}
>
{startEllipsis(saveImagePath, 20)}
</Link>
</Flex>
</Flex>
{recentImages.length > 0 && (
<Flex flexDirection="column" height="58%">
<Txt fontSize={18} mb="10px">
Recent
</Txt>
<ScrollableFlex flexDirection="column" p="0">
{recentImages
.map((recent, i) => (
<RadioButton
mb={i !== 0 ? '6px' : '0'}
key={recent.href}
checked={imageURL === recent.href}
label={`${recent.pathname.split('/').pop()} - ${
recent.href
}`}
onChange={() => {
setImageURL(recent.href);
}}
style={{
overflowWrap: 'break-word',
}}
/>
))
.reverse()}
</ScrollableFlex>
</Flex>
)}
</Flex>
</Modal>
);
};
export default URLSelector;

View File

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

View File

@@ -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>

View File

@@ -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.send('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.send('enable-screensaver');
}
export function setDevicePaths(devicePaths: string[]) {
@@ -75,25 +80,29 @@ export function setDevicePaths(devicePaths: string[]) {
});
}
export function addFailedDevicePath({
export function addFailedDeviceError({
device,
error,
}: {
device: sdk.scanner.adapters.DrivelistDrive;
device: DrivelistDrive;
error: Error;
}) {
const failedDevicePathsMap = new Map(
store.getState().toJS().failedDevicePaths,
const failedDeviceErrorsMap = new Map(
store.getState().toJS().failedDeviceErrors,
);
failedDevicePathsMap.set(device.device, {
if (failedDeviceErrorsMap.has(device.device)) {
// Only store the first error
return;
}
failedDeviceErrorsMap.set(device.device, {
description: device.description,
device: device.device,
devicePath: device.devicePath,
...error,
});
store.dispatch({
type: Actions.SET_FAILED_DEVICE_PATHS,
data: Array.from(failedDevicePathsMap),
type: Actions.SET_FAILED_DEVICE_ERRORS,
data: Array.from(failedDeviceErrorsMap),
});
}

View File

@@ -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.failedDevicePaths.map(
([devicePath]: [string]) => devicePath,
const failedDevicePaths = s.failedDeviceErrors.map(
([, { 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 }));
}
}

View File

@@ -72,26 +72,6 @@ export function getImage(): SourceMetadata | undefined {
return store.getState().toJS().selection.image;
}
export function getImagePath(): string | undefined {
return store.getState().toJS().selection.image?.path;
}
export function getImageSize(): number | undefined {
return store.getState().toJS().selection.image?.size;
}
export function getImageName(): string | undefined {
return store.getState().toJS().selection.image?.name;
}
export function getImageLogo(): string | undefined {
return store.getState().toJS().selection.image?.logo;
}
export function getImageSupportUrl(): string | undefined {
return store.getState().toJS().selection.image?.supportUrl;
}
/**
* @summary Check if there is a selected drive
*/

View File

@@ -38,23 +38,20 @@ export const DEFAULT_HEIGHT = 480;
* - `~/Library/Application Support/etcher` on macOS
* See https://electronjs.org/docs/api/app#appgetpathname
*
* NOTE: The ternary is due to this module being loaded both,
* Electron's main process and renderer process
* NOTE: We use the remote property when this module
* is loaded in the Electron's renderer process
*/
const app = electron.app || electron.remote.app;
const USER_DATA_DIR = app.getPath('userData');
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
const DOWNLOADS_DIR = app.getPath('downloads');
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 {
@@ -80,14 +77,10 @@ export async function writeConfigFile(
const DEFAULT_SETTINGS: _.Dictionary<any> = {
errorReporting: true,
unmountOnSuccess: true,
validateWriteOnSuccess: true,
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
desktopNotifications: true,
autoBlockmapping: true,
decompressFirst: true,
saveUrlImage: false,
saveUrlImageTo: DOWNLOADS_DIR,
};
const settings = _.cloneDeep(DEFAULT_SETTINGS);
@@ -111,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;

View File

@@ -16,6 +16,7 @@
import * as Immutable from 'immutable';
import * as _ from 'lodash';
import { basename } from 'path';
import * as redux from 'redux';
import { v4 as uuidV4 } from 'uuid';
@@ -62,7 +63,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
},
isFlashing: false,
devicePaths: [],
failedDevicePaths: [],
failedDeviceErrors: [],
flashResults: {},
flashState: {
active: 0,
@@ -79,7 +80,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
*/
export enum Actions {
SET_DEVICE_PATHS,
SET_FAILED_DEVICE_PATHS,
SET_FAILED_DEVICE_ERRORS,
SET_AVAILABLE_TARGETS,
SET_FLASH_STATE,
RESET_FLASH_STATE,
@@ -133,11 +134,16 @@ function storeReducer(
});
}
// Drives order is a list of devicePaths
const drivesOrder = settings.getSync('drivesOrder') ?? [];
drives = _.sortBy(drives, [
// System drives last
(d) => !!d.isSystem,
// Devices with no devicePath first (usbboot)
(d) => !!d.devicePath,
// Sort as defined in the drivesOrder setting if there is one (only for Linux with udev)
(d) => drivesOrder.indexOf(basename(d.devicePath || '')),
// Then sort by devicePath (only available on Linux with udev) or device
(d) => d.devicePath || d.device,
]);
@@ -169,7 +175,7 @@ function storeReducer(
);
const shouldAutoselectAll = Boolean(
settings.getSync('disableExplicitDriveSelection'),
settings.getSync('autoSelectAllDrives'),
);
const AUTOSELECT_DRIVE_COUNT = 1;
const nonStaleSelectedDevices = nonStaleNewState
@@ -191,18 +197,13 @@ function storeReducer(
drives,
(accState, drive) => {
if (
_.every([
constraints.isDriveValid(drive, image),
constraints.isDriveSizeRecommended(drive, image),
// We don't want to auto-select large drives
!constraints.isDriveSizeLarge(drive),
// We don't want to auto-select system drives,
// even when "unsafe mode" is enabled
!constraints.isSystemDrive(drive),
]) ||
(shouldAutoselectAll && constraints.isDriveValid(drive, image))
constraints.isDriveValid(drive, image) &&
!drive.isReadOnly &&
constraints.isDriveSizeRecommended(drive, image) &&
// We don't want to auto-select large drives execpt is autoSelectAllDrives is true
(!constraints.isDriveSizeLarge(drive) || shouldAutoselectAll) &&
// We don't want to auto-select system drives
!constraints.isSystemDrive(drive)
) {
// Auto-select this drive
return storeReducer(accState, {
@@ -269,7 +270,7 @@ function storeReducer(
.set('flashState', DEFAULT_STATE.get('flashState'))
.set('flashResults', DEFAULT_STATE.get('flashResults'))
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
.set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths'))
.set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
.set(
'lastAverageFlashingSpeed',
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
@@ -516,8 +517,8 @@ function storeReducer(
return state.set('devicePaths', action.data);
}
case Actions.SET_FAILED_DEVICE_PATHS: {
return state.set('failedDevicePaths', action.data);
case Actions.SET_FAILED_DEVICE_ERRORS: {
return state.set('failedDeviceErrors', action.data);
}
default: {

View File

@@ -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,

View File

@@ -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);

View File

@@ -15,9 +15,8 @@
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as electron from 'electron';
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import { Dictionary } from 'lodash';
import * as ipc from 'node-ipc';
import * as os from 'os';
import * as path from 'path';
@@ -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
@@ -133,6 +129,14 @@ function writerEnv() {
interface FlashResults {
skip?: boolean;
cancelled?: boolean;
results?: {
bytesWritten: number;
devices: {
failed: number;
successful: number;
};
errors: Error[];
};
}
async function performWrite(
@@ -143,14 +147,7 @@ async function performWrite(
let cancelled = false;
let skip = false;
ipc.serve();
const {
unmountOnSuccess,
validateWriteOnSuccess,
autoBlockmapping,
decompressFirst,
saveUrlImage,
saveUrlImageTo,
} = await settings.getAll();
const { autoBlockmapping, decompressFirst } = await settings.getAll();
return await new Promise((resolve, reject) => {
ipc.server.on('error', (error) => {
terminateServer();
@@ -169,22 +166,22 @@ async function performWrite(
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess,
validateWriteOnSuccess,
};
ipc.server.on('fail', ({ device, error }) => {
if (device.devicePath) {
flashState.addFailedDevicePath({ device, error });
flashState.addFailedDeviceError({ device, error });
}
handleErrorLogging(error, analyticsData);
});
ipc.server.on('done', (event) => {
event.results.errors = _.map(event.results.errors, (data) => {
return errors.fromJSON(data);
});
_.merge(flashResults, event);
event.results.errors = event.results.errors.map(
(data: Dictionary<any> & { message: string }) => {
return errors.fromJSON(data);
},
);
flashResults.results = event.results;
});
ipc.server.on('abort', () => {
@@ -204,19 +201,15 @@ async function performWrite(
image,
destinations: drives,
SourceType: image.SourceType.name,
validateWriteOnSuccess,
autoBlockmapping,
unmountOnSuccess,
decompressFirst,
saveUrlImage,
saveUrlImageTo,
});
});
const argv = writerArgv();
ipc.server.on('start', async () => {
console.log(`Elevating command: ${_.join(argv, ' ')}`);
console.log(`Elevating command: ${argv.join(' ')}`);
const env = writerEnv();
try {
const results = await permissions.elevateCommand(argv, {
@@ -225,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) {
@@ -238,11 +231,11 @@ async function performWrite(
}
console.log('Flash results', flashResults);
// This likely means the child died halfway through
// The flash wasn't cancelled and we didn't get a 'done' event
if (
!flashResults.cancelled &&
!flashResults.skip &&
!_.get(flashResults, ['results', 'bytesWritten'])
flashResults.results === undefined
) {
reject(
errors.createUserError({
@@ -275,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[],
);
@@ -287,17 +280,18 @@ export async function flash(
uuid: flashState.getFlashUuid(),
status: 'started',
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
};
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 = {
@@ -338,13 +332,11 @@ export async function cancel(type: string) {
const status = type.toLowerCase();
const drives = selectionState.getSelectedDevices();
const analyticsData = {
image: selectionState.getImagePath(),
image: selectionState.getImage()?.path,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
status,
};
analytics.logEvent('Cancel', analyticsData);
@@ -357,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);
}
}

View File

@@ -22,7 +22,7 @@ export interface FlashState {
percentage?: number;
speed: number;
position: number;
type?: 'decompressing' | 'flashing' | 'verifying' | 'downloading';
type?: 'decompressing' | 'flashing' | 'verifying';
}
export function fromFlashState({
@@ -62,12 +62,6 @@ export function fromFlashState({
} else {
return { status: 'Finishing...' };
}
} else if (type === 'downloading') {
if (percentage == null) {
return { status: 'Downloading...' };
} else if (percentage < 100) {
return { position: `${percentage}%`, status: 'Downloading...' };
}
}
return { status: 'Failed' };
}

View File

@@ -27,7 +27,7 @@ async function mountSourceDrive() {
if (sourceDrivePath) {
try {
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
} catch (error) {
} catch (error: any) {
// noop
}
}
@@ -40,12 +40,6 @@ async function mountSourceDrive() {
* Notice that by image, we mean *.img/*.iso/*.zip/etc files.
*/
export async function selectImage(): Promise<string | undefined> {
return await openDialog();
}
export async function openDialog(
type: 'openFile' | 'openDirectory' = 'openFile',
) {
await mountSourceDrive();
const options: electron.OpenDialogOptions = {
// This variable is set when running in GNU/Linux from
@@ -56,26 +50,23 @@ export async function openDialog(
//
// See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7
defaultPath: process.env.OWD,
properties: [type, 'treatPackageAsDirectory'],
filters:
type === 'openFile'
? [
{
name: 'OS Images',
extensions: SUPPORTED_EXTENSIONS,
},
{
name: 'All',
extensions: ['*'],
},
]
: undefined,
properties: ['openFile', 'treatPackageAsDirectory'],
filters: [
{
name: 'OS Images',
extensions: SUPPORTED_EXTENSIONS,
},
{
name: 'All',
extensions: ['*'],
},
],
};
const currentWindow = electron.remote.getCurrentWindow();
const [path] = (
const [file] = (
await electron.remote.dialog.showOpenDialog(currentWindow, options)
).filePaths;
return path;
return file;
}
/**

View File

@@ -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',

View File

@@ -59,6 +59,27 @@ const getErrorMessageFromCode = (errorCode: string) => {
return '';
};
function notifySuccess(
iconPath: string,
basename: string,
drives: any,
devices: { successful: number; failed: number },
) {
notification.send(
'Flash complete!',
messages.info.flashComplete(basename, drives, devices),
iconPath,
);
}
function notifyFailure(iconPath: string, basename: string, drives: any) {
notification.send(
'Oops! Looks like the flash failed.',
messages.error.flashFailure(basename, drives),
iconPath,
);
}
async function flashImageToDrive(
isFlashing: boolean,
goToSuccess: () => void,
@@ -84,20 +105,20 @@ async function flashImageToDrive(
if (!flashState.wasLastFlashCancelled()) {
const {
results = { devices: { successful: 0, failed: 0 } },
skip,
cancelled,
} = flashState.getFlashResults();
notification.send(
'Flash complete!',
messages.info.flashComplete(basename, drives as any, results.devices),
iconPath,
);
if (!skip && !cancelled) {
if (results.devices.successful > 0) {
notifySuccess(iconPath, basename, drives, results.devices);
} else {
notifyFailure(iconPath, basename, drives);
}
}
goToSuccess();
}
} catch (error) {
notification.send(
'Oops! Looks like the flash failed.',
messages.error.flashFailure(path.basename(image.path), drives),
iconPath,
);
} catch (error: any) {
notifyFailure(iconPath, basename, drives);
let errorMessage = getErrorMessageFromCode(error.code);
if (!errorMessage) {
error.image = basename;
@@ -135,6 +156,7 @@ interface FlashStepProps {
failed: number;
speed?: number;
eta?: number;
width: string;
}
export interface DriveWithWarnings extends constraints.DrivelistDrive {
@@ -199,7 +221,11 @@ export class FlashStep extends React.PureComponent<
const drives = selection.getSelectedDrives().map((drive) => {
return {
...drive,
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
statuses: constraints.getDriveImageCompatibilityStatuses(
drive,
undefined,
true,
),
};
});
if (drives.length === 0 || this.props.isFlashing) {
@@ -237,6 +263,7 @@ export class FlashStep extends React.PureComponent<
<Flex
flexDirection="column"
alignItems="start"
width={this.props.width}
style={this.props.style}
>
<FlashSvg
@@ -308,6 +335,7 @@ export class FlashStep extends React.PureComponent<
)}
{this.state.showDriveSelectorModal && (
<TargetSelectorModal
write={true}
cancel={() => this.setState({ showDriveSelectorModal: false })}
done={(modalTargets) => {
selectAllTargets(modalTargets);

View File

@@ -48,7 +48,6 @@ import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { colors } from '../../theme';
const Icon = styled(BaseIcon)`
margin-right: 20px;
@@ -88,7 +87,9 @@ const StepBorder = styled.div<{
position: relative;
height: 2px;
background-color: ${(props) =>
props.disabled ? colors.dark.disabled.foreground : colors.dark.foreground};
props.disabled
? props.theme.colors.dark.disabled.foreground
: props.theme.colors.dark.foreground};
width: 120px;
top: 19px;
@@ -131,12 +132,13 @@ export class MainPage extends React.Component<
}
private stateHelper(): MainPageStateFromStore {
const image = selectionState.getImage();
return {
isFlashing: flashState.isFlashing(),
hasImage: selectionState.hasImage(),
hasDrive: selectionState.hasDrive(),
imageLogo: selectionState.getImageLogo(),
imageSize: selectionState.getImageSize(),
imageLogo: image?.logo,
imageSize: image?.size,
imageName: getImageBasename(selectionState.getImage()),
driveTitle: getDrivesTitle(),
driveLabel: getDriveListLabel(),
@@ -237,6 +239,7 @@ export class MainPage extends React.Component<
)}
<FlashStep
width={this.state.isWebviewShowing ? '220px' : '200px'}
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing}
@@ -273,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%" />
@@ -301,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') && (
@@ -309,14 +312,14 @@ export class MainPage extends React.Component<
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
onClick={() =>
openExternal(
selectionState.getImageSupportUrl() ||
selectionState.getImage()?.supportUrl ||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
)
}
tabIndex={6}
style={{
// Make touch events click instead of dragging
'-webkit-app-region': 'no-drag',
WebkitAppRegion: 'no-drag',
}}
/>
)}

10
lib/gui/app/renderer.ts Normal file
View File

@@ -0,0 +1,10 @@
// @ts-nocheck
import { main } from './app';
if (module.hot) {
module.hot.accept('./app', () => {
main();
});
}
main();

View File

@@ -126,47 +126,30 @@ const modalFooterShadowCss = css`
export const Modal = styled(({ style, children, ...props }) => {
return (
<Provider
theme={_.merge({}, theme, {
header: {
height: '50px',
<ModalBase
position="top"
width="97vw"
cancelButtonProps={{
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
},
layer: {
extend: () => `
${theme.layer.extend()}
> div:last-child {
top: 0;
}
`,
},
})}
}}
style={{
height: '87.5vh',
...style,
}}
{...props}
>
<ModalBase
position="top"
width="97vw"
cancelButtonProps={{
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
},
}}
style={{
height: '87.5vh',
...style,
}}
{...props}
>
<ScrollableFlex flexDirection="column" width="100%" height="90%">
{...children}
</ScrollableFlex>
</ModalBase>
</Provider>
<ScrollableFlex flexDirection="column" width="100%" height="90%">
{...children}
</ScrollableFlex>
</ModalBase>
);
})`
> div {
padding: 0;
height: 100%;
height: 99%;
> div:first-child {
height: 81%;

View File

@@ -71,7 +71,11 @@ export const colors = {
const font = 'SourceSansPro';
export const theme = _.merge({}, Theme, {
colors,
font,
header: {
height: '40px',
},
global: {
font: {
family: font,
@@ -96,6 +100,7 @@ export const theme = _.merge({}, Theme, {
font-size: 16px;
&& {
width: 200px;
height: 48px;
}

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2020 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @summary Truncate text from the start with an ellipsis
*/
export function startEllipsis(input: string, limit: number): string {
// Do nothing, the string doesn't need truncation.
if (input.length <= limit) {
return input;
}
const lastPart = input.slice(input.length - limit, input.length);
return `${lastPart}`;
}

18
lib/gui/assets/src.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 39 90" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g transform="translate(-380 -166)">
<g transform="translate(380 166)">
<path d="m30.88 39.87h-23.363v23.209c0 0.6909 0.56062 1.251 1.251 1.251h20.861c0.69114 0 1.251-0.55986 1.251-1.251v-23.209zm-22.363 0.9999h21.363l4e-4 22.209c0 0.13886-0.11214 0.251-0.251 0.251h-20.861l-0.057452-0.0066403c-0.11075-0.026055-0.19355-0.12572-0.19355-0.24436l-4e-4 -22.209z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m16.558 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m25.97 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m37.398 5.418v30.534c0 2.43-1.988 4.418-4.418 4.418h-27.562c-2.43 0-4.418-1.988-4.418-4.418v-30.534c0-2.43 1.988-4.418 4.418-4.418h27.562c2.43 0 4.418 1.988 4.418 4.418" fill="#2A506F"/>
<path d="m32.98-5.6843e-14h-27.562c-2.9823 0-5.418 2.4357-5.418 5.418v30.534c0 2.9823 2.4357 5.418 5.418 5.418h27.562c2.9823 0 5.418-2.4357 5.418-5.418v-30.534c0-2.9823-2.4357-5.418-5.418-5.418zm-27.562 2h27.562c1.8777 0 3.418 1.5403 3.418 3.418v30.534c0 1.8777-1.5403 3.418-3.418 3.418h-27.562c-1.8777 0-3.418-1.5403-3.418-3.418v-30.534c0-1.8777 1.5403-3.418 3.418-3.418z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m19.147 73.551c0.24546 0 0.44961 0.17688 0.49194 0.41012l0.0080557 0.089876v14.882c0 0.27614-0.22386 0.5-0.5 0.5-0.24546 0-0.44961-0.17688-0.49194-0.41012l-0.0080557-0.089876v-14.882c0-0.27614 0.22386-0.5 0.5-0.5z" fill="#2A506F" fill-rule="nonzero"/>
<line x1="19.147" x2="14.532" y1="88.933" y2="84.214" stroke="#2A506F" stroke-linecap="round"/>
<line x1="19.147" x2="23.866" y1="88.933" y2="84.318" stroke="#2A506F" stroke-linecap="round"/>
<path d="m14.007 26.177c0.51076 0 0.96749-0.071211 1.3702-0.21363s0.74649-0.33887 1.0313-0.58934 0.50339-0.54268 0.65564-0.87664 0.22837-0.69247 0.22837-1.0755c0-0.3536-0.051567-0.66546-0.1547-0.93557s-0.2431-0.50585-0.4199-0.7072-0.38798-0.37816-0.63354-0.5304-0.50585-0.2873-0.78087-0.40517l-1.3702-0.58934c-0.19645-0.078578-0.38798-0.16452-0.5746-0.25783s-0.35851-0.20136-0.51567-0.32413-0.28239-0.2652-0.3757-0.42727-0.13997-0.36097-0.13997-0.5967c0-0.442 0.16452-0.78824 0.49357-1.0387s0.76368-0.3757 1.3039-0.3757c0.45182 0 0.85699 0.081034 1.2155 0.2431s0.6851 0.38552 0.97977 0.67037l0.663-0.7956c-0.34378-0.3536-0.76123-0.6409-1.2523-0.8619s-1.0264-0.3315-1.6059-0.3315c-0.442 0-0.84717 0.063845-1.2155 0.19153s-0.68756 0.30695-0.95767 0.53777-0.48129 0.50339-0.63354 0.8177-0.22837 0.65318-0.22837 1.0166c0 0.3536 0.058934 0.66546 0.1768 0.93557s0.27011 0.50339 0.45674 0.69984 0.3978 0.36342 0.63354 0.50094 0.46656 0.25538 0.69247 0.3536l1.3849 0.60407c0.22591 0.10804 0.43709 0.21118 0.63354 0.3094s0.36588 0.20872 0.5083 0.3315 0.25538 0.27011 0.33887 0.442 0.12523 0.38061 0.12523 0.62617c0 0.47147-0.1768 0.85208-0.5304 1.1418s-0.84963 0.43464-1.4881 0.43464c-0.50094 0-0.98468-0.1105-1.4512-0.3315s-0.87173-0.51321-1.2155-0.87664l-0.73667 0.85454c0.42236 0.442 0.92329 0.79069 1.5028 1.0461s1.2081 0.38307 1.8859 0.38307zm6.2664-0.1768v-4.5968c0.24556-0.60898 0.53286-1.0362 0.8619-1.2818s0.64581-0.36834 0.9503-0.36834c0.14733 0 0.27011 0.0098223 0.36834 0.029467s0.20627 0.049111 0.32413 0.0884l0.23573-1.0608c-0.22591-0.098223-0.48129-0.14733-0.76614-0.14733-0.41254 0-0.79315 0.1326-1.1418 0.3978s-0.64581 0.62371-0.89137 1.0755h-0.0442l-0.10313-1.2965h-1.0019v7.1604h1.2081zm6.5758 0.1768c0.43218 0 0.84471-0.081034 1.2376-0.2431s0.7514-0.38552 1.0755-0.67037l-0.5304-0.81034c-0.22591 0.19645-0.47884 0.36588-0.75877 0.5083s-0.58688 0.21363-0.92084 0.21363c-0.32413 0-0.62371-0.0663-0.89874-0.1989s-0.5083-0.31922-0.69984-0.55987-0.34132-0.52795-0.44937-0.8619-0.16207-0.7072-0.16207-1.1197 0.056478-0.78824 0.16943-1.1271 0.26766-0.63108 0.4641-0.87664 0.43218-0.43464 0.7072-0.56724 0.5746-0.1989 0.89874-0.1989c0.28485 0 0.54268 0.058934 0.7735 0.1768s0.44937 0.27011 0.65564 0.45674l0.6188-0.7956c-0.25538-0.22591-0.55005-0.42236-0.884-0.58934s-0.73667-0.25047-1.2081-0.25047c-0.46165 0-0.90119 0.083489-1.3186 0.25047s-0.78333 0.41254-1.0976 0.73667-0.56478 0.71948-0.7514 1.186-0.27993 0.99942-0.27993 1.5986c0 0.58934 0.085945 1.1173 0.25783 1.5838s0.40762 0.85945 0.7072 1.1787 0.65564 0.56232 1.0682 0.7293 0.85454 0.25047 1.326 0.25047z" fill="#fff" fill-rule="nonzero"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -43,7 +43,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 +97,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) => {
@@ -133,7 +134,7 @@ async function createMainWindow() {
width,
height,
frame: !fullscreen,
useContentSize: false,
useContentSize: true,
show: false,
resizable: false,
maximizable: false,
@@ -147,6 +148,7 @@ async function createMainWindow() {
webPreferences: {
backgroundThrottling: false,
nodeIntegration: true,
contextIsolation: false,
webviewTag: true,
zoomFactor: width / defaultWidth,
enableRemoteModule: true,

View File

@@ -15,18 +15,30 @@
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as sdk from 'etcher-sdk';
import {
BlockDevice,
File,
Http,
Metadata,
SourceDestination,
} from 'etcher-sdk/build/source-destination';
import {
MultiDestinationProgress,
OnProgressFunction,
OnFailFunction,
decompressThenFlash,
DECOMPRESSED_IMAGE_PREFIX,
} from 'etcher-sdk/build/multi-write';
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import { promises as fs } from 'fs';
import * as _ from 'lodash';
import * as ipc from 'node-ipc';
import { totalmem } from 'os';
import { BlockDevice, File, Http } from 'etcher-sdk/build/source-destination';
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;
@@ -59,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);
});
@@ -74,14 +86,25 @@ async function handleError(error: Error) {
await terminate(GENERAL_ERROR);
}
interface WriteResult {
bytesWritten: number;
devices: {
export interface FlashError extends Error {
description: string;
device: string;
code: string;
}
export interface WriteResult {
bytesWritten?: number;
devices?: {
failed: number;
successful: number;
};
errors: Array<Error & { device: string }>;
sourceMetadata: sdk.sourceDestination.Metadata;
errors: FlashError[];
sourceMetadata?: Metadata;
}
export interface FlashResults extends WriteResult {
skip?: boolean;
cancelled?: boolean;
}
/**
@@ -103,19 +126,15 @@ async function writeAndValidate({
onProgress,
onFail,
}: {
source: sdk.sourceDestination.SourceDestination;
destinations: sdk.sourceDestination.BlockDevice[];
source: SourceDestination;
destinations: BlockDevice[];
verify: boolean;
autoBlockmapping: boolean;
decompressFirst: boolean;
onProgress: sdk.multiWrite.OnProgressFunction;
onFail: sdk.multiWrite.OnFailFunction;
onProgress: OnProgressFunction;
onFail: OnFailFunction;
}): Promise<WriteResult> {
const {
sourceMetadata,
failures,
bytesWritten,
} = await sdk.multiWrite.decompressThenFlash({
const { sourceMetadata, failures, bytesWritten } = await decompressThenFlash({
source,
destinations,
onFail,
@@ -139,8 +158,8 @@ async function writeAndValidate({
sourceMetadata,
};
for (const [destination, error] of failures) {
const err = error as Error & { device: string; description: string };
const drive = destination as sdk.sourceDestination.BlockDevice;
const err = error as FlashError;
const drive = destination as BlockDevice;
err.device = drive.device;
err.description = drive.description;
result.errors.push(err);
@@ -151,18 +170,10 @@ async function writeAndValidate({
interface WriteOptions {
image: SourceMetadata;
destinations: DrivelistDrive[];
unmountOnSuccess: boolean;
validateWriteOnSuccess: boolean;
autoBlockmapping: boolean;
decompressFirst: boolean;
SourceType: string;
saveUrlImage: boolean;
saveUrlImageTo: string;
}
interface ProgressState
extends Omit<sdk.multiWrite.MultiDestinationProgress, 'type'> {
type: sdk.multiWrite.MultiDestinationProgress['type'] | 'downloading';
httpRequest?: any;
}
ipc.connectTo(IPC_SERVER_ID, () => {
@@ -200,7 +211,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
* @example
* writer.on('progress', onProgress)
*/
const onProgress = (state: ProgressState) => {
const onProgress = (state: MultiDestinationProgress) => {
ipc.of[IPC_SERVER_ID].emit('state', state);
};
@@ -236,10 +247,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
* @example
* writer.on('fail', onFail)
*/
const onFail = (
destination: sdk.sourceDestination.SourceDestination,
error: Error,
) => {
const onFail = (destination: SourceDestination, error: Error) => {
ipc.of[IPC_SERVER_ID].emit('fail', {
// TODO: device should be destination
// @ts-ignore (destination.drive is private)
@@ -252,14 +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(`Validate on success: ${options.validateWriteOnSuccess}`);
log(`Auto blockmapping: ${options.autoBlockmapping}`);
log(`Decompress first: ${options.decompressFirst}`);
const dests = options.destinations.map((destination) => {
return new sdk.sourceDestination.BlockDevice({
return new BlockDevice({
drive: destination,
unmountOnSuccess: options.unmountOnSuccess,
unmountOnSuccess: true,
write: true,
direct: true,
});
@@ -278,22 +284,28 @@ ipc.connectTo(IPC_SERVER_ID, () => {
path: imagePath,
});
} else {
if (options.saveUrlImage) {
source = await saveFileBeforeFlash(
imagePath,
options.saveUrlImageTo,
onProgress,
onFail,
);
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 });
source = new Http({
url: imagePath,
avoidRandomAccess: true,
auth: options.image.auth,
});
}
}
}
const results = await writeAndValidate({
source,
destinations: dests,
verify: options.validateWriteOnSuccess,
verify: true,
autoBlockmapping: options.autoBlockmapping,
decompressFirst: options.decompressFirst,
onProgress,
@@ -306,8 +318,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
ipc.of[IPC_SERVER_ID].emit('done', { results });
await delay(DISCONNECT_DELAY);
await terminate(exitCode);
} catch (error) {
log(`Error: ${error.message}`);
} catch (error: any) {
exitCode = GENERAL_ERROR;
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
}
@@ -320,43 +331,3 @@ ipc.connectTo(IPC_SERVER_ID, () => {
ipc.of[IPC_SERVER_ID].emit('ready', {});
});
});
async function saveFileBeforeFlash(
imagePath: string,
saveUrlImageTo: string,
onProgress: (state: ProgressState) => void,
onFail: (
destination: sdk.sourceDestination.SourceDestination,
error: Error,
) => void,
) {
const urlImage = new Http({ url: imagePath, avoidRandomAccess: true });
const source = await urlImage.getInnerSource();
const metadata = await source.getMetadata();
const fileName = `${saveUrlImageTo}/${metadata.name}`;
let alreadyDownloaded = false;
try {
alreadyDownloaded = (await fs.stat(fileName)).isFile();
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
if (!alreadyDownloaded) {
await sdk.multiWrite.decompressThenFlash({
source,
destinations: [new File({ path: fileName, write: true })],
onProgress: (progress) => {
onProgress({
...progress,
type: 'downloading',
});
},
onFail: (...args) => {
onFail(...args);
},
verify: true,
});
}
return new File({ path: fileName });
}

View File

@@ -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';
@@ -37,7 +38,7 @@ export async function sudo(
env: {
PATH: env.PATH,
SUDO_ASKPASS: join(
(app || remote.app).getAppPath(),
getAppPath(),
__dirname,
'sudo-askpass.osascript.js',
),
@@ -49,7 +50,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 };

View File

@@ -34,16 +34,6 @@ export type DrivelistDrive = Drive & {
displayName: string;
};
/**
* @summary Check if a drive is locked
*
* @description
* This usually points out a locked SD Card.
*/
export function isDriveLocked(drive: DrivelistDrive): boolean {
return Boolean(drive.isReadOnly);
}
/**
* @summary Check if a drive is a system drive
*/
@@ -73,9 +63,7 @@ export function isSourceDrive(
): boolean {
if (selection) {
if (selection.drive) {
const sourcePath = selection.drive.devicePath || selection.drive.device;
const drivePath = drive.devicePath || drive.device;
return pathIsInside(sourcePath, drivePath);
return selection.drive.device === drive.device;
}
if (selection.path) {
return sourceIsInsideDrive(selection.path, drive);
@@ -117,24 +105,18 @@ export function isDriveLargeEnough(
}
/**
* @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. not locked and large enough for an image
* @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 (
!isDriveLocked(drive) &&
isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image as SourceMetadata) &&
!isDriveDisabled(drive)
!write ||
(!drive.disabled &&
isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image as SourceMetadata))
);
}
@@ -215,17 +197,19 @@ export const statuses = {
*/
export function getDriveImageCompatibilityStatuses(
drive: DrivelistDrive,
image?: SourceMetadata,
image: SourceMetadata | undefined,
write: boolean,
) {
const statusList = [];
// Mind the order of the if-statements if you modify.
if (isDriveLocked(drive)) {
if (drive.isReadOnly && write) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.locked(),
});
} else if (
}
if (
!_.isNil(drive) &&
!_.isNil(drive.size) &&
!isDriveLargeEnough(drive, image)
@@ -264,10 +248,11 @@ export function getDriveImageCompatibilityStatuses(
*/
export function getListDriveImageCompatibilityStatuses(
drives: DrivelistDrive[],
image: SourceMetadata,
image: SourceMetadata | undefined,
write: boolean,
) {
return drives.flatMap((drive) => {
return getDriveImageCompatibilityStatuses(drive, image);
return getDriveImageCompatibilityStatuses(drive, image, write);
});
}
@@ -279,9 +264,12 @@ export function getListDriveImageCompatibilityStatuses(
*/
export function hasDriveImageCompatibilityStatus(
drive: DrivelistDrive,
image: SourceMetadata,
image: SourceMetadata | undefined,
write: boolean,
) {
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
return Boolean(
getDriveImageCompatibilityStatuses(drive, image, write).length,
);
}
export interface DriveStatus {

View File

@@ -81,13 +81,10 @@ export const compatibility = {
} 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)}.
The selected source is ${prettyBytes(source.size - target.size)}
larger than this drive.
`;
},
@@ -117,8 +114,16 @@ export const warning = {
].join(' ');
},
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.
`;
},
largeDriveSize: () => {
return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.';
return "This is a large drive! Make sure it doesn't contain files that you want to keep.";
},
systemDrive: () => {

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "balena-etcher",
"private": true,
"displayName": "balenaEtcher",
"version": "1.5.109",
"version": "1.10.0",
"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,81 @@
},
"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.2.1",
"electron-builder": "^22.7.0",
"electron-mocha": "^9.1.0",
"electron-notarize": "^1.0.0",
"electron-rebuild": "^1.11.0",
"electron-updater": "^4.3.2",
"etcher-sdk": "^4.1.30",
"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#workaround-windows-amperstand-in-username",
"sys-class-rgb-led": "^2.1.0",
"tmp": "^0.2.1",
"ts-loader": "^8.0.0",
"ts-node": "^9.0.0",
"tslib": "^2.0.0",
"typescript": "^4.0.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.2.7",
"@types/copy-webpack-plugin": "6.4.3",
"@types/mime-types": "2.1.1",
"@types/mini-css-extract-plugin": "1.2.2",
"@types/mocha": "8.0.3",
"@types/node": "14.18.33",
"@types/node-ipc": "9.1.2",
"@types/react": "16.14.34",
"@types/react-dom": "16.9.17",
"@types/semver": "7.3.13",
"@types/sinon": "9.0.0",
"@types/terser-webpack-plugin": "5.0.2",
"@types/tmp": "0.2.3",
"@types/webpack-node-externals": "2.5.3",
"aws4-axios": "2.4.9",
"chai": "4.2.0",
"copy-webpack-plugin": "7.0.0",
"css-loader": "5.0.1",
"d3": "4.13.0",
"debug": "4.2.0",
"electron": "12.2.3",
"electron-builder": "22.10.5",
"electron-mocha": "9.3.3",
"electron-notarize": "1.0.0",
"electron-rebuild": "3.2.9",
"electron-updater": "4.3.5",
"esbuild-loader": "2.16.0",
"etcher-sdk": "7.4.0",
"file-loader": "6.2.0",
"husky": "4.2.5",
"immutable": "3.8.2",
"lint-staged": "10.2.2",
"lodash": "4.17.21",
"mini-css-extract-plugin": "1.3.3",
"mocha": "8.0.1",
"native-addon-loader": "2.0.1",
"node-ipc": "9.1.1",
"omit-deep-lodash": "1.1.7",
"outdent": "0.8.0",
"path-is-inside": "1.0.2",
"pnp-webpack-plugin": "1.6.4",
"pretty-bytes": "5.3.0",
"react": "16.8.5",
"react-dom": "16.8.5",
"redux": "4.0.5",
"rendition": "19.2.0",
"resin-corvus": "2.0.5",
"semver": "7.3.8",
"simple-progress-webpack-plugin": "1.1.2",
"sinon": "9.0.2",
"spectron": "14.0.0",
"string-replace-loader": "3.0.1",
"style-loader": "2.0.0",
"styled-components": "5.1.0",
"sys-class-rgb-led": "3.0.1",
"terser-webpack-plugin": "5.2.5",
"ts-loader": "8.0.12",
"ts-node": "9.1.1",
"tslib": "2.0.0",
"typescript": "4.4.4",
"url-loader": "4.1.1",
"uuid": "8.1.0",
"webpack": "5.11.0",
"webpack-cli": "4.2.0",
"webpack-dev-server": "4.5.0"
},
"engines": {
"node": ">=14 < 16"
},
"versionist": {
"publishedAt": "2022-11-10T20:54:13.110Z"
}
}

View File

@@ -1,11 +1,21 @@
---
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
- repo: sys-class-rgb-led
url: https://github.com/balena-io-modules/sys-class-rgb-led
module: sys-class-rgb-led
- repo: rendition
url: https://github.com/balena-io-modules/rendition
module: rendition

View File

@@ -1,3 +1,2 @@
codespell==1.12.0
awscli==1.11.87
awscli==1.27.5
shyaml==0.5.0

View File

@@ -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;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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;

View File

@@ -33,26 +33,6 @@ describe('Model: selectionState', function () {
expect(selectionState.getImage()).to.be.undefined;
});
it('getImagePath() should return undefined', function () {
expect(selectionState.getImagePath()).to.be.undefined;
});
it('getImageSize() should return undefined', function () {
expect(selectionState.getImageSize()).to.be.undefined;
});
it('getImageName() should return undefined', function () {
expect(selectionState.getImageName()).to.be.undefined;
});
it('getImageLogo() should return undefined', function () {
expect(selectionState.getImageLogo()).to.be.undefined;
});
it('getImageSupportUrl() should return undefined', function () {
expect(selectionState.getImageSupportUrl()).to.be.undefined;
});
it('hasDrive() should return false', function () {
const hasDrive = selectionState.hasDrive();
expect(hasDrive).to.be.false;
@@ -379,43 +359,6 @@ describe('Model: selectionState', function () {
});
});
describe('.getImagePath()', function () {
it('should return the image path', function () {
const imagePath = selectionState.getImagePath();
expect(imagePath).to.equal('foo.img');
});
});
describe('.getImageSize()', function () {
it('should return the image size', function () {
const imageSize = selectionState.getImageSize();
expect(imageSize).to.equal(999999999);
});
});
describe('.getImageName()', function () {
it('should return the image name', function () {
const imageName = selectionState.getImageName();
expect(imageName).to.equal('Raspbian');
});
});
describe('.getImageLogo()', function () {
it('should return the image logo', function () {
const imageLogo = selectionState.getImageLogo();
expect(imageLogo).to.equal(
'<svg><text fill="red">Raspbian</text></svg>',
);
});
});
describe('.getImageSupportUrl()', function () {
it('should return the image support url', function () {
const imageSupportUrl = selectionState.getImageSupportUrl();
expect(imageSupportUrl).to.equal('https://www.raspbian.org/forums/');
});
});
describe('.hasImage()', function () {
it('should return true', function () {
const hasImage = selectionState.hasImage();
@@ -435,9 +378,9 @@ describe('Model: selectionState', function () {
SourceType: File,
});
const imagePath = selectionState.getImagePath();
const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('bar.img');
const imageSize = selectionState.getImageSize();
const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.equal(999999999);
});
});
@@ -446,9 +389,9 @@ describe('Model: selectionState', function () {
it('should clear the image', function () {
selectionState.deselectImage();
const imagePath = selectionState.getImagePath();
const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.be.undefined;
const imageSize = selectionState.getImageSize();
const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.be.undefined;
});
});
@@ -472,9 +415,9 @@ describe('Model: selectionState', function () {
it('should be able to set an image', function () {
selectionState.selectSource(image);
const imagePath = selectionState.getImagePath();
const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.img');
const imageSize = selectionState.getImageSize();
const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.equal(999999999);
});
@@ -485,7 +428,7 @@ describe('Model: selectionState', function () {
archiveExtension: 'zip',
});
const imagePath = selectionState.getImagePath();
const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.zip');
});
@@ -496,7 +439,7 @@ describe('Model: selectionState', function () {
archiveExtension: 'xz',
});
const imagePath = selectionState.getImagePath();
const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.xz');
});
@@ -507,7 +450,7 @@ describe('Model: selectionState', function () {
archiveExtension: 'gz',
});
const imagePath = selectionState.getImagePath();
const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('something.linux-x86-64.gz');
});
@@ -675,12 +618,12 @@ describe('Model: selectionState', function () {
});
it('getImagePath() should return undefined', function () {
const imagePath = selectionState.getImagePath();
const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.be.undefined;
});
it('getImageSize() should return undefined', function () {
const imageSize = selectionState.getImageSize();
const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.be.undefined;
});
@@ -700,12 +643,12 @@ describe('Model: selectionState', function () {
});
it('getImagePath() should return the image path', function () {
const imagePath = selectionState.getImagePath();
const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.img');
});
it('getImageSize() should return the image size', function () {
const imageSize = selectionState.getImageSize();
const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.equal(999999999);
});

View File

@@ -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;
}

View File

@@ -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');
}

View File

@@ -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,9 +29,6 @@ describe('Browser: progressStatus', function () {
eta: 15,
speed: 100000000000000,
};
settings.set('unmountOnSuccess', true);
settings.set('validateWriteOnSuccess', true);
});
it('should report 0% if percentage == 0 but speed != 0', function () {
@@ -41,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(
@@ -64,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(
@@ -96,40 +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, validateWriteOnSuccess', 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, !validateWriteOnSuccess', function () {
this.state.percentage = 100;
settings.set('validateWriteOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...',
);
});
it('should handle percentage == 100, flashing, !unmountOnSuccess, !validateWriteOnSuccess', function () {
this.state.percentage = 100;
settings.set('unmountOnSuccess', false);
settings.set('validateWriteOnSuccess', 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(
@@ -137,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...',
);

View File

@@ -23,37 +23,6 @@ import * as constraints from '../../lib/shared/drive-constraints';
import * as messages from '../../lib/shared/messages';
describe('Shared: DriveConstraints', function () {
describe('.isDriveLocked()', function () {
it('should return true if the drive is read-only', function () {
const result = constraints.isDriveLocked({
device: '/dev/disk2',
size: 999999999,
isReadOnly: true,
} as constraints.DrivelistDrive);
expect(result).to.be.true;
});
it('should return false if the drive is not read-only', function () {
const result = constraints.isDriveLocked({
device: '/dev/disk2',
size: 999999999,
isReadOnly: false,
} as constraints.DrivelistDrive);
expect(result).to.be.false;
});
it("should return false if we don't know if the drive is read-only", function () {
const result = constraints.isDriveLocked({
device: '/dev/disk2',
size: 999999999,
} as constraints.DrivelistDrive);
expect(result).to.be.false;
});
});
describe('.isSystemDrive()', function () {
it('should return true if the drive is a system drive', function () {
const result = constraints.isSystemDrive({
@@ -545,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',
@@ -745,7 +680,7 @@ describe('Shared: DriveConstraints', function () {
this.drive.disabled = false;
});
it('should return false if the drive is not large enough and is a source drive', function () {
it('should return false if the drive is not large enough and is the source drive', function () {
expect(
constraints.isDriveValid(this.drive, {
...image,
@@ -755,7 +690,7 @@ describe('Shared: DriveConstraints', function () {
).to.be.false;
});
it('should return false if the drive is not large enough and is not a source drive', function () {
it('should return false if the drive is not large enough and is not the source drive', function () {
expect(
constraints.isDriveValid(this.drive, {
...image,
@@ -765,17 +700,17 @@ describe('Shared: DriveConstraints', function () {
).to.be.false;
});
it('should return false if the drive is large enough and is a source drive', function () {
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
it('should return true if the drive is large enough and is the source drive', function () {
expect(constraints.isDriveValid(this.drive, image)).to.be.true;
});
it('should return false if the drive is large enough and is not a source drive', function () {
it('should return true if the drive is large enough and is not the source drive', function () {
expect(
constraints.isDriveValid(this.drive, {
...image,
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
}),
).to.be.false;
).to.be.true;
});
});
});
@@ -983,6 +918,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
expect(result).to.deep.equal([]);
@@ -995,6 +931,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expectedTuples: Array<['WARNING' | 'ERROR', string]> = [];
@@ -1009,6 +946,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'containsImage']];
@@ -1025,6 +963,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expectedTuples = [['WARNING', 'system']];
@@ -1040,6 +979,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expected = [
{
@@ -1060,6 +1000,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [];
@@ -1076,6 +1017,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'locked']];
@@ -1092,6 +1034,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['WARNING', 'sizeNotRecommended']];
@@ -1108,6 +1051,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expectedTuples = [['WARNING', 'largeDrive']];
@@ -1128,9 +1072,13 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'locked']];
const expectedTuples = [
['ERROR', 'locked'],
['ERROR', 'containsImage'],
];
// @ts-ignore
expectStatusTypesAndMessagesToBe(result, expectedTuples);
@@ -1144,6 +1092,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'locked']];
@@ -1161,6 +1110,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expected = [
{
@@ -1181,6 +1131,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [
@@ -1207,7 +1158,7 @@ describe('Shared: DriveConstraints', function () {
'/dev/disk6',
];
const drives = [
({
{
device: drivePaths[0],
description: 'My Drive',
size: 123456789,
@@ -1215,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,
@@ -1224,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,
@@ -1233,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,
@@ -1242,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,
@@ -1251,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,
@@ -1260,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,
@@ -1269,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 = {
@@ -1287,7 +1238,7 @@ describe('Shared: DriveConstraints', function () {
describe('given no drives', function () {
it('should return no statuses', function () {
expect(
constraints.getListDriveImageCompatibilityStatuses([], image),
constraints.getListDriveImageCompatibilityStatuses([], image, true),
).to.deep.equal([]);
});
});
@@ -1298,6 +1249,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[0]],
image,
true,
),
).to.deep.equal([
{
@@ -1312,6 +1264,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[1]],
image,
true,
),
).to.deep.equal([
{
@@ -1326,6 +1279,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[2]],
image,
true,
),
).to.deep.equal([
{
@@ -1340,6 +1294,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[3]],
image,
true,
),
).to.deep.equal([
{
@@ -1354,6 +1309,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[4]],
image,
true,
),
).to.deep.equal([
{
@@ -1368,6 +1324,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[5]],
image,
true,
),
).to.deep.equal([
{
@@ -1381,7 +1338,11 @@ describe('Shared: DriveConstraints', function () {
describe('given multiple drives with all warnings/errors', function () {
it('should return all statuses', function () {
expect(
constraints.getListDriveImageCompatibilityStatuses(drives, image),
constraints.getListDriveImageCompatibilityStatuses(
drives,
image,
true,
),
).to.deep.equal([
{
message: 'Source drive',

View File

@@ -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;
});
},

View File

@@ -15,43 +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()).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');
});
});
});
});
}

View File

@@ -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,
}
}

View File

@@ -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
View File

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

View File

@@ -1 +1 @@
declare module 'sudo-prompt';
declare module '@balena/sudo-prompt';

View File

@@ -17,7 +17,6 @@
import * as CopyPlugin from 'copy-webpack-plugin';
import { readdirSync } 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,70 @@ 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 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 +143,7 @@ function findLzmaNativeBindingsFolder(): string {
}
const LZMA_BINDINGS_FOLDER = findLzmaNativeBindingsFolder();
const LZMA_BINDINGS_FOLDER_RENAMED = 'binding';
interface ReplacementRule {
search: string;
@@ -108,19 +163,45 @@ function replace(test: RegExp, ...replacements: ReplacementRule[]) {
};
}
function fetchWasm(...where: string[]) {
const whereStr = where.map((x) => `'${x}'`).join(', ');
return outdent`
const Path = require('path');
let electron;
try {
// This doesn't exist in the child-writer
electron = require('electron');
} catch {
}
function appPath() {
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}, '/');
`;
}
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,
@@ -131,7 +212,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$/,
@@ -141,9 +227,11 @@ const commonConfig = {
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
loader: 'esbuild-loader',
options: {
configFile: 'tsconfig.webpack.json',
loader: 'tsx',
target: 'es2021',
tsconfigRaw,
},
},
],
@@ -174,13 +262,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
{
@@ -189,14 +272,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')",
}),
// remove bindings magic from ext2fs
replace(/node_modules\/ext2fs\/lib\/(ext2fs|binding)\.js$/, {
search: "require('bindings')('bindings')",
replace: "require('../build/Release/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$/, {
@@ -229,9 +307,33 @@ 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: fetchWasm('ext2fs', 'lib'),
}),
// Same for node-crc-utils
replace(/node_modules\/@balena\/node-crc-utils\/crc32\.js$/, {
search: 'scriptDirectory=__dirname+"/"',
replace: fetchWasm('@balena', 'node-crc-utils'),
}),
// Copy native modules to generated folder
{
test: /\.node$/,
@@ -248,16 +350,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',
@@ -281,13 +387,21 @@ const guiConfigCopyPatterns = [
from: 'node_modules/node-raspberrypi-usbboot/blobs',
to: 'modules/node-raspberrypi-usbboot/blobs',
},
{
from: 'node_modules/ext2fs/lib/libext2fs.wasm',
to: 'modules/ext2fs/lib/libext2fs.wasm',
},
{
from: 'node_modules/@balena/node-crc-utils/crc32.wasm',
to: 'modules/@balena/node-crc-utils/crc32.wasm',
},
];
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`,
});
}
@@ -299,10 +413,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 };',
@@ -341,41 +464,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
View File

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