Compare commits

..

87 Commits
2.3.1 ... main

Author SHA1 Message Date
Giacomo Cusinato
0f9f0d07b7
chore: switch to version 2.3.7 after the release (#2701) 2025-04-09 19:48:50 +07:00
Giacomo Cusinato
2f0414a5a1
fix: use ElectronConnectionHandler to connect ide updater services (#2697) 2025-04-09 13:58:47 +07:00
github-actions[bot]
e3319dab1a
Updated translation files (#2692)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-08 16:14:03 +07:00
Giacomo Cusinato
a669a43449
fix: wait for theia initalWindow to be set before opening sketch through open-file event (#2693)
* chore: use actual app name as `uriScheme`

* fix: wait for `initialWindow` to be set before opening sketch from file
2025-04-07 19:15:59 +02:00
Giacomo Cusinato
56ab874177
fix: propagate electron params in second instance startup (#2686) 2025-04-05 19:56:00 +09:00
Giacomo Cusinato
e36f393682
fix: prevent OutputWidget to gain focus when updated (#2681) 2025-04-03 17:51:30 +02:00
Giacomo Cusinato
4d52bb2843
chore: switch to version 2.3.6 after the release (#2682) 2025-04-03 17:51:12 +02:00
Giacomo Cusinato
39c8db8e90
fix: add missing linux dependencies for create-changelog job (#2677) 2025-04-02 23:24:35 +09:00
Giacomo Cusinato
8aa3c28c50
fix: allow write permission on release job (#2676)
Permission were previously changed here https://github.com/arduino/arduino-ide/pull/2651
Repo write permission is needed to allow creating the github release and publishing files
2025-04-02 21:51:35 +09:00
Giacomo Cusinato
d293595b89
fix: add missing linux dependencies (#2675) 2025-04-02 09:23:57 +09:00
Giacomo Cusinato
4b0982ccb3
fix: safer electron version parsing for electron-builder command (#2673) 2025-04-01 18:42:04 +09:00
Per Tillisch
9b15695c60
Give build workflow step access to required deployment environment (#2672)
* Trim trailing whitespace in build workflow

* Give build workflow step access to required deployment environment

Certain operations in the "Arduino IDE" GitHub Actions workflow use GitHub Actions "secrets" which are defined in the
repository's administrative settings.

These secrets will typically not be defined when the workflow is run in a fork. However, the workflow's base
functionality, the automated building of the application, does not require secrets. Since that base functionality alone
is very useful to contributors (either to validate relevant changes to the application and infrastructure, or to
generate tester builds) who are performing development work in a fork. For this reason, the workflow is configured to
only perform the secret-dependent operations when the required secrets have been defined in the repository settings.

One such operation is publishing the generated builds to Amazon S3, which Arduino uses to host files for distribution.
This operation depends on the "AWS_ROLE_ARN" secret. As a security measure, this secret is defined inside a deployment
environment (named "production"). GitHub Actions workflow jobs can only use secrets from deployment environments which
they have been explicitly configured to have access to.

At the time the workflow was originally developed, GitHub did not have the deployment environment feature, and so the
workflow was not configured to use environments. The switch to using a deployment environment for this secret was made
only recently, and when that was done, the workflow job that checks whether the secret is defined was not configured to
have access to the "production" environment. This caused the workflow to think it was running in a context where that
secret is not defined even when the secret is in fact defined. The bug caused the workflow to always spuriously skip the
"publish" job which publishes nightly builds of Arduino IDE, and the "publish release" step which publishes production
releases.

The bug is fixed by configuring the "build-type-determination" job so that it has access to the "production"
environment.
2025-04-01 16:27:06 +09:00
github-actions[bot]
0dff87e29c
Updated translation files (#2597)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-31 19:42:48 +09:00
Giacomo Cusinato
7dafe7b0d3
feat: use Arduino CLI 1.2.0 (#2645) 2025-03-28 17:53:46 +01:00
Giacomo Cusinato
859d29d41a
feat: use theia@1.57.0 (#2654) 2025-03-29 01:33:25 +09:00
Christian Sarnataro
d298b3ffc9
fix: sanitize message in notification component (#2664)
fix: sanitize messages in notification component
2025-03-24 12:42:48 +01:00
Giacomo Cusinato
9ab87bf8b5 chore: use AWS OpenID Connect for S3 publish 2025-03-11 21:42:12 +07:00
dankeboy36
5ec1915000
fix(plugin): decouple state update from the LS (#2643)
* fix(plugin): decouple state update from the LS

To enhance the reliability of Arduino IDE extensions, the update
process for `ArduinoState` has been modified to ensure independence
from the language server's availability. This change addresses issues
caused by `compileSummary` being `undefined` due to potential startup
failures of the Arduino Language Server, as noted in
https://github.com/dankeboy36/esp-exception-decoder/issues/28#issuecomment-2681800772.

The `compile` command now resolves with a `CompileSummary` rather than
`void`, facilitating a more reliable way for extensions to access
necessary data. Furthermore, the command has been adjusted to allow
resolution with `undefined` when the compiled data is partial.

By transitioning to direct usage of the resolved compile value for
state updates, the reliance on executed commands for extensions is
eliminated. This update also moves the VSIX command execution to the
frontend without altering existing IDE behavior.

Closes arduino/arduino-ide#2642

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: install missing libx11-dev and libxkbfile-dev

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: pick better GH step name

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: install the required dependencies on Linux

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix(revert): do not manually install deps on Linux

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* chore: pin `ubuntu-22.04` for linux actions

* fix: restore accidentally removed dispose on finally

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix(test): align mock naming 💄

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: let the ino contribution notify the LS

+ event emitter dispatches the new state.

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix(test): emit the new compiler summary state

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* chore(revert): unpin linux version, use latest

revert of b11bde1c47

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

---------

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
Co-authored-by: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com>
2025-03-10 15:20:22 +07:00
Per Tillisch
6d96e227eb Bump built-in example sketches version to 1.10.2
The Arduino IDE installation includes a collection of example sketches demonstrating fundamental concepts.

These examples are hosted in a dedicated repository, which is a dependency of this project. A new release has been made
in that `arduino/arduino-examples` repository.

The infrastructure for downloading the examples during the Arduino IDE build is hereby updated to use the latest release
of the `arduino/arduino-examples` repository.
2025-02-28 06:56:33 -08:00
dankeboy36
1712f9ea9d fix: install missing linux dependencies
Install `libx11-dev`, `libxkbfile-dev`, `libsecret-1-dev` libraries as the most recent update to ubuntu-latest does not include them
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2025-02-28 19:15:04 +07:00
Giacomo Cusinato
6eef09efd8
chore: switch to version 2.3.5 after the release (#2587)
To produce a correctly versioned nightly build.
See the [docs](1b9c7e93e0/docs/internal/release-procedure.md (7-%EF%B8%8F-bump-version-metadata-of-packages)) for more details.
2024-12-03 13:21:25 +01:00
github-actions[bot]
1112057979
Updated translation files (#2523)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-02 16:02:58 +01:00
Giacomo Cusinato
8e18c47d30 feat: use dompurify to sanitize translations
Pin same version of `dompurify` used in Theia
2024-12-02 15:21:22 +01:00
Giacomo Cusinato
4788bfbc3f feat: introduce VersionWelcomeDialog
Show donate dialog after the first time a first IDE version is loaded
2024-12-02 15:21:22 +01:00
Giacomo Cusinato
71b11ed829 feat: add donate footer to updater dialog 2024-12-02 15:21:22 +01:00
dependabot[bot]
3aedafa306 build(deps): Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-29 01:38:27 -08:00
dependabot[bot]
284dd83d7d build(deps): Bump peter-evans/create-pull-request from 5 to 7
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 5 to 7.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v5...v7)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-29 00:05:37 -08:00
per1234
c09b5f718a Use Ubuntu 18.10 in Linux build container
Background
==========

Shared Library Dependencies
---------------------------

The Linux build of Arduino IDE has dynamic linkage against the libstdc++ and glibc shared libraries. This results in
it having a dependency on the version of the libraries that happens to be present in the environment it is built in.

Although newer versions of the shared libraries are compatible with executables linked against an older version, the
reverse is not true. This means that building Arduino IDE on a Linux machine with a recent distro version installed
causes the IDE to error on startup for users who have a distro with older versions of the dependencies.

For example, if Arduino IDE were built on a machine with version 3.4.33 of libstdc++, then attempting to run it on a
machine with an older version of libstdc++ would fail with an error like:

```
Error: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.33' not found (required by /home/foo/arduino-ide/resources/app/lib/backend/native/nsfw.node)
```

Likewise,  if Arduino IDE were built on a machine with version 2.39 of glibc, then attempting to run it on a machine
with an older version of glibc would fail with an error like:

```
Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found (required by /home/foo/arduino-ide/resources/app/node_modules/nsfw/build/Release/nsfw.node)
```

Build Machine Requirements
--------------------------

The IDE builds distributed by Arduino should be compatible with a reasonable range of Linux distribution versions. In
order to achieve this, the builds must be performed in a machine with an older version of the shared libraries. The
shared libraries are part of the Linux distro, and installing a different version is not feasible. So this imposes a
maximum limit on the build machine's distro version.

The distributed builds are generated via a GitHub Actions workflow. The most simple approach is to run the build in the
machine of the GitHub-hosted runners provided for each operating system. However, GitHub provides a limited range of
operating system versions in their runners, and removes the older versions as newer versions are added. This means that
building in the GitHub-hosted runner machine would not allow for the desired range of Linux distro version
compatibility. For this reason, the Linux build is performed in a Docker container that provides an older version of
Ubuntu.

The same situation of incompatibility with Linux distro versions that have a version of the shared library dependencies
older than the version present on the build machine occurs for several of the tools and frameworks used by the build
process (e.g., Node.js, Python). In this case, the tables are turned as we are now the user rather than the distributor
and so are at the mercy of the Linux distro version compatibility range provided by the distributor. So this imposes a
minimum limit on the build machine's distro version.

Although several of the dependencies used by the standard build system have dependencies on versions of glibc higher
than the version 2.27 present in Ubuntu 18.04, it was possible to use this distro version in the Linux build container
by using alternative distributions and/or versions of these dependencies.

Workflow Artifacts
------------------

The build workflow uses GitHub actions workflow artifacts to transfer the files generated by the build job to subsequent
jobs in the workflow. The "actions/upload-artifact" action is used for this purpose.

Problem
=======

GitHub is dropping support for the workflow artifacts produced by the version 3.x of the "actions/upload-artifact"
action that was previously used by the build job. So the action version used in the build workflow was updated to the
current version 4.x. This version of the action uses a newer version of the Node.js runtime (20). Unfortunately the the
Node.js 20 runtime used by the action has a dependency on glibc version 2.28, which causes the Linux build job to fail
after the update of the "actions/upload-artifact" action:

```
Run actions/upload-artifact@v4
/__e/node20/bin/node: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by /__e/node20/bin/node)
```

Unlike the other dependencies of the build process, it is no longer possible to work around this incompatibility by
continuing to use the older compatible version of the "actions/upload-artifact" action. It is also impossible to replace
the incompatible Node.js 20.x distribution used by the action, since it comes from the read-only file system of the
runner image. Likewise, it is not possible to configure or force the action to use a Node.js installation at a different
path on the runner machine.

Resolution
==========

Compatibility with the new version of the "actions/upload-artifact" action is attained by updating the version of Linux
in the build container to 18.10, which is the oldest version that has glibc 2.28. The presence of a newer glibc version
in the container also makes it compatible with several other dependencies of the build process, meaning the code in the
Dockerfile and workflow for working around the incompatibilities of Ubuntu 18.04 can be removed.

Consequences
============

Unfortunately this means the loss of compatibility of the Linux Arduino IDE builds with distros that use glibc 2.27
(e.g., Ubuntu 18.04). User of those distros will now find that Arduino IDE fails to start with an error like:

```
Error: node-loader:
Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by /home/foo/arduino-ide/resources/app/lib/backend/native/pty.node)
    at 85467 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:2766)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
    at 23571 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:3374073)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
    at 55444 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:3369761)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
    at 24290 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:1780542)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
    at 43416 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:1770138)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
```
2024-11-27 06:58:10 -08:00
per1234
dba57b312c Don't upload multiple times to same artifact in build workflow
The build workflow produces binaries for a range of target hosts. This is done by using a job matrix in the GitHub
Actions workflow that produces each build in a parallel job. GitHub Actions workflow artifacts are used to transfer the
generated files between sequential jobs in the workflow. The "actions/upload-artifact" action is used for this purpose.

Previously, a single artifact was used for this purpose, with each of the parallel jobs uploading its own generated
files to that artifact. However, support for uploading multiple times to a single artifact was dropped in version 4.0.0
of the "actions/upload-artifact" action. So it is now necessary to use a dedicated artifact for each of the builds.
These can be downloaded in aggregate by using the artifact name globbing and merging features which were introduced in
version 4.1.0 of the "actions/download-artifact" action.
2024-11-27 06:58:10 -08:00
per1234
90d3d77ca4 Don't upload multiple times to same artifact in label sync workflow
The "Sync Labels" GitHub Actions workflow is configured to allow the use of multiple shared label configuration files.
This is done by using a job matrix in the GitHub Actions workflow to download each of the files from the source
repository in a parallel GitHub Actions workflow job. A GitHub Actions workflow artifact was used to transfer the
generated files between sequential jobs in the workflow. The "actions/upload-artifact" and "actions/download-artifact"
actions are used for this purpose.

Previously, a single artifact was used for the transfer of all the shared label configuration files, with each of the
parallel jobs uploading its own generated files to that artifact. However, support for uploading multiple times to a
single artifact was dropped in version 4.0.0 of the "actions/upload-artifact" action. So it is now necessary to use a
dedicated artifact for each of the builds. These can be downloaded in aggregate by using the artifact name globbing and
merging features which were introduced in version 4.1.0 of the "actions/download-artifact" action.
2024-11-27 06:58:10 -08:00
dependabot[bot]
0aec778e84 build(deps): Bump geekyeggo/delete-artifact from 2 to 5
Bumps [geekyeggo/delete-artifact](https://github.com/geekyeggo/delete-artifact) from 2 to 5.
- [Release notes](https://github.com/geekyeggo/delete-artifact/releases)
- [Changelog](https://github.com/GeekyEggo/delete-artifact/blob/main/CHANGELOG.md)
- [Commits](https://github.com/geekyeggo/delete-artifact/compare/v2...v5)

---
updated-dependencies:
- dependency-name: geekyeggo/delete-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-27 06:58:10 -08:00
dependabot[bot]
84d2dfd13e build(deps): Bump actions/download-artifact from 3 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-27 06:58:10 -08:00
dependabot[bot]
86c7fd7b59 build(deps): Bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-27 06:58:10 -08:00
Giacomo Cusinato
de265694ee feat: use Arduino CLI v1.1.1 2024-11-27 14:05:16 +01:00
Giacomo Cusinato
8462d8a391 refactor: generate-protocol now fetch proto files from arduino_cli_{version}_proto.zip
- Use the CLI release proto.zip to get proto files for production versions of CLI
- Extract the proto files from repo if CLI version is declared as `commitsh` or version is 1.1.0

See https://github.com/arduino/arduino-cli/pull/2761
2024-11-27 14:05:16 +01:00
dankeboy36
48d6d37539 feat: can skip verify before upload
Adds a new preference to control whether the
verify command should automatically run before the
upload. If the `arduino.upload.autoVerify` setting
value is `false`, IDE does not recompile the
sketch code before uploading it to the board.

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2024-11-26 21:41:48 +01:00
Giacomo Cusinato
d1065886ef feat: use Arduino CLI 1.1.0 2024-11-21 14:21:30 +01:00
Giacomo Cusinato
8773bd67ab fix: use missing google proto files in CLI 2024-11-21 14:21:30 +01:00
Giacomo Cusinato
4189b086de fix: update yarn.lock 2024-11-21 10:44:20 +01:00
dankeboy36
3fc8474d71
fix: align viewsWelcome behavior to VS Code (#2543)
* fix: align `viewsWelcome` behavior to VS Code

Ref: eclipse-theia/theia#14309
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: update change proposal from Theia as is

Ref: arduino/arduino-ide#2543
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

---------

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2024-11-21 08:43:04 +01:00
Giacomo Cusinato
4cf9909a07
fix: retry compilation if grpc client needs to be reinitialized (#2548)
* fix: use `Status` enum for status code in `ServiceError` type guards

This change resolves the issue where the intersection of `ServiceError` error codes of type `number` resulted in the `never` type due to conflict between number and `State` enum if `StatusObject`

* feat: add `isInvalidArgument` type guard to `ServiceError`

* fix: retry compilation if grpc client needs to be reinitialized

See https://github.com/arduino/arduino-ide/issues/2547
2024-11-21 08:42:14 +01:00
Giacomo Cusinato
41844c9470
feat: implement menu action to reload current board data (#2553) 2024-11-21 08:41:26 +01:00
Giacomo Cusinato
7c231fff76
fix: memory leak when scanning sketchbooks with large files (#2555)
Resolves https://github.com/arduino/arduino-ide/issues/2537

Fix memory leak issue caused by inflight dependency, see https://github.com/isaacs/node-glob/issues/435
2024-11-21 08:40:52 +01:00
dependabot[bot]
d6235f0a0c build(deps): Bump svenstaro/upload-release-action from 2.7.0 to 2.9.0
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.7.0 to 2.9.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.7.0...2.9.0)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-19 04:04:35 -08:00
per1234
d377d00042 Use appropriate equality operator in changelog script
It is considered good practice to use JavaScript's type-safe strict equality operator === instead of the equality
operator ==. Compliance with this practice is enforced by the project's ESLint configuration, via the "eqeqeq" rule.

The script used to generate the changelog for Arduino IDE's auto-update dialog contained an inappropriate usage of the
equality operator. This caused linting runs to fail:

arduino-ide-extension/scripts/compose-changelog.js
  37:19  error  Expected '===' and instead saw '=='  eqeqeq
2024-11-19 03:53:36 -08:00
per1234
f232010bec Correct eslint command in lint script
The `lint` script of the "arduino-ide-extension" package is intended to use the ESLint linter tool to check for problems
in all the package's JavaScript and TypeScript code files. It is used by the continuous integration system to validate
contributions.

Previously, the command invoked `eslint` without any arguments. With the 8.x version of ESLint used by the project, it
is necessary to provide a path argument in order to cause it to lint the contents of files. Because that argument was
not provided, the script didn't do anything at all and so would return a 0 exit status even if the code had linting rule
violations.

This is fixed by adding a `.` path argument to the command invoked by the script. This will cause ESLint to recurse
through the `arduino-ide-extension` folder and lint the code in all relevant files.
2024-11-19 03:53:36 -08:00
per1234
788017bb99 Use a dedicated GitHub workflow to check for problems with Yarn configuration
The "build" workflow builds the application for all supported targets, generates workflow artifacts from which the
builds can be downloaded by users and beta testers, and publishes nightly and production releases.

As if that wasn't enough, the workflow was also configured to check the sync of the Yarn lockfile.

This monolithic approach is harmful for multiple reasons:

* Makes it difficult to interpret a failed workflow run
* Makes the build workflow more difficult to maintain
* Increases the turnaround time for contributors and maintainers to get feedback from the CI system

The sync check operation is hereby moved to a dedicated workflow, consistent with standard practices for Arduino Tooling
projects.
2024-11-18 06:57:16 -08:00
per1234
9331d2ec0d Use a dedicated GitHub Actions workflow for testing TypeScript/JavaScript code
The "build" workflow builds the application for all supported targets, generates workflow artifacts from which the
builds can be downloaded by users and beta testers, and publishes nightly and production releases.

As if that wasn't enough, the workflow was also configured to perform the unrelated operation of running the project's
test suites.

This monolithic approach is harmful for multiple reasons:

* Makes it difficult to interpret a failed workflow run
* Unnecessarily adds a significant amount of extra content to the already extensive logs produced by the build process
* Makes the build workflow more difficult to maintain
* Increases the length of a build workflow run
* Increases the impact of a spurious failure
* Increases the turnaround time for contributors and maintainers to get feedback from the CI system

The test run operation is hereby moved to a dedicated workflow, consistent with standard practices for Arduino Tooling
projects.
2024-11-18 06:57:16 -08:00
per1234
6e695429cc Use a dedicated GitHub Actions workflow for linting TypeScript/JavaScript code
The "build" workflow builds the application for all supported targets, generates workflow artifacts from which the
builds can be downloaded by users and beta testers, and publishes nightly and production releases.

As if that wasn't enough, the workflow was also configured to perform the unrelated operation of linting the project's
TypeScript and JavaScript code.

This monolithic approach is harmful for multiple reasons:

* Makes it difficult to interpret a failed workflow run
* Unnecessarily adds a significant amount of extra content to the already extensive logs produced by the build process
* Makes the build workflow more difficult to maintain
* Increases the length of a build workflow run
* Increases the impact of a spurious failure
* Increases the turnaround time for contributors and maintainers to get feedback from the CI system

The linting operation is hereby moved to a dedicated workflow, consistent with standard practices for Arduino Tooling
projects.
2024-11-18 06:57:16 -08:00
per1234
4f8b9800a0 Remove redundant signing determination code from build system
The "build" workflow signs the macOS and Windows builds of the application. The signing process relies on access to GitHub Actions
secrets. For this reason, the workflow is configured to only sign the builds when it has access to GitHub Actions
secrets to avoid spurious failures of the workflow that would otherwise be caused by signing failure.

A flexible general purpose system for determining whether to attempt signing of a build was established years ago. However, a redundant system was added specific to the Windows build instead of using the existing system.

The redundant system is hereby removed. This makes the workflow easier to understand and maintain.
2024-11-17 22:00:34 -08:00
per1234
f72d1f0ac8 Use appropriate indicator for Windows signing determination in build workflow
The "build" workflow signs the Windows builds of the application. The signing process relies on access to GitHub Actions
secrets. For this reason, the workflow is configured to only sign the builds when it has access to GitHub Actions
secrets to avoid spurious failures of the workflow that would otherwise be caused by signing failure.

Previously the signing was determined based on the value of the `github.event.pull_request.head.repo.fork` context item.
That was effective for the use case of the workflow being triggered by a pull request from a fork (for security reasons,
GitHub Actions does not give access to secrets under these conditions).

However, there is another context under which the workflow might run without access to the signing secrets, for which
the use of context item is not appropriate. It is important to support the use of the workflow in forks of the
repository. In addition to the possible value to hard forked projects, this is essential to allow conscientious
contributors to test contributions to the build and release system in their own fork prior to submitting a pull request.
The previous configuration would cause a workflow run performed by a contributor in a fork to attempt to sign the
Windows build. Unless the contributor had set up the ridiculously complex infrastructure required to perform the signing
for the Windows build, which is utterly infeasible, this would cause the workflow to fail spuriously.

The appropriate approach, which has been the established convention in the rest of the workflow code, is to use the
secret itself when determining whether to attempt the signing process. If the secret is not defined (resulting in it
having an empty string value), then the signing should be skipped. If it is defined, then the signing should be
performed.
2024-11-17 22:00:34 -08:00
per1234
0fe0feace4 Get job-specific configuration from matrix in build workflow
The "build" workflow builds the application for a range of target hosts. This is done by using a job matrix. A separate
parallel job runs for each target. The target-specific configuration data is defined in the job matrix array.

This configuration data includes the information related to the code signing certificates. Inexplicably, during the work
to add support for signing the Windows builds with an "eToken" hardware authentication device, this data was not used
for the Windows code signing configuration. Instead the certificate data was redundantly hardcoded into the workflow
code.

The Windows code signing certificate configuration is hereby changed to use the established flexible job configuration
data system. This makes the workflow easier to understand and maintain.
2024-11-17 20:18:52 -08:00
per1234
43f0ccb250 Use appropriate indicator for dependency installation conditionals in build workflow
The Windows builds of the application are cryptographically signed. The signing requires an "eToken" hardware
authentication device be connected to the machine performing the signing. This means that it is necessary to use a
self-hosted GitHub Actions runner for the Windows job of the build workflow rather than the runners hosted by GitHub.

There are some unique characteristics of the self-hosted runner which the workflow code must accommodate. One of these
is that, rather than installing dependencies of the build process during the workflow run as is done for the
GitHub-hosted runners, the dependencies are preinstalled in the self-hosted runner machine. So the dependency
installation steps must be configured so that they will be skipped when the job is running on the self-hosted runner.

This is done by adding a conditional to the steps. Previously the conditional was based on the value of the `runner.os`
context item. This is not an appropriate indicator of the job running on the self-hosted runner because `runner.os` will
have the same value if the job was running on a GitHub-hosted Windows runner. That might seem like only a hypothetical
problem since the workflow does not use a GitHub-hosted Windows runner. However, it is important to support the use of
the workflow in forks of the repository. In addition to the possible value to hard forked projects, this is essential to
allow conscientious contributors to test contributions to the build and release system in their own fork prior to
submitting a pull request.

The conditionals are changed to use the more appropriate indicator of the specific name of the self-hosted Windows
runner (via the `runner.name` context item).
2024-11-17 03:15:42 -08:00
per1234
c0b0b84d79 Simplify and generalize configurable working directory code in build workflow
The Windows builds of the application are cryptographically signed. The signing requires an "eToken" hardware
authentication device be connected to the machine performing the signing. This means that it is necessary to use a
self-hosted GitHub Actions runner for the Windows job of the build workflow rather than the runners hosted by GitHub.

There are some unique characteristics of the self-hosted runner which the workflow code must accommodate. The default
working directory of the self-hosted runner is not suitable to perform the build under because the the resulting folder
structure produced paths that exceeded the ridiculously small maximum path length of Windows. So the workflow must be
configured to use a custom working directory with a short path (`C:\a`).

This custom working directory must be used only for the job running on the self-hosted Windows runner so the working
directory of the relevant workflow steps are configured using a ternary expression. Previously, this expression had
multiple conditions:

* the value of the `runner.os` context item
* the definition of a custom working directory value in the job matrix

The second condition is entirely sufficient. The use of the first condition only added unnecessary complexity to the
workflow code, and imposed a pointless limitation of only allowing the use of the custom working directory system on
Windows runners.

Removing the unnecessary condition makes the workflow easier to understand and maintain, and makes it possible to
configure any job to use a custom working directory if necessary.
2024-11-17 02:15:21 -08:00
per1234
3d82cb3525 Add PAID_RUNNER_BUILD_DATA environment variable back to build workflow
The build workflow produces builds for a range of target host architectures, including macOS Apple Silicon. This is done
by running a native build in a machine of the target architecture.

At the time the support for producing Apple Silicon builds was added to the workflow, use of GitHub-hosted Apple Silicon
runner machines was charged by the minute (while use of the other runners is free). In order to avoid excessive
expenses, the workflow was configured so that the Apple Silicon builds were only produced when absolutely necessary.
This was done by defining two sets of job matrix arrays, one for jobs using free runners, and the other for jobs using
paid runners. Due to the limitations of the GitHub Actions framework, it was necessary to use workflow environment
variables for this purpose.

Since that time, GitHub made free GitHub-hosted Apple Silicon runners available. When the workflow was adjusted to use
that runner, the configuration for the Apple Silicon build job was moved to the free runner job matrix array. The system
for supporting selective use of paid GitHub-hosted runners to produce builds was not removed at that time. This is
reasonable since it is possible the need will arise for using paid runners at some point in the future (e.g., only
legacy older versions of free macOS "Intel" runners are now provided and it is likely that even these will eventually be
phased out forcing us to use the paid runner to produce builds for that target). However, the environment variable for
the paid runner job matrix array data was removed. The absence of that variable made it very difficult to understand the
workflow code for the system.

For this reason, the environment variable is replaced, but empty of data. A comment is added to explain the reason for
this.
2024-11-17 00:54:17 -08:00
per1234
9cbee0eacf Trim trailing whitespace in build workflow 2024-11-17 00:54:17 -08:00
Giacomo Cusinato
63e9dfd7f5 fix: disable local windows signing for forks PR
Resolves https://github.com/arduino/arduino-ide/issues/2545
2024-11-11 14:53:42 +01:00
dankeboy36
3ccc864453
fix(doc): add missing prerequisites to dev docs (#2531)
Document prerequisites of the Arduino CLI, LS, etc. tools when built
from a Git commitish.

---------

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
Co-authored-by: per1234 <accounts@perglass.com>
2024-10-26 17:15:23 -07:00
Dave Simpson
44f15238d6
chore: switch to version 2.3.4 after the release (#2514) 2024-10-24 09:26:49 +02:00
Giacomo Cusinato
4a3abf542c fix: prevent parsing CLI errors without metadata
When parsing a CLI error, check if any metadata from grpc is present before trying to parse it.
Closes #2516
2024-10-22 09:06:28 -07:00
per1234
91bb75ca97 Bump version metadata post release
On every startup, Arduino IDE checks for new versions of the IDE. If a newer version is available, a notification/dialog
is shown offering an update.

"Newer" is determined by comparing the version of the user's IDE to the latest available version on the update channel.
This comparison is done according to the Semantic Versioning Specification ("SemVer").

In order to facilitate beta testing, builds are generated of the Arduino IDE at the current stage in development. These
builds are given an identifying version of the following form:

- <version>-snapshot-<short hash> - builds generated for every push and pull request that modifies relevant files
- <version>-nightly-<YYYYMMDD> - daily builds of the tip of the default branch

In order to cause these builds to be correctly considered "newer" than the release version, the version metadata must be
bumped immediately following each release.

This will also serve as the metadata bump for the next release in the event that release is a minor release. In case it
is instead a minor or major release, the version metadata will need to be updated once more before the release tag is
created.
2024-10-22 07:52:48 -07:00
Dave Simpson
77136687d3
Use macos-latest runner for macOS ARM build (#2513) 2024-09-24 18:30:58 +02:00
github-actions[bot]
16bc1a4610
Updated translation files (#2392)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-09-24 17:05:43 +02:00
Dave Simpson
2921979678
Use macos-13 for Intel build (#2508) 2024-09-24 15:59:16 +02:00
Giacomo Cusinato
a5bf56ffa6 feat: upload using programmer by default if board requires it 2024-09-19 11:57:42 +02:00
Giacomo Cusinato
2de8bd1717 feat: decode grpc status objects and map them to protocol types
Status object thrown by grpc commands contains metadata that needs to be serialized in order to map it to custom errors generated through proto files https://github.com/grpc/grpc-web/issues/399
2024-09-19 11:57:42 +02:00
Giacomo Cusinato
1ec0a8cc77
feat: use Arduino CLI 1.0.4 (#2457)
* fix: use `@pingghost/protoc` to compile proto files

The npm package previously used (`protoc`) is still lacking apple arm32 support, see https://github.com/YePpHa/node-protoc/pull/10

* feat: use Arduino CLI 1.0.4

* fix: allow use of node16 in github actions

* chore: update `arduino-language-server` version for cli-1.0.0

* fix: deprecated platform order test

Arduino deprecated platforms should have more priority then other deprecated ones
2024-09-06 11:38:55 +02:00
Giacomo Cusinato
c3adde5460
feat: add shared space support (#2486) 2024-09-06 10:29:31 +02:00
Dave Simpson
2e78e96b75
[chore] Update Windows signing Cert to eToken (#2452) 2024-07-03 09:42:10 +02:00
Akos Kitta
aa9b10d68e fix: copy example with .pde main sketch file
Closes arduino/arduino-ide#2377

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-22 16:19:10 +01:00
Akos Kitta
4217c0001d fix: add missing installed version to the platform
Closes arduino/arduino-ide#2378

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-21 17:01:28 +01:00
Akos Kitta
a088ba99f5 fix: invalid custom board option handling in FQBN
Closes arduino/arduino-ide#1588

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 13:38:52 +01:00
Akos Kitta
2a325a5b74 feat: cancelable verify+upload
Closes arduino/arduino-ide#1199

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 13:38:52 +01:00
Akos Kitta
347e3d8118 fix: can unset network#proxy in the CLI config
An empty object (`{}`) must be used to correctly unset the CLI config
value to its default.

Closes arduino/arduino-ide#2184

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 13:38:52 +01:00
Akos Kitta
8e09971078 feat: use Arduino CLI 0.36.0-rc.1 APIs
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 13:38:52 +01:00
Akos Kitta
48e7bf6b5d chore: switch to version 2.3.3 after the release
To produce a correctly versioned nightly build.
See the [docs](1b9c7e93e0/docs/internal/release-procedure.md (7-%EF%B8%8F-bump-version-metadata-of-packages)) for more details.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 12:55:39 +01:00
Akos Kitta
95c4399c07 fix(ci): use go 1.21 for the on the fly bin builds
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-19 17:37:27 +01:00
Akos Kitta
4a807ab538 fix: no required programmer for debug --info
Ref: arduino/arduino-cli#2540
Closes: arduino/arduino-ide#2368

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-19 17:37:27 +01:00
Akos Kitta
1a98485b02 fix(security): use ip@2.0.1 for CVE-2023-42282
Refs:
 - https://github.com/advisories/GHSA-78xj-cgh5-2h22
  - 32f468f124
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-19 17:02:23 +01:00
github-actions[bot]
8fe6a81230 Updated translation files 2024-02-19 11:40:19 +01:00
Akos Kitta
547a630598 chore: switch to version 2.3.2 after the release
To produce a correctly versioned nightly build.
See the [docs](1b9c7e93e0/docs/internal/release-procedure.md (7-%EF%B8%8F-bump-version-metadata-of-packages)) for more details.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-15 10:27:29 +01:00
github-actions[bot]
ff8c646cfa Updated translation files 2024-02-14 13:35:47 +01:00
Akos Kitta
316e0fd8be fix(security): update all vulnerable dependencies
Resolutions:
 - `nano@^10.1.3`,
 - `msgpackr@^1.10.1`,
 - `axios@^1.6.7`

Fixes:
 - [GHSA-wf5p-g6vw-rhxx](https://github.com/advisories/GHSA-wf5p-g6vw-rhxx) (`CVE-2023-45857`)
 - [GHSA-jchw-25xp-jwwc](https://github.com/advisories/GHSA-jchw-25xp-jwwc) (`CVE-2023-26159`)
 - [GHSA-7hpj-7hhx-2fgx](https://github.com/advisories/GHSA-7hpj-7hhx-2fgx) (`CVE-2023-52079`)

Ref: nrwl/nx#20493
Ref: eclipse-theia/theia#13365

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-14 09:31:44 +01:00
Akos Kitta
ca779e5cf2 fix: debug widget layout updates
Use customized `PanelLayout#removeWidget` and
`PanelLayout#insertWidget` logic for the layout
updates. The customized functions ensure no side
effect if adding/removing the widget to/from the
layout but it's already present/absent.

Unlike the default [`PanelLayout#removeWidget`](9f5e11025b/packages/widgets/src/panellayout.ts (L154-L156))
behavior, do not try to remove the widget if it's
not present (has a negative index). Otherwise,
required widgets might be removed based on the
default [`ArrayExt#removeAt`](9f5e11025b/packages/algorithm/src/array.ts (L1075-L1077))
behavior.

Closes: arduino/arduino-ide#2354

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-08 14:17:10 +01:00
per1234
74c580175b Use arduino/setup-task@v1 action for Linux build job
Unfortunately the latest v2 version of the arduino/setup-task action used to install the Task task runner tool in the
runner machine has a dependency on a higher version of glibc than is provided by the Linux container. For this reason,
the workflow is configured to use arduino/setup-task@v1 for the Linux build job.

We will receive pull requests from Dependabot offering to update this outdated action dependency at each subsequent
major version release of the action (which are not terribly frequent). We must decline the bump of the action in that
specific step, but we can accept the bumps of all other usages of the action in the workflows. Dependabot remembers when
you decline a bump so this should not be too bothersome.
2024-02-07 20:40:49 -08:00
dependabot[bot]
71bd189eb1 build(deps): Bump arduino/setup-task from 1 to 2
Bumps [arduino/setup-task](https://github.com/arduino/setup-task) from 1 to 2.
- [Release notes](https://github.com/arduino/setup-task/releases)
- [Commits](https://github.com/arduino/setup-task/compare/v1...v2)

---
updated-dependencies:
- dependency-name: arduino/setup-task
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-07 20:40:49 -08:00
Akos Kitta
0822ed28da chore: switch to version 2.3.1 after the release
To produce a correctly versioned nightly build.
See the [docs](1b9c7e93e0/docs/internal/release-procedure.md (7-%EF%B8%8F-bump-version-metadata-of-packages)) for more details.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-07 19:05:11 +01:00
211 changed files with 28139 additions and 11473 deletions

View File

@ -1,43 +1,28 @@
# The Arduino IDE Linux build workflow job runs in this container.
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
# See: https://hub.docker.com/_/ubuntu/tags
FROM ubuntu:18.10
# See: https://unofficial-builds.nodejs.org/download/release/
ARG node_version="18.17.1"
# This is required in order to use the Ubuntu package repositories for EOL Ubuntu versions:
# https://help.ubuntu.com/community/EOLUpgrades#Update_sources.list
RUN \
sed \
--in-place \
--regexp-extended \
--expression='s/([a-z]{2}\.)?archive.ubuntu.com|security.ubuntu.com/old-releases.ubuntu.com/g' \
"/etc/apt/sources.list"
RUN \
apt-get \
--yes \
update
# This is required to get add-apt-repository
RUN \
apt-get \
--yes \
install \
"software-properties-common=0.96.24.32.22"
# Install Git
# The PPA is required to get a modern version of Git. The version in the Ubuntu 18.04 package repository is 2.17.1,
# while action/checkout@v3 requires 2.18 or higher.
RUN \
add-apt-repository \
--yes \
"ppa:git-core/ppa" && \
apt-get \
--yes \
update && \
\
apt-get \
--yes \
install \
"git" && \
\
apt-get \
--yes \
purge \
"software-properties-common"
"git"
# The repository path must be added to safe.directory, otherwise any Git operations on it would fail with a
# "dubious ownership" error. actions/checkout configures this, but it is not applied to containers.
@ -51,18 +36,12 @@ ENV \
# Install Python
# The Python installed by actions/setup-python has dependency on a higher version of glibc than available in the
# ubuntu:18.04 container.
# container.
RUN \
apt-get \
--yes \
install \
"python3.8-minimal=3.8.0-3ubuntu1~18.04.2" && \
\
ln \
--symbolic \
--force \
"$(which python3.8)" \
"/usr/bin/python3"
"python3.7-minimal=3.7.3-2~18.10"
# Install Theia's package dependencies
# These are pre-installed in the GitHub Actions hosted runner machines.
@ -70,43 +49,15 @@ RUN \
apt-get \
--yes \
install \
"libsecret-1-dev=0.18.6-1" \
"libx11-dev=2:1.6.4-3ubuntu0.4" \
"libsecret-1-dev=0.18.6-3" \
"libx11-dev=2:1.6.7-1" \
"libxkbfile-dev=1:1.0.9-2"
# Install Node.js
# It is necessary to use the "unofficial" linux-x64-glibc-217 build because the official Node.js 18.x is dynamically
# linked against glibc 2.28, while Ubuntu 18.04 has glibc 2.27.
ARG node_installation_path="/tmp/node-installation"
ARG artifact_name="node-v${node_version}-linux-x64-glibc-217"
# Target python3 symlink to Python 3.7 installation. It would otherwise target version 3.6 due to the installation of
# the `python3` package as a transitive dependency.
RUN \
mkdir "$node_installation_path" && \
cd "$node_installation_path" && \
\
apt-get \
--yes \
install \
"wget=1.19.4-1ubuntu2.2" && \
\
archive_name="${artifact_name}.tar.xz" && \
wget \
"https://unofficial-builds.nodejs.org/download/release/v${node_version}/${archive_name}" && \
\
apt-get \
--yes \
purge \
"wget" && \
\
tar \
--file="$archive_name" \
--extract && \
rm "$archive_name"
ENV PATH="${PATH}:${node_installation_path}/${artifact_name}/bin"
# Install Yarn
# Yarn is pre-installed in the GitHub Actions hosted runner machines.
RUN \
npm \
install \
--global \
"yarn@1.22.19"
ln \
--symbolic \
--force \
"$(which python3.7)" \
"/usr/bin/python3"

View File

@ -40,36 +40,46 @@ on:
- Push Container Images
branches:
- main
types:
types:
- completed
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.19'
GO_VERSION: '1.21'
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: '18.17'
JOB_TRANSFER_ARTIFACT: build-artifacts
YARN_VERSION: '1.22'
JOB_TRANSFER_ARTIFACT_PREFIX: build-artifacts-
CHANGELOG_ARTIFACTS: changelog
STAGED_CHANNEL_FILES_ARTIFACT: staged-channel-files
STAGED_CHANNEL_FILE_ARTIFACT_PREFIX: staged-channel-file-
BASE_BUILD_DATA: |
- config:
# Human identifier for the job.
name: Windows
runs-on: windows-2019
runs-on: [self-hosted, windows-sign-pc]
# The value is a string representing a JSON document.
# Setting this to null causes the job to run directly in the runner machine instead of in a container.
container: |
null
# Name of the secret that contains the certificate.
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
certificate-secret: INSTALLER_CERT_WINDOWS_CER
# Name of the secret that contains the certificate password.
certificate-password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
certificate-password-secret: INSTALLER_CERT_WINDOWS_PASSWORD
# File extension for the certificate.
certificate-extension: pfx
# Container for windows cert signing
certificate-container: INSTALLER_CERT_WINDOWS_CONTAINER
# Arbitrary identifier used to give the workflow artifact uploaded by each "build" matrix job a unique name.
job-transfer-artifact-suffix: Windows_64bit
# Quoting on the value is required here to allow the same comparison expression syntax to be used for this
# and the companion needs.select-targets.outputs.merge-channel-files property (output values always have string
# type).
mergeable-channel-file: 'false'
# as this runs on a self hosted runner, we need to avoid building with the default working directory path,
# otherwise paths in the build job will be too long for `light.exe`
# we use the below as a Symbolic link (just changing the wd will break the checkout action)
# this is a work around (see: https://github.com/actions/checkout/issues/197).
working-directory: 'C:\a'
artifacts:
- path: '*Windows_64bit.exe'
name: Windows_X86-64_interactive_installer
@ -84,6 +94,7 @@ env:
{
\"image\": \"ghcr.io/arduino/arduino-ide/linux:main\"
}
job-transfer-artifact-suffix: Linux_64bit
mergeable-channel-file: 'false'
artifacts:
- path: '*Linux_64bit.zip'
@ -92,7 +103,7 @@ env:
name: Linux_X86-64_app_image
- config:
name: macOS x86
runs-on: macos-latest
runs-on: macos-13
container: |
null
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
@ -100,27 +111,32 @@ env:
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
certificate-password-secret: KEYCHAIN_PASSWORD
certificate-extension: p12
job-transfer-artifact-suffix: macOS_64bit
mergeable-channel-file: 'true'
artifacts:
- path: '*macOS_64bit.dmg'
name: macOS_X86-64_dmg
- path: '*macOS_64bit.zip'
name: macOS_X86-64_zip
PAID_RUNNER_BUILD_DATA: |
- config:
name: macOS ARM
runs-on: macos-latest-xlarge
runs-on: macos-latest
container: |
null
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
certificate-password-secret: KEYCHAIN_PASSWORD
certificate-extension: p12
job-transfer-artifact-suffix: macOS_arm64
mergeable-channel-file: 'true'
artifacts:
- path: '*macOS_arm64.dmg'
name: macOS_arm64_dmg
- path: '*macOS_arm64.zip'
name: macOS_arm64_zip
PAID_RUNNER_BUILD_DATA: |
# This system was implemented to allow selective use of paid GitHub-hosted runners, due to the Apple Silicon runner
# incurring a charge at that time. Free Apple Silicon runners are now available so the configuration was moved to
# `BASE_BUILD_DATA`, but the system was left in place for future use.
jobs:
run-determination:
@ -156,6 +172,7 @@ jobs:
is-nightly: ${{ steps.determination.outputs.is-nightly }}
channel-name: ${{ steps.determination.outputs.channel-name }}
publish-to-s3: ${{ steps.determination.outputs.publish-to-s3 }}
environment: production
permissions: {}
steps:
- name: Determine the type of build
@ -187,7 +204,7 @@ jobs:
echo "is-nightly=$is_nightly" >> $GITHUB_OUTPUT
echo "channel-name=$channel_name" >> $GITHUB_OUTPUT
# Only attempt upload to Amazon S3 if the credentials are available.
echo "publish-to-s3=${{ secrets.AWS_SECRET_ACCESS_KEY != '' }}" >> $GITHUB_OUTPUT
echo "publish-to-s3=${{ secrets.AWS_ROLE_ARN != '' }}" >> $GITHUB_OUTPUT
select-targets:
needs: build-type-determination
@ -223,7 +240,7 @@ jobs:
) | \
yq \
--output-format json \
'[.[].artifacts.[]]'
'map(.artifacts[] + (.config | pick(["job-transfer-artifact-suffix"])))'
)"
# The build matrix produces two macOS jobs (x86 and ARM) so the "channel update info files"
@ -242,7 +259,7 @@ jobs:
echo "${{ env.BASE_BUILD_DATA }}" | \
yq \
--output-format json \
'[.[].artifacts.[]]'
'map(.artifacts[] + (.config | pick(["job-transfer-artifact-suffix"])))'
)"
merge_channel_files="false"
@ -270,6 +287,16 @@ jobs:
env:
# Location of artifacts generated by build.
BUILD_ARTIFACTS_PATH: electron-app/dist/build-artifacts
# to skip passing signing credentials to electron-builder
IS_WINDOWS_CONFIG: ${{ matrix.config.name == 'Windows' }}
INSTALLER_CERT_WINDOWS_CER: "/tmp/cert.cer"
# We are hardcoding the path for signtool because is not present on the windows PATH env var by default.
# Keep in mind that this path could change when upgrading to a new runner version
SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.19041.0/x86/signtool.exe"
WIN_CERT_PASSWORD: ${{ secrets[matrix.config.certificate-password-secret] }}
WIN_CERT_CONTAINER_NAME: ${{ secrets[matrix.config.certificate-container] }}
PUPPETEER_SKIP_DOWNLOAD: true
strategy:
matrix:
config: ${{ fromJson(needs.select-targets.outputs.build-matrix) }}
@ -283,76 +310,66 @@ jobs:
timeout-minutes: 90
steps:
- name: Symlink custom working directory
shell: cmd
if: runner.os == 'Windows' && matrix.config.working-directory
run: |
if not exist "${{ matrix.config.working-directory }}" mklink /d "${{ matrix.config.working-directory }}" "C:\actions-runner\_work\arduino-ide\arduino-ide"
- name: Checkout
if: fromJSON(matrix.config.container) == null
uses: actions/checkout@v4
- name: Checkout
# actions/checkout@v4 has dependency on a higher version of glibc than available in the Linux container.
if: fromJSON(matrix.config.container) != null
uses: actions/checkout@v3
- name: Install Node.js
if: fromJSON(matrix.config.container) == null
if: runner.name != 'WINDOWS-SIGN-PC'
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
# Yarn is a prerequisite for the action's cache feature, so caching should be disabled when running in the
# container where Yarn is not pre-installed.
cache: ${{ fromJSON(matrix.config.container) == null && 'yarn' || null }}
- name: Install Yarn
if: runner.name != 'WINDOWS-SIGN-PC'
run: |
npm \
install \
--global \
"yarn@${{ env.YARN_VERSION }}"
- name: Install Python 3.x
if: fromJSON(matrix.config.container) == null
if: fromJSON(matrix.config.container) == null && runner.name != 'WINDOWS-SIGN-PC'
uses: actions/setup-python@v5
with:
python-version: '3.11.x'
- name: Install Go
if: fromJSON(matrix.config.container) == null
if: runner.name != 'WINDOWS-SIGN-PC'
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Go
# actions/setup-go@v5 has dependency on a higher version of glibc than available in the Linux container.
if: fromJSON(matrix.config.container) != null
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
if: fromJSON(matrix.config.container) == null
if: runner.name != 'WINDOWS-SIGN-PC'
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install Taskfile
# actions/setup-task@v2 has dependency on a higher version of glibc than available in the Linux container.
if: fromJSON(matrix.config.container) != null
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Package
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
IS_NIGHTLY: ${{ needs.build-type-determination.outputs.is-nightly }}
IS_RELEASE: ${{ needs.build-type-determination.outputs.is-release }}
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
# The CREATE_* environment vars are only used to run tests. These secrets are optional. Dependent tests will
# be skipped if not available.
CREATE_USERNAME: ${{ secrets.CREATE_USERNAME }}
CREATE_PASSWORD: ${{ secrets.CREATE_PASSWORD }}
CREATE_CLIENT_SECRET: ${{ secrets.CREATE_CLIENT_SECRET }}
working-directory: ${{ matrix.config.working-directory || './' }}
run: |
# See: https://www.electron.build/code-signing
if [ $CAN_SIGN = false ]; then
if [ $CAN_SIGN = false ] || [ $IS_WINDOWS_CONFIG = true ]; then
echo "Skipping the app signing: certificate not provided."
else
export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}"
@ -362,13 +379,9 @@ jobs:
fi
npx node-gyp install
yarn install --immutable
yarn install
yarn --cwd arduino-ide-extension build
yarn test
yarn --cwd arduino-ide-extension test:slow
yarn --cwd arduino-ide-extension lint
yarn --cwd electron-app rebuild
yarn --cwd electron-app build
yarn --cwd electron-app package
@ -379,6 +392,7 @@ jobs:
if: >
needs.select-targets.outputs.merge-channel-files == 'true' &&
matrix.config.mergeable-channel-file == 'true'
working-directory: ${{ matrix.config.working-directory || './' }}
run: |
staged_channel_files_path="${{ runner.temp }}/staged-channel-files"
mkdir "$staged_channel_files_path"
@ -391,20 +405,26 @@ jobs:
echo "STAGED_CHANNEL_FILES_PATH=$staged_channel_files_path" >> "$GITHUB_ENV"
- name: Upload staged-for-merge channel file artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: >
needs.select-targets.outputs.merge-channel-files == 'true' &&
matrix.config.mergeable-channel-file == 'true'
with:
if-no-files-found: error
name: ${{ env.STAGED_CHANNEL_FILES_ARTIFACT }}
path: ${{ env.STAGED_CHANNEL_FILES_PATH }}
name: ${{ env.STAGED_CHANNEL_FILE_ARTIFACT_PREFIX }}${{ matrix.config.job-transfer-artifact-suffix }}
path: ${{ matrix.config.working-directory && format('{0}/{1}', matrix.config.working-directory, env.STAGED_CHANNEL_FILES_PATH) || env.STAGED_CHANNEL_FILES_PATH }}
- name: Upload [GitHub Actions]
uses: actions/upload-artifact@v3
- name: Upload builds to job transfer artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.BUILD_ARTIFACTS_PATH }}
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}${{ matrix.config.job-transfer-artifact-suffix }}
path: ${{ matrix.config.working-directory && format('{0}/{1}', matrix.config.working-directory, env.BUILD_ARTIFACTS_PATH) || env.BUILD_ARTIFACTS_PATH }}
- name: Manual Clean up for self-hosted runners
if: runner.os == 'Windows' && matrix.config.working-directory
shell: cmd
run: |
rmdir /s /q "${{ matrix.config.working-directory }}\${{ env.BUILD_ARTIFACTS_PATH }}"
merge-channel-files:
needs:
@ -423,16 +443,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Download staged-for-merge channel files artifact
uses: actions/download-artifact@v3
- name: Download staged-for-merge channel file artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.STAGED_CHANNEL_FILES_ARTIFACT }}
merge-multiple: true
path: ${{ env.CHANNEL_FILES_PATH }}
pattern: ${{ env.STAGED_CHANNEL_FILE_ARTIFACT_PREFIX }}*
- name: Remove no longer needed artifact
uses: geekyeggo/delete-artifact@v2
- name: Remove no longer needed artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: ${{ env.STAGED_CHANNEL_FILES_ARTIFACT }}
name: ${{ env.STAGED_CHANNEL_FILE_ARTIFACT_PREFIX }}*
- name: Install Node.js
uses: actions/setup-node@v4
@ -452,6 +473,12 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn
@ -462,11 +489,11 @@ jobs:
--channel "${{ needs.build-type-determination.outputs.channel-name }}" \
--input "${{ env.CHANNEL_FILES_PATH }}"
- name: Upload merged channel files to job transfer artifact
uses: actions/upload-artifact@v3
- name: Upload merged channel files job transfer artifact
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}channel-files
path: ${{ env.CHANNEL_FILES_PATH }}
artifacts:
@ -477,22 +504,25 @@ jobs:
if: always() && needs.build.result != 'skipped'
runs-on: ubuntu-latest
env:
BUILD_ARTIFACTS_FOLDER: build-artifacts
strategy:
matrix:
artifact: ${{ fromJson(needs.select-targets.outputs.artifact-matrix) }}
steps:
- name: Download job transfer artifact
uses: actions/download-artifact@v3
- name: Download job transfer artifact that contains ${{ matrix.artifact.name }} tester build
uses: actions/download-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}${{ matrix.artifact.job-transfer-artifact-suffix }}
path: ${{ env.BUILD_ARTIFACTS_FOLDER }}
- name: Upload tester build artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact.name }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }}
path: ${{ env.BUILD_ARTIFACTS_FOLDER }}/${{ matrix.artifact.path }}
changelog:
needs:
@ -535,11 +565,11 @@ jobs:
echo "$BODY" > CHANGELOG.txt
- name: Upload Changelog [GitHub Actions]
- name: Upload changelog job transfer artifact
if: needs.build-type-determination.outputs.is-nightly == 'true'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}changelog
path: CHANGELOG.txt
publish:
@ -558,22 +588,33 @@ jobs:
needs.build-type-determination.outputs.publish-to-s3 == 'true' &&
needs.build-type-determination.outputs.is-nightly == 'true'
runs-on: ubuntu-latest
env:
ARTIFACTS_FOLDER: build-artifacts
environment: production
permissions:
id-token: write
contents: read
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v3
- name: Download all job transfer artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
merge-multiple: true
path: ${{ env.ARTIFACTS_FOLDER }}
pattern: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}*
- name: Configure AWS Credentials for Nightly [S3]
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Publish Nightly [S3]
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
PLUGIN_TARGET: '/arduino-ide/nightly'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ${{ env.ARTIFACTS_FOLDER }} s3://${{ secrets.DOWNLOADS_BUCKET }}/arduino-ide/nightly
release:
needs:
@ -590,12 +631,23 @@ jobs:
needs.changelog.result == 'success' &&
needs.build-type-determination.outputs.is-release == 'true'
runs-on: ubuntu-latest
env:
ARTIFACTS_FOLDER: build-artifacts
environment: production
permissions:
id-token: write
contents: write
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v3
- name: Download all job transfer artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
merge-multiple: true
path: ${{ env.ARTIFACTS_FOLDER }}
pattern: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}*
- name: Get Tag
id: tag_name
@ -603,25 +655,26 @@ jobs:
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@2.7.0
uses: svenstaro/upload-release-action@2.9.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
file: ${{ env.JOB_TRANSFER_ARTIFACT }}/*
file: ${{ env.ARTIFACTS_FOLDER }}/*
tag: ${{ github.ref }}
file_glob: true
body: ${{ needs.changelog.outputs.BODY }}
- name: Configure AWS Credentials for Release [S3]
if: needs.build-type-determination.outputs.publish-to-s3 == 'true'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Publish Release [S3]
if: needs.build-type-determination.outputs.publish-to-s3 == 'true'
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
PLUGIN_TARGET: '/arduino-ide'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ${{ env.ARTIFACTS_FOLDER }} s3://${{ secrets.DOWNLOADS_BUCKET }}/arduino-ide
clean:
# This job must run after all jobs that use the transfer artifact.
@ -635,7 +688,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Remove unneeded job transfer artifact
uses: geekyeggo/delete-artifact@v2
- name: Remove unneeded job transfer artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}*

View File

@ -74,9 +74,11 @@ jobs:
- identifier: macOS signing certificate # Text used to identify certificate in notifications.
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # Name of the secret that contains the certificate.
password-secret: KEYCHAIN_PASSWORD # Name of the secret that contains the certificate password.
type: pkcs12
- identifier: Windows signing certificate
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
certificate-secret: INSTALLER_CERT_WINDOWS_CER
# The password for the Windows certificate is not needed, because its not a container, but a single certificate.
type: x509
steps:
- name: Set certificate path environment variable
@ -95,7 +97,7 @@ jobs:
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
run: |
(
openssl pkcs12 \
openssl ${{ matrix.certificate.type }} \
-in "${{ env.CERTIFICATE_PATH }}" \
-legacy \
-noout \
@ -122,26 +124,43 @@ jobs:
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
id: get-days-before-expiration
run: |
EXPIRATION_DATE="$(
(
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-clcerts \
-legacy \
-nodes \
-passin env:CERTIFICATE_PASSWORD
) | (
openssl x509 \
-noout \
-enddate
) | (
grep \
--max-count=1 \
--only-matching \
--perl-regexp \
'notAfter=(\K.*)'
)
)"
if [[ ${{ matrix.certificate.type }} == "pkcs12" ]]; then
EXPIRATION_DATE="$(
(
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-clcerts \
-legacy \
-nodes \
-passin env:CERTIFICATE_PASSWORD
) | (
openssl x509 \
-noout \
-enddate
) | (
grep \
--max-count=1 \
--only-matching \
--perl-regexp \
'notAfter=(\K.*)'
)
)"
elif [[ ${{ matrix.certificate.type }} == "x509" ]]; then
EXPIRATION_DATE="$(
(
openssl x509 \
-in ${{ env.CERTIFICATE_PATH }} \
-noout \
-enddate
) | (
grep \
--max-count=1 \
--only-matching \
--perl-regexp \
'notAfter=(\K.*)'
)
)"
fi
DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"

View File

@ -43,7 +43,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and push to local registry
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.image.path }}

View File

@ -2,7 +2,7 @@ name: Check Internationalization
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.19'
GO_VERSION: '1.21'
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
@ -76,6 +76,12 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn install --immutable
env:

94
.github/workflows/check-javascript.yml vendored Normal file
View File

@ -0,0 +1,94 @@
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/check-javascript-task.md
name: Check JavaScript
env:
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 18.17
# See: https://docs.github.com/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows
on:
create:
push:
paths:
- '.github/workflows/check-javascript.ya?ml'
- '**/.eslintignore'
- '**/.eslintrc*'
- '**/.npmrc'
- '**/package.json'
- '**/package-lock.json'
- '**/yarn.lock'
- '**.jsx?'
pull_request:
paths:
- '.github/workflows/check-javascript.ya?ml'
- '**/.eslintignore'
- '**/.eslintrc*'
- '**/.npmrc'
- '**/package.json'
- '**/package-lock.json'
- '**/yarn.lock'
- '**.jsx?'
workflow_dispatch:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
permissions: {}
outputs:
result: ${{ steps.determination.outputs.result }}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
check:
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ env.NODE_VERSION }}
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install npm package dependencies
env:
# Avoid failure of @vscode/ripgrep installation due to GitHub API rate limiting:
# https://github.com/microsoft/vscode-ripgrep#github-api-limit-note
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
yarn install
- name: Lint
run: |
yarn \
--cwd arduino-ide-extension \
lint

97
.github/workflows/check-yarn.yml vendored Normal file
View File

@ -0,0 +1,97 @@
name: Check Yarn
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
create:
push:
paths:
- ".github/workflows/check-yarn.ya?ml"
- "**/.yarnrc"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
pull_request:
paths:
- ".github/workflows/check-yarn.ya?ml"
- "**/.yarnrc"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
schedule:
# Run every Tuesday at 8 AM UTC to catch breakage resulting from changes to the JSON schema.
- cron: "0 8 * * TUE"
workflow_dispatch:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
permissions: {}
outputs:
result: ${{ steps.determination.outputs.result }}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
check-sync:
name: check-sync (${{ matrix.project.path }})
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
project:
- path: .
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ env.NODE_VERSION }}
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install npm package dependencies
env:
# Avoid failure of @vscode/ripgrep installation due to GitHub API rate limiting:
# https://github.com/microsoft/vscode-ripgrep#github-api-limit-note
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
yarn \
install \
--ignore-scripts
- name: Check yarn.lock
run: |
git \
diff \
--color \
--exit-code \
"${{ matrix.project.path }}/yarn.lock"

View File

@ -14,6 +14,11 @@ jobs:
create-changelog:
if: github.repository == 'arduino/arduino-ide'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
@ -24,6 +29,12 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Get Tag
id: tag_name
run: |
@ -44,12 +55,12 @@ jobs:
# Compose changelog
yarn run compose-changelog "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/$CHANGELOG_FILE_NAME"
- name: Configure AWS Credentials for Changelog [S3]
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Publish Changelog [S3]
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: '${{ env.CHANGELOG_ARTIFACTS }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.CHANGELOG_ARTIFACTS }}/'
PLUGIN_TARGET: '/arduino-ide/changelog'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ${{ env.CHANGELOG_ARTIFACTS }} s3://${{ secrets.DOWNLOADS_BUCKET }}/arduino-ide/changelog

View File

@ -2,7 +2,7 @@ name: i18n-nightly-push
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.19'
GO_VERSION: '1.21'
on:
schedule:
@ -34,6 +34,12 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn install --immutable

View File

@ -2,7 +2,7 @@ name: i18n-weekly-pull
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.19'
GO_VERSION: '1.21'
on:
schedule:
@ -34,6 +34,12 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn install --immutable
@ -46,7 +52,7 @@ jobs:
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v7
with:
commit-message: Updated translation files
title: Update translation files

View File

@ -59,7 +59,7 @@ jobs:
images: ${{ matrix.image.registry }}/${{ matrix.image.name }}
- name: Build and push image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.image.path }}

View File

@ -19,7 +19,7 @@ on:
env:
CONFIGURATIONS_FOLDER: .github/label-configuration-files
CONFIGURATIONS_ARTIFACT: label-configuration-files
CONFIGURATIONS_ARTIFACT_PREFIX: label-configuration-file-
jobs:
check:
@ -71,13 +71,13 @@ jobs:
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }}
- name: Pass configuration files to next job via workflow artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
path: |
*.yaml
*.yml
if-no-files-found: error
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
name: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}${{ matrix.filename }}
sync:
needs: download
@ -108,16 +108,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download configuration files artifact
uses: actions/download-artifact@v3
- name: Download configuration file artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
merge-multiple: true
pattern: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}*
path: ${{ env.CONFIGURATIONS_FOLDER }}
- name: Remove unneeded artifact
uses: geekyeggo/delete-artifact@v2
- name: Remove unneeded artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
name: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}*
- name: Merge label configuration files
run: |

140
.github/workflows/test-javascript.yml vendored Normal file
View File

@ -0,0 +1,140 @@
name: Test JavaScript
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.21'
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 18.17
on:
push:
paths:
- ".github/workflows/test-javascript.ya?ml"
- "**/.mocharc.js"
- "**/.mocharc.jsonc?"
- "**/.mocharc.ya?ml"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
- "tests/testdata/**"
- "**/tsconfig.json"
- "**.[jt]sx?"
pull_request:
paths:
- ".github/workflows/test-javascript.ya?ml"
- "**/.mocharc.js"
- "**/.mocharc.jsonc?"
- "**/.mocharc.ya?ml"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
- "tests/testdata/**"
- "**/tsconfig.json"
- "**.[jt]sx?"
workflow_dispatch:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
permissions: {}
outputs:
result: ${{ steps.determination.outputs.result }}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
test:
name: test (${{ matrix.project.path }}, ${{ matrix.operating-system }})
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ${{ matrix.operating-system }}
defaults:
run:
shell: bash
permissions:
contents: read
strategy:
fail-fast: false
matrix:
project:
- path: .
operating-system:
- macos-latest
- ubuntu-latest
- windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ env.NODE_VERSION }}
# See: https://github.com/eclipse-theia/theia/blob/master/doc/Developing.md#prerequisites
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: '3.11.x'
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install npm package dependencies
env:
# Avoid failure of @vscode/ripgrep installation due to GitHub API rate limiting:
# https://github.com/microsoft/vscode-ripgrep#github-api-limit-note
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
yarn install
- name: Compile TypeScript
run: |
yarn \
--cwd arduino-ide-extension \
build
- name: Run tests
env:
# These secrets are optional. Dependent tests will be skipped if not available.
CREATE_USERNAME: ${{ secrets.CREATE_USERNAME }}
CREATE_PASSWORD: ${{ secrets.CREATE_PASSWORD }}
CREATE_CLIENT_SECRET: ${{ secrets.CREATE_CLIENT_SECRET }}
run: |
yarn test
yarn \
--cwd arduino-ide-extension \
test:slow

View File

@ -8,7 +8,7 @@ on:
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.19'
GO_VERSION: '1.21'
NODE_VERSION: '18.17'
jobs:
@ -36,6 +36,12 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn install --immutable
@ -55,7 +61,7 @@ jobs:
run: yarn run themes:generate
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v7
with:
commit-message: Updated themes
title: Update themes

View File

@ -2,7 +2,9 @@
# Arduino IDE 2.x
[![Arduino IDE](https://github.com/arduino/arduino-ide/workflows/Arduino%20IDE/badge.svg)](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
[![Build status](https://github.com/arduino/arduino-ide/actions/workflows/build.yml/badge.svg)](https://github.com/arduino/arduino-ide/actions/workflows/build.yml)
[![Check JavaScript status](https://github.com/arduino/arduino-ide/actions/workflows/check-javascript.yml/badge.svg)](https://github.com/arduino/arduino-ide/actions/workflows/check-javascript.yml)
[![Test JavaScript status](https://github.com/arduino/arduino-ide/actions/workflows/test-javascript.yml/badge.svg)](https://github.com/arduino/arduino-ide/actions/workflows/test-javascript.yml)
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the [repository of the 1.x version](https://github.com/arduino/Arduino).

View File

@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "2.3.1",
"version": "2.3.7",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
@ -13,7 +13,7 @@
"download-ls": "node ./scripts/download-ls.js",
"download-examples": "node ./scripts/download-examples.js",
"generate-protocol": "node ./scripts/generate-protocol.js",
"lint": "eslint",
"lint": "eslint .",
"prebuild": "rimraf lib",
"build": "tsc",
"build:dev": "yarn build",
@ -24,27 +24,29 @@
},
"dependencies": {
"@grpc/grpc-js": "^1.8.14",
"@theia/application-package": "1.41.0",
"@theia/core": "1.41.0",
"@theia/debug": "1.41.0",
"@theia/editor": "1.41.0",
"@theia/electron": "1.41.0",
"@theia/filesystem": "1.41.0",
"@theia/keymaps": "1.41.0",
"@theia/markers": "1.41.0",
"@theia/messages": "1.41.0",
"@theia/monaco": "1.41.0",
"@theia/monaco-editor-core": "1.72.3",
"@theia/navigator": "1.41.0",
"@theia/outline-view": "1.41.0",
"@theia/output": "1.41.0",
"@theia/plugin-ext": "1.41.0",
"@theia/preferences": "1.41.0",
"@theia/scm": "1.41.0",
"@theia/search-in-workspace": "1.41.0",
"@theia/terminal": "1.41.0",
"@theia/typehierarchy": "1.41.0",
"@theia/workspace": "1.41.0",
"@theia/application-package": "1.57.0",
"@theia/core": "1.57.0",
"@theia/debug": "1.57.0",
"@theia/editor": "1.57.0",
"@theia/electron": "1.57.0",
"@theia/filesystem": "1.57.0",
"@theia/keymaps": "1.57.0",
"@theia/markers": "1.57.0",
"@theia/messages": "1.57.0",
"@theia/monaco": "1.57.0",
"@theia/monaco-editor-core": "1.83.101",
"@theia/navigator": "1.57.0",
"@theia/outline-view": "1.57.0",
"@theia/output": "1.57.0",
"@theia/plugin-ext": "1.57.0",
"@theia/plugin-ext-vscode": "1.57.0",
"@theia/preferences": "1.57.0",
"@theia/scm": "1.57.0",
"@theia/search-in-workspace": "1.57.0",
"@theia/terminal": "1.57.0",
"@theia/test": "1.57.0",
"@theia/typehierarchy": "1.57.0",
"@theia/workspace": "1.57.0",
"@tippyjs/react": "^4.2.5",
"@types/auth0-js": "^9.21.3",
"@types/btoa": "^1.2.3",
@ -56,7 +58,6 @@
"@types/node-fetch": "^2.5.7",
"@types/p-queue": "^2.3.1",
"@types/ps-tree": "^1.1.0",
"@types/react-tabs": "^2.3.2",
"@types/temp": "^0.8.34",
"arduino-serial-plotter-webapp": "0.2.0",
"async-mutex": "^0.3.0",
@ -66,13 +67,15 @@
"cross-fetch": "^3.1.5",
"dateformat": "^3.0.3",
"deepmerge": "^4.2.2",
"dompurify": "^2.4.7",
"drivelist": "^9.2.4",
"electron-updater": "^4.6.5",
"fast-deep-equal": "^3.1.3",
"fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1",
"filename-reserved-regex": "^2.0.0",
"glob": "^7.1.6",
"fqbn": "^1.0.5",
"glob": "10.4.4",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-online": "^10.0.0",
@ -96,7 +99,7 @@
"react-markdown": "^8.0.0",
"react-perfect-scrollbar": "^1.5.8",
"react-select": "^5.6.0",
"react-tabs": "^3.1.2",
"react-tabs": "^6.1.0",
"react-window": "^1.8.6",
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
@ -123,11 +126,11 @@
"mockdate": "^3.0.5",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"rimraf": "^2.6.1"
"rimraf": "^5.0.0"
},
"optionalDependencies": {
"grpc-tools": "^1.12.4",
"protoc": "^1.0.4"
"@pingghost/protoc": "^1.0.2",
"grpc-tools": "^1.12.4"
},
"mocha": {
"require": [
@ -169,13 +172,17 @@
],
"arduino": {
"arduino-cli": {
"version": "0.35.2"
"version": "1.2.0"
},
"arduino-fwuploader": {
"version": "2.4.1"
},
"arduino-language-server": {
"version": "0.7.6"
"version": {
"owner": "arduino",
"repo": "arduino-language-server",
"commitish": "05ec308"
}
},
"clangd": {
"version": "14.0.0"

View File

@ -34,7 +34,7 @@
}, '');
const args = process.argv.slice(2);
if (args.length == 0) {
if (args.length === 0) {
console.error('Missing argument to destination file');
process.exit(1);
}

View File

@ -1,7 +1,7 @@
// @ts-check
// The version to use.
const version = '1.10.1';
const version = '1.10.2';
(async () => {
const os = require('node:os');

View File

@ -3,13 +3,15 @@
(async () => {
const os = require('node:os');
const path = require('node:path');
const { mkdirSync, promises: fs } = require('node:fs');
const decompress = require('decompress');
const unzip = require('decompress-unzip');
const { mkdirSync, promises: fs, rmSync, existsSync } = require('node:fs');
const { exec } = require('./utils');
const glob = require('glob');
const { SemVer, gte, valid: validSemVer } = require('semver');
const protoc = path.dirname(require('protoc/protoc'));
const repository = await fs.mkdtemp(path.join(os.tmpdir(), 'arduino-cli-'));
const { glob } = require('glob');
const { SemVer, gte, valid: validSemVer, eq } = require('semver');
// Use a node-protoc fork until apple arm32 is supported
// https://github.com/YePpHa/node-protoc/pull/10
const protoc = path.dirname(require('@pingghost/protoc/protoc'));
const { owner, repo, commitish } = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
@ -56,11 +58,6 @@
return { owner, repo, commitish };
})();
const url = `https://github.com/${owner}/${repo}.git`;
console.log(`>>> Cloning repository from '${url}'...`);
exec('git', ['clone', url, repository], { logStdout: true });
console.log(`<<< Repository cloned.`);
const { platform } = process;
const resourcesFolder = path.join(
__dirname,
@ -86,108 +83,207 @@
// - `git-snapshot` for local build executed via `task build`. We do not do this.
// - rest, we assume it is a valid semver and has the corresponding tagged code, we use the tag to generate the APIs from the `proto` files.
/*
{
"Application": "arduino-cli",
"VersionString": "nightly-20210126",
"Commit": "079bb6c6",
"Status": "alpha",
"Date": "2021-01-26T01:46:31Z"
}
*/
const versionObject = JSON.parse(versionJson);
let version = versionObject.VersionString;
if (validSemVer(version)) {
// https://github.com/arduino/arduino-cli/pull/2374
if (gte(new SemVer(version, { loose: true }), new SemVer('0.35.0-rc.1'))) {
version = `v${version}`;
{
"Application": "arduino-cli",
"VersionString": "nightly-20210126",
"Commit": "079bb6c6",
"Status": "alpha",
"Date": "2021-01-26T01:46:31Z"
}
console.log(`>>> Checking out tagged version: '${version}'...`);
exec('git', ['-C', repository, 'fetch', '--all', '--tags'], {
logStdout: true,
});
exec(
'git',
['-C', repository, 'checkout', `tags/${version}`, '-b', version],
{ logStdout: true }
);
console.log(`<<< Checked out tagged version: '${version}'.`);
} else if (commitish) {
console.log(
`>>> Checking out commitish from 'package.json': '${commitish}'...`
);
exec('git', ['-C', repository, 'checkout', commitish], { logStdout: true });
console.log(
`<<< Checked out commitish from 'package.json': '${commitish}'.`
);
} else if (versionObject.Commit) {
console.log(
`>>> Checking out commitish from the CLI: '${versionObject.Commit}'...`
);
exec('git', ['-C', repository, 'checkout', versionObject.Commit], {
logStdout: true,
});
console.log(
`<<< Checked out commitish from the CLI: '${versionObject.Commit}'.`
);
} else {
console.log(`WARN: no 'git checkout'. Generating from the HEAD revision.`);
*/
const versionObject = JSON.parse(versionJson);
async function globProtos(folder, pattern = '**/*.proto') {
let protos = [];
try {
const matches = await glob(pattern, { cwd: folder });
protos = matches.map((filename) => path.join(folder, filename));
} catch (error) {
console.log(error.stack ?? error.message);
}
return protos;
}
console.log('>>> Generating TS/JS API from:');
exec('git', ['-C', repository, 'rev-parse', '--abbrev-ref', 'HEAD'], {
logStdout: true,
});
async function getProtosFromRepo(
commitish = '',
version = '',
owner = 'arduino',
repo = 'arduino-cli'
) {
const repoFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'arduino-cli-'));
const rpc = path.join(repository, 'rpc');
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
mkdirSync(out, { recursive: true });
const url = `https://github.com/${owner}/${repo}.git`;
console.log(`>>> Cloning repository from '${url}'...`);
exec('git', ['clone', url, repoFolder], { logStdout: true });
console.log(`<<< Repository cloned.`);
const protos = await new Promise((resolve) =>
glob('**/*.proto', { cwd: rpc }, (error, matches) => {
if (error) {
console.log(error.stack ?? error.message);
resolve([]);
return;
if (validSemVer(version)) {
let versionTag = version;
// https://github.com/arduino/arduino-cli/pull/2374
if (
gte(new SemVer(version, { loose: true }), new SemVer('0.35.0-rc.1'))
) {
versionTag = `v${version}`;
}
resolve(matches.map((filename) => path.join(rpc, filename)));
})
);
if (!protos || protos.length === 0) {
console.log(`Could not find any .proto files under ${rpc}.`);
console.log(`>>> Checking out tagged version: '${versionTag}'...`);
exec('git', ['-C', repoFolder, 'fetch', '--all', '--tags'], {
logStdout: true,
});
exec(
'git',
['-C', repoFolder, 'checkout', `tags/${versionTag}`, '-b', versionTag],
{ logStdout: true }
);
console.log(`<<< Checked out tagged version: '${versionTag}'.`);
} else if (commitish) {
console.log(`>>> Checking out commitish: '${commitish}'...`);
exec('git', ['-C', repoFolder, 'checkout', commitish], {
logStdout: true,
});
console.log(`<<< Checked out commitish: '${commitish}'.`);
} else {
console.log(
`WARN: no 'git checkout'. Generating from the HEAD revision.`
);
}
const rpcFolder = await fs.mkdtemp(
path.join(os.tmpdir(), 'arduino-cli-rpc')
);
// Copy the the repository rpc folder so we can remove the repository
await fs.cp(path.join(repoFolder, 'rpc'), path.join(rpcFolder), {
recursive: true,
});
rmSync(repoFolder, { recursive: true, maxRetries: 5, force: true });
// Patch for https://github.com/arduino/arduino-cli/issues/2755
// Google proto files are removed from source since v1.1.0
if (!existsSync(path.join(rpcFolder, 'google'))) {
// Include packaged google proto files from v1.1.1
// See https://github.com/arduino/arduino-cli/pull/2761
console.log(`>>> Missing google proto files. Including from v1.1.1...`);
const v111ProtoFolder = await getProtosFromZip('1.1.1');
// Create an return a folder name google in rpcFolder
const googleFolder = path.join(rpcFolder, 'google');
await fs.cp(path.join(v111ProtoFolder, 'google'), googleFolder, {
recursive: true,
});
console.log(`<<< Included google proto files from v1.1.1.`);
}
return rpcFolder;
}
async function getProtosFromZip(version) {
if (!version) {
console.log(`Could not download proto files: CLI version not provided.`);
process.exit(1);
}
console.log(`>>> Downloading proto files from zip for ${version}.`);
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_proto.zip`;
const protos = await fs.mkdtemp(
path.join(os.tmpdir(), 'arduino-cli-proto')
);
const { default: download } = await import('@xhmikosr/downloader');
/** @type {import('node:buffer').Buffer} */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data = await download(url);
await decompress(data, protos, {
plugins: [unzip()],
filter: (file) => file.path.endsWith('.proto'),
});
console.log(
`<<< Finished downloading and extracting proto files for ${version}.`
);
return protos;
}
let protosFolder;
if (commitish) {
protosFolder = await getProtosFromRepo(commitish, undefined, owner, repo);
} else if (
versionObject.VersionString &&
validSemVer(versionObject.VersionString)
) {
const version = versionObject.VersionString;
// v1.1.0 does not contains google proto files in zip
// See https://github.com/arduino/arduino-cli/issues/2755
const isV110 = eq(new SemVer(version, { loose: true }), '1.1.0');
protosFolder = isV110
? await getProtosFromRepo(undefined, version)
: await getProtosFromZip(version);
} else if (versionObject.Commit) {
protosFolder = await getProtosFromRepo(versionObject.Commit);
}
if (!protosFolder) {
console.log(`Could not get proto files: missing commitish or version.`);
process.exit(1);
}
// Generate JS code from the `.proto` files.
exec(
'grpc_tools_node_protoc',
[
`--js_out=import_style=commonjs,binary:${out}`,
`--grpc_out=generate_package_definition:${out}`,
'-I',
rpc,
...protos,
],
{ logStdout: true }
);
const protos = await globProtos(protosFolder);
// Generate the `.d.ts` files for JS.
exec(
path.join(protoc, `protoc${platform === 'win32' ? '.exe' : ''}`),
[
`--plugin=protoc-gen-ts=${path.resolve(
__dirname,
'..',
'node_modules',
'.bin',
`protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`
)}`,
`--ts_out=generate_package_definition:${out}`,
'-I',
rpc,
...protos,
],
{ logStdout: true }
);
if (!protos || protos.length === 0) {
rmSync(protosFolder, { recursive: true, maxRetries: 5, force: true });
console.log(`Could not find any .proto files under ${protosFolder}.`);
process.exit(1);
}
console.log('>>> Generating TS/JS API from:');
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
// Must wipe the gen output folder. Otherwise, dangling service implementation remain in IDE2 code,
// although it has been removed from the proto file.
// For example, https://github.com/arduino/arduino-cli/commit/50a8bf5c3e61d5b661ccfcd6a055e82eeb510859.
// rmSync(out, { recursive: true, maxRetries: 5, force: true });
mkdirSync(out, { recursive: true });
try {
// Generate JS code from the `.proto` files.
exec(
'grpc_tools_node_protoc',
[
`--js_out=import_style=commonjs,binary:${out}`,
`--grpc_out=generate_package_definition:${out}`,
'-I',
protosFolder,
...protos,
],
{ logStdout: true }
);
// Generate the `.d.ts` files for JS.
exec(
path.join(protoc, `protoc${platform === 'win32' ? '.exe' : ''}`),
[
`--plugin=protoc-gen-ts=${path.resolve(
__dirname,
'..',
'node_modules',
'.bin',
`protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`
)}`,
`--ts_out=generate_package_definition:${out}`,
'-I',
protosFolder,
...protos,
],
{ logStdout: true }
);
} catch (error) {
console.log(error);
} finally {
rmSync(protosFolder, { recursive: true, maxRetries: 5, force: true });
}
console.log('<<< Generation was successful.');
})();

View File

@ -1,7 +1,7 @@
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
TabBarToolbarContribution,

View File

@ -5,10 +5,8 @@ import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import {
FrontendApplicationContribution,
FrontendApplication as TheiaFrontendApplication,
} from '@theia/core/lib/browser/frontend-application';
import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
import {
@ -91,7 +89,6 @@ import {
ArduinoDaemonPath,
ArduinoDaemon,
} from '../common/protocol/arduino-daemon';
import { EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser';
import {
FrontendConnectionStatusService,
ApplicationConnectionStatusContribution,
@ -125,7 +122,10 @@ import { OpenSketch } from './contributions/open-sketch';
import { Close } from './contributions/close';
import { SaveAsSketch } from './contributions/save-as-sketch';
import { SaveSketch } from './contributions/save-sketch';
import { VerifySketch } from './contributions/verify-sketch';
import {
CompileSummaryProvider,
VerifySketch,
} from './contributions/verify-sketch';
import { UploadSketch } from './contributions/upload-sketch';
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
import { EditContributions } from './contributions/edit-contributions';
@ -177,7 +177,6 @@ import {
import { About } from './contributions/about';
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
import { TabBarRenderer } from './theia/core/tab-bars';
import { EditorCommandContribution } from './theia/editor/editor-command';
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
import { Debug, DebugDisabledStatusMessageSource } from './contributions/debug';
@ -266,7 +265,7 @@ import {
IDEUpdaterDialog,
IDEUpdaterDialogProps,
} from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-source';
import { MonitorModel } from './monitor-model';
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
@ -286,10 +285,6 @@ import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-gen
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
import { AboutDialog } from './theia/core/about-dialog';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { WindowContribution } from './theia/core/window-contribution';
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
import { CoreErrorHandler } from './contributions/core-error-handler';
@ -373,6 +368,13 @@ import { DebugConfigurationWidget } from './theia/debug/debug-configuration-widg
import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
import {
VersionWelcomeDialog,
VersionWelcomeDialogProps,
} from './dialogs/version-welcome-dialog';
import { TestViewContribution as TheiaTestViewContribution } from '@theia/test/lib/browser/view/test-view-contribution';
import { TestViewContribution } from './theia/test/test-view-contribution';
// Hack to fix copy/cut/paste issue after electron version update in Theia.
// https://github.com/eclipse-theia/theia/issues/12487
import('@theia/core/lib/browser/common-frontend-contribution.js').then(
@ -552,15 +554,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
WorkspaceVariableContribution
);
bind(SurveyNotificationService)
.toDynamicValue((context) => {
return ElectronIpcConnectionProvider.createProxy(
context.container,
SurveyNotificationServicePath
);
})
.inSingletonScope();
// Layout and shell customizations.
rebind(TheiaOutlineViewContribution)
.to(OutlineViewContribution)
@ -769,6 +762,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, BoardsDataMenuUpdater);
Contribution.configure(bind, AutoSelectProgrammer);
bind(CompileSummaryProvider).toService(VerifySketch);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@ -832,13 +827,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
});
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
// Note: `"editor.autoSave" was renamed to `"files.autoSave" and `"on"` was replaced with three
// different cases, but we treat `!== 'off'` as auto save enabled. (https://github.com/eclipse-theia/theia/issues/10812)
bind(EditorCommandContribution).toSelf().inSingletonScope();
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
// Silent the badge decoration in the Explorer view.
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
@ -999,6 +987,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
title: 'IDEUpdater',
});
bind(VersionWelcomeDialog).toSelf().inSingletonScope();
bind(VersionWelcomeDialogProps).toConstantValue({
title: 'VersionWelcomeDialog',
});
bind(UserFieldsDialog).toSelf().inSingletonScope();
bind(UserFieldsDialogProps).toConstantValue({
title: 'UserFields',
@ -1082,4 +1075,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaTerminalFrontendContribution).toService(
TerminalFrontendContribution
);
// Hides the Test Explorer from the side-bar
bind(TestViewContribution).toSelf().inSingletonScope();
rebind(TheiaTestViewContribution).toService(TestViewContribution);
});

View File

@ -137,6 +137,18 @@ const properties: ArduinoPreferenceSchemaProperties = {
'arduino.upload.verify': {
type: 'boolean',
default: false,
description: nls.localize(
'arduino/preferences/upload.verify',
'After upload, verify that the contents of the memory on the board match the uploaded binary.'
),
},
'arduino.upload.autoVerify': {
type: 'boolean',
default: true,
description: nls.localize(
'arduino/preferences/upload.autoVerify',
"True if the IDE should automatically verify the code before the upload. True by default. When this value is false, IDE does not recompile the code before uploading the binary to the board. It's highly advised to only set this value to false if you know what you are doing."
),
},
'arduino.window.autoScale': {
type: 'boolean',
@ -228,6 +240,14 @@ const properties: ArduinoPreferenceSchemaProperties = {
),
default: 'https://api2.arduino.cc/create',
},
'arduino.cloud.sharedSpaceID': {
type: 'string',
description: nls.localize(
'arduino/preferences/cloud.sharedSpaceId',
'The ID of the Arduino Cloud shared space to load the sketchbook from. If empty, your private space is selected.'
),
default: '',
},
'arduino.auth.clientID': {
type: 'string',
description: nls.localize(
@ -260,14 +280,6 @@ const properties: ArduinoPreferenceSchemaProperties = {
),
default: 'https://auth.arduino.cc/login#/register',
},
'arduino.survey.notification': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/survey.notification',
'True if users should be notified if a survey is available. True by default.'
),
default: true,
},
'arduino.cli.daemon.debug': {
type: 'boolean',
description: nls.localize(
@ -319,6 +331,7 @@ export interface ArduinoConfiguration {
'arduino.compile.warnings': CompilerWarnings;
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
'arduino.upload.autoVerify': boolean;
'arduino.window.autoScale': boolean;
'arduino.ide.updateChannel': UpdateChannel;
'arduino.ide.updateBaseUrl': string;
@ -329,11 +342,11 @@ export interface ArduinoConfiguration {
'arduino.cloud.push.warn': boolean;
'arduino.cloud.pushpublic.warn': boolean;
'arduino.cloud.sketchSyncEndpoint': string;
'arduino.cloud.sharedSpaceID': string;
'arduino.auth.clientID': string;
'arduino.auth.domain': string;
'arduino.auth.audience': string;
'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean;
'arduino.sketch.inoBlueprint': string;
'arduino.checkForUpdates': boolean;

View File

@ -3,7 +3,7 @@ import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import {
CommandRegistry,
CommandContribution,

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MessageService } from '@theia/core/lib/common/message-service';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';

View File

@ -98,6 +98,7 @@ export class BoardsConfigDialog extends ReactDialog<BoardsConfigDialogState> {
}
override async open(
disposeOnResolve = true,
params?: EditBoardsConfigActionParams
): Promise<BoardsConfig | undefined> {
this._searchSet = undefined;
@ -119,7 +120,7 @@ export class BoardsConfigDialog extends ReactDialog<BoardsConfigDialogState> {
this._searchSet = params.searchSet.slice();
}
}
return super.open();
return super.open(disposeOnResolve);
}
protected override onAfterAttach(msg: Message): void {

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { StorageService } from '@theia/core/lib/browser/storage-service';
import type {
@ -12,6 +12,7 @@ import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone, deepFreeze } from '@theia/core/lib/common/objects';
import type { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { FQBN } from 'fqbn';
import {
BoardDetails,
BoardsService,
@ -20,6 +21,7 @@ import {
Programmer,
isBoardIdentifierChangeEvent,
isProgrammer,
sanitizeFqbn,
} from '../../common/protocol';
import { notEmpty } from '../../common/utils';
import type {
@ -29,6 +31,14 @@ import type {
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider';
export interface SelectConfigOptionParams {
readonly fqbn: string;
readonly optionsToUpdate: readonly Readonly<{
option: string;
selectedValue: string;
}>[];
}
@injectable()
export class BoardsDataStore
implements
@ -64,7 +74,12 @@ export class BoardsDataStore
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.updateSelectedBoardData(event.selectedBoard?.fqbn);
this.updateSelectedBoardData(
event.selectedBoard?.fqbn,
// If the change event comes from toolbar and the FQBN contains custom board options, change the currently selected options
// https://github.com/arduino/arduino-ide/issues/1588
event.reason === 'toolbar'
);
}
}),
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
@ -116,7 +131,7 @@ export class BoardsDataStore
if (!fqbn) {
return undefined;
} else {
const data = await this.getData(fqbn);
const data = await this.getData(sanitizeFqbn(fqbn));
if (data === BoardsDataStore.Data.EMPTY) {
return undefined;
}
@ -125,9 +140,22 @@ export class BoardsDataStore
}
private async updateSelectedBoardData(
fqbn: string | undefined
fqbn: string | undefined,
updateConfigOptions = false
): Promise<void> {
this._selectedBoardData = await this.getSelectedBoardData(fqbn);
if (fqbn && updateConfigOptions) {
const { options } = new FQBN(fqbn);
if (options) {
const optionsToUpdate = Object.entries(options).map(([key, value]) => ({
option: key,
selectedValue: value,
}));
const params = { fqbn, optionsToUpdate };
await this.selectConfigOption(params);
this._selectedBoardData = await this.getSelectedBoardData(fqbn); // reload the updated data
}
}
}
onStop(): void {
@ -168,7 +196,7 @@ export class BoardsDataStore
return undefined;
}
const { configOptions } = await this.getData(fqbn);
return ConfigOption.decorate(fqbn, configOptions);
return new FQBN(fqbn).withConfigOptions(...configOptions).toString();
}
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
@ -194,6 +222,20 @@ export class BoardsDataStore
return data;
}
async reloadBoardData(fqbn: string | undefined): Promise<void> {
if (!fqbn) {
return;
}
const key = this.getStorageKey(fqbn);
const details = await this.loadBoardDetails(fqbn, true);
if (!details) {
return;
}
const data = createDataStoreEntry(details);
await this.storageService.setData(key, data);
this.fireChanged({ fqbn, data });
}
async selectProgrammer({
fqbn,
selectedProgrammer,
@ -201,48 +243,63 @@ export class BoardsDataStore
fqbn: string;
selectedProgrammer: Programmer;
}): Promise<boolean> {
const storedData = deepClone(await this.getData(fqbn));
const sanitizedFQBN = sanitizeFqbn(fqbn);
const storedData = deepClone(await this.getData(sanitizedFQBN));
const { programmers } = storedData;
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
return false;
}
const data = { ...storedData, selectedProgrammer };
await this.setData({ fqbn, data });
this.fireChanged({ fqbn, data });
const change: BoardsDataStoreChange = {
fqbn: sanitizedFQBN,
data: { ...storedData, selectedProgrammer },
};
await this.setData(change);
this.fireChanged(change);
return true;
}
async selectConfigOption({
fqbn,
option,
selectedValue,
}: {
fqbn: string;
option: string;
selectedValue: string;
}): Promise<boolean> {
const data = deepClone(await this.getData(fqbn));
const { configOptions } = data;
const configOption = configOptions.find((c) => c.option === option);
if (!configOption) {
async selectConfigOption(params: SelectConfigOptionParams): Promise<boolean> {
const { fqbn, optionsToUpdate } = params;
if (!optionsToUpdate.length) {
return false;
}
let updated = false;
for (const value of configOption.values) {
const mutable: Mutable<ConfigValue> = value;
if (mutable.value === selectedValue) {
mutable.selected = true;
updated = true;
} else {
mutable.selected = false;
const sanitizedFQBN = sanitizeFqbn(fqbn);
const mutableData = deepClone(await this.getData(sanitizedFQBN));
let didChange = false;
for (const { option, selectedValue } of optionsToUpdate) {
const { configOptions } = mutableData;
const configOption = configOptions.find((c) => c.option === option);
if (configOption) {
const configOptionValueIndex = configOption.values.findIndex(
(configOptionValue) => configOptionValue.value === selectedValue
);
if (configOptionValueIndex >= 0) {
// unselect all
configOption.values
.map((value) => value as Mutable<ConfigValue>)
.forEach((value) => (value.selected = false));
const mutableConfigValue: Mutable<ConfigValue> =
configOption.values[configOptionValueIndex];
// make the new value `selected`
mutableConfigValue.selected = true;
didChange = true;
}
}
}
if (!updated) {
if (!didChange) {
return false;
}
await this.setData({ fqbn, data });
this.fireChanged({ fqbn, data });
const change: BoardsDataStoreChange = {
fqbn: sanitizedFQBN,
data: mutableData,
};
await this.setData(change);
this.fireChanged(change);
return true;
}
@ -256,9 +313,15 @@ export class BoardsDataStore
return `.arduinoIDE-configOptions-${fqbn}`;
}
async loadBoardDetails(fqbn: string): Promise<BoardDetails | undefined> {
async loadBoardDetails(
fqbn: string,
forceRefresh = false
): Promise<BoardDetails | undefined> {
try {
const details = await this.boardsService.getBoardDetails({ fqbn });
const details = await this.boardsService.getBoardDetails({
fqbn,
forceRefresh,
});
return details;
} catch (err) {
if (

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { StorageService } from '@theia/core/lib/browser/storage-service';
import {
@ -12,6 +12,7 @@ import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls';
import { deepClone } from '@theia/core/lib/common/objects';
import { Deferred } from '@theia/core/lib/common/promise-util';
import type { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable, optional } from '@theia/core/shared/inversify';
@ -21,31 +22,32 @@ import {
} from '@theia/output/lib/browser/output-channel';
import {
BoardIdentifier,
boardIdentifierEquals,
BoardUserField,
BoardWithPackage,
BoardsConfig,
BoardsConfigChangeEvent,
BoardsPackage,
BoardsService,
BoardUserField,
BoardWithPackage,
DetectedPorts,
Port,
PortIdentifier,
boardIdentifierEquals,
emptyBoardsConfig,
isBoardIdentifier,
isBoardIdentifierChangeEvent,
isPortIdentifier,
isPortIdentifierChangeEvent,
Port,
PortIdentifier,
portIdentifierEquals,
sanitizeFqbn,
serializePlatformIdentifier,
} from '../../common/protocol';
import {
BoardList,
BoardListHistory,
createBoardList,
EditBoardsConfigActionParams,
isBoardListHistory,
SelectBoardsConfigActionParams,
createBoardList,
isBoardListHistory,
} from '../../common/protocol/board-list';
import type { Defined } from '../../common/types';
import type {
@ -104,6 +106,21 @@ type BoardListHistoryUpdateResult =
type BoardToSelect = BoardIdentifier | undefined | 'ignore-board';
type PortToSelect = PortIdentifier | undefined | 'ignore-port';
function sanitizeBoardToSelectFQBN(board: BoardToSelect): BoardToSelect {
if (isBoardIdentifier(board)) {
return sanitizeBoardIdentifierFQBN(board);
}
return board;
}
function sanitizeBoardIdentifierFQBN(board: BoardIdentifier): BoardIdentifier {
if (board.fqbn) {
const copy: Mutable<BoardIdentifier> = deepClone(board);
copy.fqbn = sanitizeFqbn(board.fqbn);
return copy;
}
return board;
}
interface UpdateBoardListHistoryParams {
readonly portToSelect: PortToSelect;
readonly boardToSelect: BoardToSelect;
@ -136,6 +153,9 @@ export interface BoardListUIActions {
}
export type BoardListUI = BoardList & BoardListUIActions;
export type BoardsConfigChangeEventUI = BoardsConfigChangeEvent &
Readonly<{ reason?: UpdateBoardsConfigReason }>;
@injectable()
export class BoardListDumper implements Disposable {
@inject(OutputChannelManager)
@ -190,7 +210,7 @@ export class BoardsServiceProvider
private _ready = new Deferred<void>();
private readonly boardsConfigDidChangeEmitter =
new Emitter<BoardsConfigChangeEvent>();
new Emitter<BoardsConfigChangeEventUI>();
readonly onBoardsConfigDidChange = this.boardsConfigDidChangeEmitter.event;
private readonly boardListDidChangeEmitter = new Emitter<BoardListUI>();
@ -353,7 +373,8 @@ export class BoardsServiceProvider
portToSelect !== 'ignore-port' &&
!portIdentifierEquals(portToSelect, previousSelectedPort);
const boardDidChangeEvent = boardDidChange
? { selectedBoard: boardToSelect, previousSelectedBoard }
? // The change event must always contain any custom board options. Hence the board to select is not sanitized.
{ selectedBoard: boardToSelect, previousSelectedBoard }
: undefined;
const portDidChangeEvent = portDidChange
? { selectedPort: portToSelect, previousSelectedPort }
@ -374,16 +395,31 @@ export class BoardsServiceProvider
return false;
}
this.maybeUpdateBoardListHistory({ portToSelect, boardToSelect });
this.maybeUpdateBoardsData({ boardToSelect, reason });
// unlike for the board change event, every persistent state must not contain custom board config options in the FQBN
const sanitizedBoardToSelect = sanitizeBoardToSelectFQBN(boardToSelect);
this.maybeUpdateBoardListHistory({
portToSelect,
boardToSelect: sanitizedBoardToSelect,
});
this.maybeUpdateBoardsData({
boardToSelect: sanitizedBoardToSelect,
reason,
});
if (isBoardIdentifierChangeEvent(event)) {
this._boardsConfig.selectedBoard = event.selectedBoard;
this._boardsConfig.selectedBoard = event.selectedBoard
? sanitizeBoardIdentifierFQBN(event.selectedBoard)
: event.selectedBoard;
}
if (isPortIdentifierChangeEvent(event)) {
this._boardsConfig.selectedPort = event.selectedPort;
}
if (reason) {
event = Object.assign(event, { reason });
}
this.boardsConfigDidChangeEmitter.fire(event);
this.refreshBoardList();
this.saveState();

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';

View File

@ -20,6 +20,7 @@ import {
} from '../../common/protocol';
import type { BoardList } from '../../common/protocol/board-list';
import { BoardsListWidget } from '../boards/boards-list-widget';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
ArduinoMenus,
@ -39,6 +40,8 @@ export class BoardSelection extends SketchContribution {
private readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
@ -74,6 +77,29 @@ SN: ${SN}
});
},
});
registry.registerCommand(BoardSelection.Commands.RELOAD_BOARD_DATA, {
execute: async () => {
const selectedFqbn =
this.boardsServiceProvider.boardList.boardsConfig.selectedBoard?.fqbn;
let message: string;
if (selectedFqbn) {
await this.boardsDataStore.reloadBoardData(selectedFqbn);
message = nls.localize(
'arduino/board/boardDataReloaded',
'Board data reloaded.'
);
} else {
message = nls.localize(
'arduino/board/selectBoardToReload',
'Please select a board first.'
);
}
this.messageService.info(message, { timeout: 2000 });
},
});
}
override onStart(): void {
@ -151,6 +177,21 @@ SN: ${SN}
)
);
const reloadBoardData = {
commandId: BoardSelection.Commands.RELOAD_BOARD_DATA.id,
label: nls.localize('arduino/board/reloadBoardData', 'Reload Board Data'),
order: '102',
};
this.menuModelRegistry.registerMenuAction(
ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
reloadBoardData
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuAction(reloadBoardData)
)
);
const getBoardInfo = {
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
label: nls.localize('arduino/board/getBoardInfo', 'Get Board Info'),
@ -361,5 +402,8 @@ SN: ${SN}
export namespace BoardSelection {
export namespace Commands {
export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' };
export const RELOAD_BOARD_DATA: Command = {
id: 'arduino-reload-board-data',
};
}
}

View File

@ -87,8 +87,7 @@ export class BoardsDataMenuUpdater extends Contribution {
execute: () =>
this.boardsDataStore.selectConfigOption({
fqbn,
option,
selectedValue: value.value,
optionsToUpdate: [{ option, selectedValue: value.value }],
}),
isToggled: () => value.selected,
};

View File

@ -37,11 +37,15 @@ export class BurnBootloader extends CoreServiceContribution {
'arduino/bootloader/burningBootloader',
'Burning bootloader...'
),
task: (progressId, coreService) =>
coreService.burnBootloader({
...options,
progressId,
}),
task: (progressId, coreService, token) =>
coreService.burnBootloader(
{
...options,
progressId,
},
token
),
cancelable: true,
});
this.messageService.info(
nls.localize(

View File

@ -3,10 +3,14 @@ import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
IDEUpdater,
LAST_USED_IDE_VERSION,
SKIP_IDE_VERSION,
} from '../../common/protocol/ide-updater';
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
import { Contribution } from './contribution';
import { VersionWelcomeDialog } from '../dialogs/version-welcome-dialog';
import { AppService } from '../app-service';
import { SemVer } from 'semver';
@injectable()
export class CheckForIDEUpdates extends Contribution {
@ -16,9 +20,15 @@ export class CheckForIDEUpdates extends Contribution {
@inject(IDEUpdaterDialog)
private readonly updaterDialog: IDEUpdaterDialog;
@inject(VersionWelcomeDialog)
private readonly versionWelcomeDialog: VersionWelcomeDialog;
@inject(LocalStorageService)
private readonly localStorage: LocalStorageService;
@inject(AppService)
private readonly appService: AppService;
override onStart(): void {
this.preferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => {
@ -36,7 +46,7 @@ export class CheckForIDEUpdates extends Contribution {
);
}
override onReady(): void {
override async onReady(): Promise<void> {
this.updater
.init(
this.preferences.get('arduino.ide.updateChannel'),
@ -49,12 +59,18 @@ export class CheckForIDEUpdates extends Contribution {
return this.updater.checkForUpdates(true);
})
.then(async (updateInfo) => {
if (!updateInfo) return;
if (!updateInfo) {
const isNewVersion = await this.isNewStableVersion();
if (isNewVersion) {
this.versionWelcomeDialog.open();
}
return;
}
const versionToSkip = await this.localStorage.getData<string>(
SKIP_IDE_VERSION
);
if (versionToSkip === updateInfo.version) return;
this.updaterDialog.open(updateInfo);
this.updaterDialog.open(true, updateInfo);
})
.catch((e) => {
this.messageService.error(
@ -64,6 +80,44 @@ export class CheckForIDEUpdates extends Contribution {
e.message
)
);
})
.finally(() => {
this.setCurrentIDEVersion();
});
}
private async setCurrentIDEVersion(): Promise<void> {
try {
const { appVersion } = await this.appService.info();
const currSemVer = new SemVer(appVersion ?? '');
this.localStorage.setData(LAST_USED_IDE_VERSION, currSemVer.format());
} catch {
// ignore invalid versions
}
}
/**
* Check if user is running a new IDE version for the first time.
* @returns true if the current IDE version is greater than the last used version
* and both are non-prerelease versions.
*/
private async isNewStableVersion(): Promise<boolean> {
try {
const { appVersion } = await this.appService.info();
const prevVersion = await this.localStorage.getData<string>(
LAST_USED_IDE_VERSION
);
const prevSemVer = new SemVer(prevVersion ?? '');
const currSemVer = new SemVer(appVersion ?? '');
if (prevSemVer.prerelease.length || currSemVer.prerelease.length) {
return false;
}
return currSemVer.compare(prevSemVer) === 1;
} catch (e) {
return false;
}
}
}

View File

@ -1,8 +1,6 @@
import { Dialog } from '@theia/core/lib/browser/dialogs';
import type {
FrontendApplication,
OnWillStopAction,
} from '@theia/core/lib/browser/frontend-application';
import type { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application-contribution';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { nls } from '@theia/core/lib/common/nls';
import type { MaybePromise } from '@theia/core/lib/common/types';

View File

@ -779,7 +779,7 @@ export class CompilerErrors
return undefined;
} else {
return this.editorManager
.getByUri(new URI(uriOrWidget))
.getByUri(new URI(uriOrWidget.toString()))
.then((editor) => {
if (editor) {
return this.monacoEditor(editor);

View File

@ -1,83 +1,87 @@
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
KeybindingContribution,
KeybindingRegistry,
} from '@theia/core/lib/browser/keybinding';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { OpenerService, open } from '@theia/core/lib/browser/opener-service';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import {
Command,
CommandContribution,
CommandRegistry,
CommandService,
} from '@theia/core/lib/common/command';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ILogger } from '@theia/core/lib/common/logger';
import {
MenuContribution,
MenuModelRegistry,
} from '@theia/core/lib/common/menu';
import { MessageService } from '@theia/core/lib/common/message-service';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { MaybePromise, isObject } from '@theia/core/lib/common/types';
import URI from '@theia/core/lib/common/uri';
import {
inject,
injectable,
interfaces,
postConstruct,
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { OutputChannelSeverity } from '@theia/output/lib/browser/output-channel';
import { MainMenuManager } from '../../common/main-menu-manager';
import { userAbort } from '../../common/nls';
import {
MenuModelRegistry,
MenuContribution,
} from '@theia/core/lib/common/menu';
CoreError,
CoreService,
FileSystemExt,
ResponseServiceClient,
Sketch,
SketchesService,
} from '../../common/protocol';
import {
KeybindingRegistry,
KeybindingContribution,
} from '@theia/core/lib/browser/keybinding';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import {
FrontendApplicationContribution,
FrontendApplication,
} from '@theia/core/lib/browser/frontend-application';
import {
Command,
CommandRegistry,
CommandContribution,
CommandService,
} from '@theia/core/lib/common/command';
ExecuteWithProgress,
UserAbortApplicationError,
} from '../../common/protocol/progressible';
import { ArduinoPreferences } from '../arduino-preferences';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ConfigServiceClient } from '../config/config-service-client';
import { DialogService } from '../dialog-service';
import { SettingsService } from '../dialogs/settings/settings';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../sketches-service-client-impl';
import {
SketchesService,
FileSystemExt,
Sketch,
CoreService,
CoreError,
ResponseServiceClient,
} from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { nls } from '@theia/core';
import { OutputChannelManager } from '../theia/output/output-channel';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ConfigServiceClient } from '../config/config-service-client';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { DialogService } from '../dialog-service';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import { OutputChannelManager } from '../theia/output/output-channel';
import { WorkspaceService } from '../theia/workspace/workspace-service';
export {
Command,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
MenuModelRegistry,
Sketch,
TabBarToolbarRegistry,
URI,
Sketch,
open,
};
@ -247,6 +251,12 @@ export abstract class CoreServiceContribution extends SketchContribution {
}
protected handleError(error: unknown): void {
if (isObject(error) && UserAbortApplicationError.is(error)) {
this.outputChannelManager
.getChannel('Arduino')
.appendLine(userAbort, OutputChannelSeverity.Warning);
return;
}
this.tryToastErrorMessage(error);
}
@ -293,7 +303,13 @@ export abstract class CoreServiceContribution extends SketchContribution {
protected async doWithProgress<T>(options: {
progressText: string;
keepOutput?: boolean;
task: (progressId: string, coreService: CoreService) => Promise<T>;
task: (
progressId: string,
coreService: CoreService,
cancellationToken?: CancellationToken
) => Promise<T>;
// false by default
cancelable?: boolean;
}): Promise<T> {
const toDisposeOnComplete = new DisposableCollection(
this.maybeActivateMonitorWidget()
@ -306,8 +322,10 @@ export abstract class CoreServiceContribution extends SketchContribution {
messageService: this.messageService,
responseService: this.responseService,
progressText,
run: ({ progressId }) => task(progressId, this.coreService),
run: ({ progressId, cancellationToken }) =>
task(progressId, this.coreService, cancellationToken),
keepOutput,
cancelable: options.cancelable,
});
toDisposeOnComplete.dispose();
return result;

View File

@ -289,8 +289,8 @@ export class Debug
): Promise<boolean> {
if (err instanceof Error) {
try {
const tempBuildPaths = await this.sketchesService.tempBuildPath(sketch);
return tempBuildPaths.some((tempBuildPath) =>
const buildPaths = await this.sketchesService.getBuildPath(sketch);
return buildPaths.some((tempBuildPath) =>
err.message.includes(tempBuildPath)
);
} catch {
@ -398,12 +398,9 @@ export async function isDebugEnabled(
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
);
}
if (!data.selectedProgrammer) {
throw new Error(noProgrammerSelectedFor(board.name));
}
const params = {
fqbn: fqbnWithConfig,
programmer: data.selectedProgrammer.id,
programmer: data.selectedProgrammer?.id,
};
try {
const debugFqbn = await checkDebugEnabled(params);
@ -443,13 +440,3 @@ export function debuggingNotSupported(boardName: string): string {
boardName
);
}
/**
* (non-API)
*/
export function noProgrammerSelectedFor(boardName: string): string {
return nls.localize(
'arduino/debug/noProgrammerSelectedFor',
"No programmer selected for '{0}'",
boardName
);
}

View File

@ -1,7 +1,11 @@
import { nls } from '@theia/core/lib/common';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
import {
Contribution,
Command,
@ -10,17 +14,11 @@ import {
CommandRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
// Depends on https://github.com/eclipse-theia/theia/pull/7964
@injectable()
export class EditContributions extends Contribution {
@inject(MonacoEditorService)
private readonly codeEditorService: MonacoEditorService;
@inject(ClipboardService)
private readonly clipboardService: ClipboardService;
@ -208,9 +206,10 @@ ${value}
protected async current(): Promise<
ICodeEditor | StandaloneCodeEditor | undefined
> {
const codeEditorService = StandaloneServices.get(ICodeEditorService);
return (
this.codeEditorService.getFocusedCodeEditor() ||
this.codeEditorService.getActiveCodeEditor() ||
codeEditorService.getFocusedCodeEditor() ||
codeEditorService.getActiveCodeEditor() ||
undefined
);
}

View File

@ -8,8 +8,8 @@ import {
ArduinoDaemon,
BoardIdentifier,
BoardsService,
CompileSummary,
ExecutableService,
assertSanitizedFqbn,
isBoardIdentifierChangeEvent,
sanitizeFqbn,
} from '../../common/protocol';
@ -24,6 +24,7 @@ import { HostedPluginEvents } from '../hosted/hosted-plugin-events';
import { NotificationCenter } from '../notification-center';
import { CurrentSketch } from '../sketches-service-client-impl';
import { SketchContribution, URI } from './contribution';
import { CompileSummaryProvider } from './verify-sketch';
interface DaemonAddress {
/**
@ -108,6 +109,8 @@ export class InoLanguage extends SketchContribution {
private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardDataStore: BoardsDataStore;
@inject(CompileSummaryProvider)
private readonly compileSummaryProvider: CompileSummaryProvider;
private readonly toDispose = new DisposableCollection();
private readonly languageServerStartMutex = new Mutex();
@ -159,14 +162,11 @@ export class InoLanguage extends SketchContribution {
this.notificationCenter.onDidReinitialize(() => forceRestart()),
this.boardDataStore.onDidChange((event) => {
if (this.languageServerFqbn) {
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
if (!sanitizeFqbn) {
throw new Error(
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
);
}
const sanitizedFQBN = sanitizeFqbn(this.languageServerFqbn);
// The incoming FQBNs might contain custom boards configs, sanitize them before the comparison.
// https://github.com/arduino/arduino-ide/pull/2113#pullrequestreview-1499998328
const matchingChange = event.changes.find(
(change) => change.fqbn === sanitizedFqbn
(change) => sanitizedFQBN === sanitizeFqbn(change.fqbn)
);
const { boardsConfig } = this.boardsServiceProvider;
if (
@ -177,6 +177,13 @@ export class InoLanguage extends SketchContribution {
}
}
}),
this.compileSummaryProvider.onDidChangeCompileSummary(
(compileSummary) => {
if (compileSummary) {
this.fireBuildDidComplete(compileSummary);
}
}
),
]);
Promise.all([
this.boardsServiceProvider.ready,
@ -196,11 +203,7 @@ export class InoLanguage extends SketchContribution {
forceStart = false
): Promise<void> {
const port = await this.daemon.tryGetPort();
if (!port) {
return;
}
const portNumber = Number.parseInt(port, 10); // TODO: IDE2 APIs should provide a number and not string
if (Number.isNaN(portNumber)) {
if (typeof port !== 'number') {
return;
}
const release = await this.languageServerStartMutex.acquire();
@ -232,7 +235,6 @@ export class InoLanguage extends SketchContribution {
}
return;
}
assertSanitizedFqbn(fqbn);
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
if (!fqbnWithConfig) {
throw new Error(
@ -280,7 +282,7 @@ export class InoLanguage extends SketchContribution {
lsPath,
daemonAddress: {
hostname: 'localhost',
port: portNumber,
port,
instance: 1, // TODO: get it from the backend
},
clangdPath,
@ -326,4 +328,32 @@ export class InoLanguage extends SketchContribution {
params
);
}
// Execute the a command contributed by the Arduino Tools VSIX to send the `ino/buildDidComplete` notification to the language server
private async fireBuildDidComplete(
compileSummary: CompileSummary
): Promise<void> {
const params = {
...compileSummary,
};
console.info(
`Executing 'arduino.languageserver.notifyBuildDidComplete' with ${JSON.stringify(
params.buildOutputUri
)}`
);
try {
await this.commandService.executeCommand(
'arduino.languageserver.notifyBuildDidComplete',
params
);
} catch (err) {
console.error(
`Unexpected error when firing event on build did complete. ${JSON.stringify(
params.buildOutputUri
)}`,
err
);
}
}
}

View File

@ -12,7 +12,7 @@ export class OpenBoardsConfig extends Contribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, {
execute: async (params?: EditBoardsConfigActionParams) =>
this.boardsConfigDialog.open(params),
this.boardsConfigDialog.open(true, params),
});
}
}

View File

@ -1,78 +0,0 @@
import { MessageService } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { LocalStorageService } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ArduinoPreferences } from '../arduino-preferences';
import { SurveyNotificationService } from '../../common/protocol/survey-service';
const SURVEY_MESSAGE = nls.localize(
'arduino/survey/surveyMessage',
'Please help us improve by answering this super short survey. We value our community and would like to get to know our supporters a little better.'
);
const DO_NOT_SHOW_AGAIN = nls.localize(
'arduino/survey/dismissSurvey',
"Don't show again"
);
const GO_TO_SURVEY = nls.localize(
'arduino/survey/answerSurvey',
'Answer survey'
);
const SURVEY_BASE_URL = 'https://surveys.hotjar.com/';
const surveyId = '17887b40-e1f0-4bd6-b9f0-a37f229ccd8b';
@injectable()
export class SurveyNotification implements FrontendApplicationContribution {
@inject(MessageService)
private readonly messageService: MessageService;
@inject(LocalStorageService)
private readonly localStorageService: LocalStorageService;
@inject(WindowService)
private readonly windowService: WindowService;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SurveyNotificationService)
private readonly surveyNotificationService: SurveyNotificationService;
onStart(): void {
this.arduinoPreferences.ready.then(async () => {
if (
(await this.surveyNotificationService.isFirstInstance()) &&
this.arduinoPreferences.get('arduino.survey.notification')
) {
const surveyAnswered = await this.localStorageService.getData(
this.surveyKey(surveyId)
);
if (surveyAnswered !== undefined) {
return;
}
const answer = await this.messageService.info(
SURVEY_MESSAGE,
DO_NOT_SHOW_AGAIN,
GO_TO_SURVEY
);
switch (answer) {
case GO_TO_SURVEY:
this.windowService.openNewWindow(SURVEY_BASE_URL + surveyId, {
external: true,
});
this.localStorageService.setData(this.surveyKey(surveyId), true);
break;
case DO_NOT_SHOW_AGAIN:
this.localStorageService.setData(this.surveyKey(surveyId), false);
break;
}
}
});
}
private surveyKey(id: string): string {
return `answered_survey:${id}`;
}
}

View File

@ -1,13 +1,11 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { HostedPluginSupport } from '../hosted/hosted-plugin-support';
import type { ArduinoState } from 'vscode-arduino-api';
import {
BoardsConfig,
BoardsService,
CompileSummary,
isCompileSummary,
BoardsConfig,
PortIdentifier,
resolveDetectedPort,
} from '../../common/protocol';
@ -18,8 +16,10 @@ import {
} from '../../common/protocol/arduino-context-mapper';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { HostedPluginSupport } from '../hosted/hosted-plugin-support';
import { CurrentSketch } from '../sketches-service-client-impl';
import { SketchContribution } from './contribution';
import { CompileSummaryProvider } from './verify-sketch';
/**
* (non-API) exported for tests
@ -43,6 +43,8 @@ export class UpdateArduinoState extends SketchContribution {
private readonly boardsDataStore: BoardsDataStore;
@inject(HostedPluginSupport)
private readonly hostedPluginSupport: HostedPluginSupport;
@inject(CompileSummaryProvider)
private readonly compileSummaryProvider: CompileSummaryProvider;
private readonly toDispose = new DisposableCollection();
@ -60,14 +62,13 @@ export class UpdateArduinoState extends SketchContribution {
this.configService.onDidChangeSketchDirUri((userDirUri) =>
this.updateUserDirPath(userDirUri)
),
this.commandService.onDidExecuteCommand(({ commandId, args }) => {
if (
commandId === 'arduino.languageserver.notifyBuildDidComplete' &&
isCompileSummary(args[0])
) {
this.updateCompileSummary(args[0]);
this.compileSummaryProvider.onDidChangeCompileSummary(
(compilerSummary) => {
if (compilerSummary) {
this.updateCompileSummary(compilerSummary);
}
}
}),
),
this.boardsDataStore.onDidChange((event) => {
const selectedFqbn =
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
@ -88,6 +89,10 @@ export class UpdateArduinoState extends SketchContribution {
this.updateSketchPath(this.sketchServiceClient.tryGetCurrentSketch());
this.updateUserDirPath(this.configService.tryGetSketchDirUri());
this.updateDataDirPath(this.configService.tryGetDataDirUri());
const { compileSummary } = this.compileSummaryProvider;
if (compileSummary) {
this.updateCompileSummary(compileSummary);
}
}
onStop(): void {

View File

@ -1,7 +1,8 @@
import { Emitter } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CoreService, sanitizeFqbn } from '../../common/protocol';
import { FQBN } from 'fqbn';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@ -103,6 +104,7 @@ export class UploadSketch extends CoreServiceContribution {
}
try {
const autoVerify = this.preferences['arduino.upload.autoVerify'];
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
@ -115,7 +117,7 @@ export class UploadSketch extends CoreServiceContribution {
'arduino-verify-sketch',
<VerifySketchParams>{
exportBinaries: false,
silent: true,
mode: autoVerify ? 'auto' : 'dry-run',
}
);
if (!verifyOptions) {
@ -126,6 +128,7 @@ export class UploadSketch extends CoreServiceContribution {
usingProgrammer,
verifyOptions
);
if (!uploadOptions) {
return;
}
@ -136,10 +139,37 @@ export class UploadSketch extends CoreServiceContribution {
const uploadResponse = await this.doWithProgress({
progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
task: (progressId, coreService) =>
coreService.upload({ ...uploadOptions, progressId }),
task: async (progressId, coreService, token) => {
try {
return await coreService.upload(
{ ...uploadOptions, progressId },
token
);
} catch (err) {
if (err.code === 4005) {
const uploadWithProgrammerOptions = await this.uploadOptions(
true,
verifyOptions
);
if (uploadWithProgrammerOptions) {
return coreService.upload(
{ ...uploadWithProgrammerOptions, progressId },
token
);
}
} else {
throw err;
}
}
},
keepOutput: true,
cancelable: true,
});
if (!uploadResponse) {
return;
}
// the port update is NOOP if nothing has changed
this.boardsServiceProvider.updateConfig(uploadResponse.portAfterUpload);
@ -172,7 +202,11 @@ export class UploadSketch extends CoreServiceContribution {
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
verifyOptions.fqbn, // already decorated FQBN
this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)),
this.boardsDataStore.getData(
verifyOptions.fqbn
? new FQBN(verifyOptions.fqbn).toString(true)
: undefined
),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);

View File

@ -1,46 +1,71 @@
import { Emitter, Event } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import type { CompileSummary, CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
CoreServiceContribution,
KeybindingRegistry,
MenuModelRegistry,
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CoreService } from '../../common/protocol';
import { CoreErrorHandler } from './core-error-handler';
export const CompileSummaryProvider = Symbol('CompileSummaryProvider');
export interface CompileSummaryProvider {
readonly compileSummary: CompileSummary | undefined;
readonly onDidChangeCompileSummary: Event<CompileSummary | undefined>;
}
export type VerifySketchMode =
/**
* When the user explicitly triggers the verify command from the primary UI: menu, toolbar, or keybinding. The UI shows the output, updates the toolbar items state, etc.
*/
| 'explicit'
/**
* When the verify phase automatically runs as part of the upload but there is no UI indication of the command: the toolbar items do not update.
*/
| 'auto'
/**
* The verify does not run. There is no UI indication of the command. For example, when the user decides to disable the auto verify (`'arduino.upload.autoVerify'`) to skips the code recompilation phase.
*/
| 'dry-run';
export interface VerifySketchParams {
/**
* Same as `CoreService.Options.Compile#exportBinaries`
*/
readonly exportBinaries?: boolean;
/**
* If `true`, there won't be any UI indication of the verify command in the toolbar. It's `false` by default.
* The mode specifying how verify should run. It's `'explicit'` by default.
*/
readonly silent?: boolean;
readonly mode?: VerifySketchMode;
}
/**
* - `"idle"` when neither verify, nor upload is running,
* - `"explicit-verify"` when only verify is running triggered by the user, and
* - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
* - `"idle"` when neither verify, nor upload is running
*/
type VerifyProgress = 'idle' | 'explicit-verify' | 'automatic-verify';
type VerifyProgress = 'idle' | VerifySketchMode;
@injectable()
export class VerifySketch extends CoreServiceContribution {
export class VerifySketch
extends CoreServiceContribution
implements CompileSummaryProvider
{
@inject(CoreErrorHandler)
private readonly coreErrorHandler: CoreErrorHandler;
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private readonly onDidChangeCompileSummaryEmitter = new Emitter<
CompileSummary | undefined
>();
private verifyProgress: VerifyProgress = 'idle';
private _compileSummary: CompileSummary | undefined;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
@ -54,10 +79,10 @@ export class VerifySketch extends CoreServiceContribution {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => this.verifyProgress !== 'explicit-verify',
isEnabled: () => this.verifyProgress !== 'explicit',
// toggled only when verify is running, but not toggled when automatic verify is running before the upload
// https://github.com/arduino/arduino-ide/pull/1750#pullrequestreview-1214762975
isToggled: () => this.verifyProgress === 'explicit-verify',
isToggled: () => this.verifyProgress === 'explicit',
execute: () =>
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
});
@ -105,6 +130,21 @@ export class VerifySketch extends CoreServiceContribution {
super.handleError(error);
}
get compileSummary(): CompileSummary | undefined {
return this._compileSummary;
}
private updateCompileSummary(
compileSummary: CompileSummary | undefined
): void {
this._compileSummary = compileSummary;
this.onDidChangeCompileSummaryEmitter.fire(this._compileSummary);
}
get onDidChangeCompileSummary(): Event<CompileSummary | undefined> {
return this.onDidChangeCompileSummaryEmitter.event;
}
private async verifySketch(
params?: VerifySketchParams
): Promise<CoreService.Options.Compile | undefined> {
@ -113,34 +153,44 @@ export class VerifySketch extends CoreServiceContribution {
}
try {
this.verifyProgress = params?.silent
? 'automatic-verify'
: 'explicit-verify';
this.verifyProgress = params?.mode ?? 'explicit';
this.onDidChangeEmitter.fire();
this.menuManager.update();
this.clearVisibleNotification();
this.coreErrorHandler.reset();
const dryRun = this.verifyProgress === 'dry-run';
const options = await this.options(params?.exportBinaries);
if (!options) {
return undefined;
}
await this.doWithProgress({
if (dryRun) {
return options;
}
const compileSummary = await this.doWithProgress({
progressText: nls.localize(
'arduino/sketch/compile',
'Compiling sketch...'
),
task: (progressId, coreService) =>
coreService.compile({
...options,
progressId,
}),
task: (progressId, coreService, token) =>
coreService.compile(
{
...options,
progressId,
},
token
),
cancelable: true,
});
this.messageService.info(
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
{ timeout: 3000 }
);
this.updateCompileSummary(compileSummary);
// Returns with the used options for the compilation
// so that follow-up tasks (such as upload) can reuse the compiled code.
// Note that the `fqbn` is already decorated with the board settings, if any.

View File

@ -509,11 +509,19 @@ export class CreateApi {
private async headers(): Promise<Record<string, string>> {
const token = await this.token();
return {
const headers: Record<string, string> = {
'content-type': 'application/json',
accept: 'application/json',
authorization: `Bearer ${token}`,
};
const sharedSpaceID =
this.arduinoPreferences['arduino.cloud.sharedSpaceID'];
if (sharedSpaceID) {
headers['x-organization'] = sharedSpaceID;
}
return headers;
}
private domain(apiVersion = 'v2'): string {

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import URI from '@theia/core/lib/common/uri';

View File

@ -5,7 +5,7 @@ import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import {
Stat,
FileType,

View File

@ -17,6 +17,7 @@ import {
} from '../../../common/protocol/ide-updater';
import { LocalStorageService } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { sanitize } from 'dompurify';
@injectable()
export class IDEUpdaterDialogProps extends DialogProps {}
@ -165,6 +166,51 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
goToDownloadPageButton.focus();
}
private appendDonateFooter() {
const footer = document.createElement('div');
footer.classList.add('ide-updater-dialog--footer');
const footerContent = document.createElement('div');
footerContent.classList.add('ide-updater-dialog--footer-content');
footer.appendChild(footerContent);
const footerLink = document.createElement('a');
footerLink.innerText = sanitize(
nls.localize('arduino/ide-updater/donateLinkText', 'donate to support us')
);
footerLink.classList.add('ide-updater-dialog--footer-link');
footerLink.onclick = () =>
this.openExternal('https://www.arduino.cc/en/donate');
const footerLinkIcon = document.createElement('span');
footerLinkIcon.title = nls.localize(
'arduino/ide-updater/donateLinkIconTitle',
'open donation page'
);
footerLinkIcon.classList.add('ide-updater-dialog--footer-link-icon');
footerLink.appendChild(footerLinkIcon);
const placeholderKey = '%%link%%';
const footerText = sanitize(
nls.localize(
'arduino/ide-updater/donateText',
'Open source is love, {0}',
placeholderKey
)
);
const placeholder = footerText.indexOf(placeholderKey);
if (placeholder !== -1) {
const parts = footerText.split(placeholderKey);
footerContent.appendChild(document.createTextNode(parts[0]));
footerContent.appendChild(footerLink);
footerContent.appendChild(document.createTextNode(parts[1]));
} else {
footerContent.appendChild(document.createTextNode(footerText));
footerContent.appendChild(footerLink);
}
this.controlPanel.insertAdjacentElement('afterend', footer);
}
private openDownloadPage(): void {
this.openExternal('https://www.arduino.cc/en/software');
this.close();
@ -187,6 +233,7 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
downloadStarted: true,
});
this.clearButtons();
this.appendDonateFooter();
this.updater.downloadUpdate();
}
@ -214,6 +261,7 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
}
override async open(
disposeOnResolve = true,
data: UpdateInfo | undefined = undefined
): Promise<UpdateInfo | undefined> {
if (data && data.version) {
@ -224,7 +272,7 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
error: undefined,
});
this.updateInfo = data;
return super.open();
return super.open(disposeOnResolve);
}
}

View File

@ -0,0 +1,107 @@
import React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactDialog } from '../theia/dialogs/dialogs';
import { nls } from '@theia/core';
import { DialogProps } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { AppService } from '../app-service';
import { sanitize } from 'dompurify';
@injectable()
export class VersionWelcomeDialogProps extends DialogProps {}
@injectable()
export class VersionWelcomeDialog extends ReactDialog<void> {
@inject(AppService)
private readonly appService: AppService;
@inject(WindowService)
private readonly windowService: WindowService;
constructor(
@inject(VersionWelcomeDialogProps)
protected override readonly props: VersionWelcomeDialogProps
) {
super({
title: nls.localize(
'arduino/versionWelcome/title',
'Welcome to a new version of the Arduino IDE!'
),
});
this.node.id = 'version-welcome-dialog-container';
this.contentNode.classList.add('version-welcome-dialog');
}
protected render(): React.ReactNode {
return (
<div>
<p>
{nls.localize(
'arduino/versionWelcome/donateMessage',
'Arduino is committed to keeping software free and open-source for everyone. Your donation helps us develop new features, improve libraries, and support millions of users worldwide.'
)}
</p>
<p className="bold">
{nls.localize(
'arduino/versionWelcome/donateMessage2',
'Please consider supporting our work on the free open source Arduino IDE.'
)}
</p>
</div>
);
}
override get value(): void {
return;
}
private appendButtons(): void {
const cancelButton = this.createButton(
nls.localize('arduino/versionWelcome/cancelButton', 'Maybe later')
);
cancelButton.classList.add('secondary');
cancelButton.classList.add('cancel-button');
this.addAction(cancelButton, this.close.bind(this), 'click');
this.controlPanel.appendChild(cancelButton);
const donateButton = this.createButton(
nls.localize('arduino/versionWelcome/donateButton', 'Donate now')
);
this.addAction(donateButton, this.onDonateButtonClick.bind(this), 'click');
this.controlPanel.appendChild(donateButton);
donateButton.focus();
}
private onDonateButtonClick(): void {
this.openDonationPage();
this.close();
}
private readonly openDonationPage = () => {
const url = 'https://www.arduino.cc/en/donate';
this.windowService.openNewWindow(url, { external: true });
};
private async updateTitleVersion(): Promise<void> {
const appInfo = await this.appService.info();
const { appVersion } = appInfo;
if (appVersion) {
this.titleNode.innerText = sanitize(
nls.localize(
'arduino/versionWelcome/titleWithVersion',
'Welcome to the new Arduino IDE {0}!',
appVersion
)
);
}
}
protected override onAfterAttach(msg: Message): void {
this.update();
this.appendButtons();
this.updateTitleVersion();
super.onAfterAttach(msg);
}
}

View File

@ -0,0 +1,3 @@
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.878141 10.6219C0.960188 10.7039 1.07147 10.75 1.1875 10.75H10.8125C10.9285 10.75 11.0398 10.7039 11.1219 10.6219C11.2039 10.5398 11.25 10.4285 11.25 10.3125V6.81252C11.25 6.69648 11.2039 6.5852 11.1219 6.50316C11.0398 6.42111 10.9285 6.37502 10.8125 6.37502C10.6965 6.37502 10.5852 6.42111 10.5031 6.50316C10.4211 6.5852 10.375 6.69648 10.375 6.81252V9.87502H1.625V1.12502H4.6875C4.80353 1.12502 4.91481 1.07892 4.99686 0.996874C5.07891 0.914827 5.125 0.803548 5.125 0.687515C5.125 0.571483 5.07891 0.460203 4.99686 0.378156C4.91481 0.296109 4.80353 0.250015 4.6875 0.250015H1.1875C1.07147 0.250015 0.960188 0.296109 0.878141 0.378156C0.796094 0.460203 0.75 0.571483 0.75 0.687515V10.3125C0.75 10.4285 0.796094 10.5398 0.878141 10.6219ZM11.25 4.62502V0.687515C11.25 0.571483 11.2039 0.460203 11.1219 0.378156C11.0398 0.296109 10.9285 0.250015 10.8125 0.250015H6.875C6.75897 0.250015 6.64769 0.296109 6.56564 0.378156C6.48359 0.460203 6.4375 0.571483 6.4375 0.687515C6.4375 0.803548 6.48359 0.914827 6.56564 0.996874C6.64769 1.07892 6.75897 1.12502 6.875 1.12502H9.75375L5.68937 5.18939C5.64837 5.23006 5.61582 5.27845 5.59361 5.33176C5.5714 5.38508 5.55996 5.44226 5.55996 5.50002C5.55996 5.55777 5.5714 5.61495 5.59361 5.66827C5.61582 5.72158 5.64837 5.76997 5.68937 5.81064C5.73005 5.85165 5.77843 5.88419 5.83175 5.90641C5.88506 5.92862 5.94224 5.94005 6 5.94005C6.05776 5.94005 6.11494 5.92862 6.16825 5.90641C6.22157 5.88419 6.26995 5.85165 6.31062 5.81064L10.375 1.74627V4.62502C10.375 4.74105 10.4211 4.85233 10.5031 4.93437C10.5852 5.01642 10.6965 5.06252 10.8125 5.06252C10.9285 5.06252 11.0398 5.01642 11.1219 4.93437C11.2039 4.85233 11.25 4.74105 11.25 4.62502Z" fill="#008184"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -30,7 +30,7 @@ export class IDEUpdaterCommands implements CommandContribution {
try {
const updateInfo = await this.updater.checkForUpdates(initialCheck);
if (!!updateInfo) {
this.updaterDialog.open(updateInfo);
this.updaterDialog.open(true, updateInfo);
} else {
this.messageService.info(
nls.localize(

View File

@ -12,15 +12,13 @@ import {
LibrarySearch,
LibraryService,
} from '../../common/protocol/library-service';
import {
ListWidget,
UserAbortError,
} from '../widgets/component-list/list-widget';
import { ListWidget } from '../widgets/component-list/list-widget';
import { Installable } from '../../common/protocol';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
import { nls } from '@theia/core/lib/common';
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
import { findChildTheiaButton, splitByBoldTag } from '../utils/dom';
import { UserAbortError } from '../../common/protocol/progressible';
@injectable()
export class LibraryListWidget extends ListWidget<

View File

@ -6,7 +6,7 @@ import {
import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import {
IndexUpdateDidCompleteParams,
IndexUpdateDidFailParams,
@ -46,7 +46,7 @@ export class NotificationCenter
new Emitter<ProgressMessage>();
private readonly indexUpdateDidFailEmitter =
new Emitter<IndexUpdateDidFailParams>();
private readonly daemonDidStartEmitter = new Emitter<string>();
private readonly daemonDidStartEmitter = new Emitter<number>();
private readonly daemonDidStopEmitter = new Emitter<void>();
private readonly configDidChangeEmitter = new Emitter<ConfigState>();
private readonly platformDidInstallEmitter = new Emitter<{
@ -136,7 +136,7 @@ export class NotificationCenter
this.indexUpdateDidFailEmitter.fire(params);
}
notifyDaemonDidStart(port: string): void {
notifyDaemonDidStart(port: number): void {
this.daemonDidStartEmitter.fire(port);
}

View File

@ -9,7 +9,7 @@ import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { Sketch, SketchesService } from '../common/protocol';
import { ConfigServiceClient } from './config/config-service-client';
import {
@ -74,6 +74,7 @@ export class SketchesServiceClientImpl
const sketchDirUri = this.configService.tryGetSketchDirUri();
this.watchSketchbookDir(sketchDirUri);
const refreshCurrentSketch = async () => {
await this.workspaceService.ready;
const currentSketch = await this.loadCurrentSketch();
const ideTempFolderUri = await this.getIdeTempFolderUriForSketch(
currentSketch
@ -287,7 +288,7 @@ export class SketchesServiceClientImpl
* `true` if the `uri` is not contained in any of the opened workspaces. Otherwise, `false`.
*/
isReadOnly(uri: URI | monaco.Uri | string): boolean {
const toCheck = uri instanceof URI ? uri : new URI(uri);
const toCheck = uri instanceof URI ? uri : new URI(uri.toString());
if (toCheck.scheme === 'user-storage') {
return false;
}

View File

@ -98,16 +98,12 @@
color: var(--theia-textLink-foreground);
}
.account-icon {
img.arduino-account-picture {
width: var(--theia-private-sidebar-icon-size);
height: var(--theia-private-sidebar-icon-size);
border-radius: 50%;
overflow: hidden;
}
.account-icon > img {
max-width: 100%;
max-height: 100%;
border-radius: 50%;
}
.connected-status-icon {

View File

@ -34,6 +34,37 @@
min-width: 0;
}
.ide-updater-dialog--footer {
display: inline-block;
margin-top: -16px;
padding: 12px 0 24px 0;
border-top: 1px solid var(--theia-editorWidget-border);
}
.ide-updater-dialog--footer-content {
float: right;
}
.ide-updater-dialog--footer-link {
display: inline-block;
color: var(--theia-textLink-foreground);
font-weight: 500;
line-height: 13px;
}
.ide-updater-dialog--footer-link:hover {
color: var(--theia-textLink-foreground);
cursor: pointer;
}
.ide-updater-dialog--footer-link-icon {
display: inline-block;
-webkit-mask: url(../icons/link-open-icon.svg) center no-repeat;
background-color: var(--theia-textLink-foreground);
height: 12px;
width: 12px;
cursor: pointer;
transform: translateY(2px);
margin-left: 4px;
}
.ide-updater-dialog .changelog {
color: var(--theia-editor-foreground);
background-color: var(--theia-editor-background);
@ -109,6 +140,7 @@
max-height: 100%;
overflow: hidden;
display: flex;
padding-bottom: 20px !important;
}
#ide-updater-dialog-container .skip-version-button {

View File

@ -10,6 +10,7 @@
@import "./settings-dialog.css";
@import "./firmware-uploader-dialog.css";
@import "./ide-updater-dialog.css";
@import "./version-welcome-dialog.css";
@import "./certificate-uploader-dialog.css";
@import "./user-fields-dialog.css";
@import "./debug.css";

View File

@ -0,0 +1,7 @@
#version-welcome-dialog-container > .dialogBlock {
width: 546px;
.bold {
font-weight: bold;
}
}

View File

@ -13,6 +13,8 @@ import { MessageService } from '@theia/core/lib/common/message-service';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ApplicationConnectionStatusContribution } from './connection-status-service';
import { ToolbarAwareTabBar } from './tab-bars';
import { find } from '@theia/core/shared/@phosphor/algorithm';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
@injectable()
export class ApplicationShell extends TheiaApplicationShell {
@ -48,6 +50,38 @@ export class ApplicationShell extends TheiaApplicationShell {
return super.addWidget(widget, { ...options, ref });
}
override doRevealWidget(id: string): Widget | undefined {
let widget = find(this.mainPanel.widgets(), (w) => w.id === id);
if (!widget) {
widget = find(this.bottomPanel.widgets(), (w) => w.id === id);
if (widget) {
this.expandBottomPanel();
}
}
if (widget) {
const tabBar = this.getTabBarFor(widget);
if (tabBar) {
tabBar.currentTitle = widget.title;
}
}
if (!widget) {
widget = this.leftPanelHandler.expand(id);
}
if (!widget) {
widget = this.rightPanelHandler.expand(id);
}
if (widget) {
// Prevent focusing the output widget when is updated
// See https://github.com/arduino/arduino-ide/issues/2679
if (!(widget instanceof OutputWidget)) {
this.windowService.focus();
}
return widget;
} else {
return this.secondaryWindowHandler.revealWidget(id);
}
}
override handleEvent(): boolean {
// NOOP, dragging has been disabled
return false;

View File

@ -2,7 +2,7 @@ import {
CommonCommands,
CommonFrontendContribution as TheiaCommonFrontendContribution,
} from '@theia/core/lib/browser/common-frontend-contribution';
import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application';
import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application-contribution';
import type { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import type { CommandRegistry } from '@theia/core/lib/common/command';
import type { MenuModelRegistry } from '@theia/core/lib/common/menu';

View File

@ -1,10 +1,10 @@
import {
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service';
import type { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/index';
import type { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { WebSocketConnectionSource } from '@theia/core/lib/browser/messaging/ws-connection-source';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import { Disposable } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
@ -74,8 +74,8 @@ export class DaemonPort implements FrontendApplicationContribution {
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
private readonly onPortDidChangeEmitter = new Emitter<string | undefined>();
private _port: string | undefined;
private readonly onPortDidChangeEmitter = new Emitter<number | undefined>();
private _port: number | undefined;
onStart(): void {
this.daemon.tryGetPort().then(
@ -91,15 +91,15 @@ export class DaemonPort implements FrontendApplicationContribution {
this.onPortDidChangeEmitter.dispose();
}
get port(): string | undefined {
get port(): number | undefined {
return this._port;
}
get onDidChangePort(): Event<string | undefined> {
get onDidChangePort(): Event<number | undefined> {
return this.onPortDidChangeEmitter.event;
}
private setPort(port: string | undefined): void {
private setPort(port: number | undefined): void {
const oldPort = this._port;
this._port = port;
if (this._port !== oldPort) {
@ -114,8 +114,8 @@ export class FrontendConnectionStatusService extends TheiaFrontendConnectionStat
private readonly daemonPort: DaemonPort;
@inject(IsOnline)
private readonly isOnline: IsOnline;
@inject(WebSocketConnectionProvider)
private readonly connectionProvider: WebSocketConnectionProvider;
@inject(WebSocketConnectionSource)
private readonly connectionSource: WebSocketConnectionSource;
@postConstruct()
protected override init(): void {
@ -128,7 +128,7 @@ export class FrontendConnectionStatusService extends TheiaFrontendConnectionStat
}
protected override async performPingRequest(): Promise<void> {
if (!this.connectionProvider['socket'].connected) {
if (!this.connectionSource['socket'].connected) {
this.updateStatus(false);
return;
}
@ -171,8 +171,8 @@ export class ApplicationConnectionStatusContribution extends TheiaApplicationCon
private readonly notificationManager: NotificationManager;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@inject(WebSocketConnectionProvider)
private readonly connectionProvider: WebSocketConnectionProvider;
@inject(WebSocketConnectionSource)
private readonly connectionSource: WebSocketConnectionSource;
private readonly offlineStatusDidChangeEmitter = new Emitter<
OfflineConnectionStatus | undefined
@ -202,7 +202,7 @@ export class ApplicationConnectionStatusContribution extends TheiaApplicationCon
const params = <OfflineMessageParams>{
port: this.daemonPort.port,
online: this.isOnline.online,
backendConnected: this.connectionProvider['socket'].connected, // https://github.com/arduino/arduino-ide/issues/2081
backendConnected: this.connectionSource['socket'].connected, // https://github.com/arduino/arduino-ide/issues/2081
};
this._offlineStatus = offlineConnectionStatusType(params);
const { text, tooltip } = offlineMessage(params);

View File

@ -1,5 +1,5 @@
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
import type { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
import type { SidebarMenuItem } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
import type { MenuPath } from '@theia/core/lib/common/menu';
import { nls } from '@theia/core/lib/common/nls';
import {
@ -46,46 +46,45 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
this.contextMenuRenderer.render(options);
}
protected override render(): React.ReactNode {
return (
<React.Fragment>
{this.menus.map((menu) => this.renderMenu(menu))}
</React.Fragment>
);
}
private renderMenu(menu: SidebarMenu): React.ReactNode {
override renderItem(item: SidebarMenuItem): React.ReactNode {
// Removes the _Settings_ (cog) icon from the left sidebar
if (menu.id === 'settings-menu') {
if (item.menu.id === 'settings-menu') {
return undefined;
}
const arduinoAccount = menu.id === accountMenu.id;
const picture =
const arduinoAccount = item.menu.id === accountMenu.id;
const arduinoAccountPicture =
arduinoAccount &&
this.connectionStatue.offlineStatus !== 'internet' &&
this.createFeatures.session?.account.picture;
const className = typeof picture === 'string' ? undefined : menu.iconClass;
return (
<i
key={menu.id}
className={className}
title={menu.title}
onClick={(e) => this.onClick(e, menu.menuPath)}
<div
key={item.menu.id}
className="theia-sidebar-menu-item"
title={item.menu.title}
onClick={(e) => this.onClick(e, item.menu.menuPath)}
onMouseDown={this.onMouseDown}
onMouseEnter={(e) => this.onMouseEnter(e, item.menu.title)}
onMouseOut={this.onMouseOut}
>
{picture && (
<div className="account-icon">
{arduinoAccountPicture ? (
<i>
<img
src={picture}
className="arduino-account-picture"
src={arduinoAccountPicture}
alt={nls.localize(
'arduino/cloud/profilePicture',
'Profile picture'
)}
/>
</div>
</i>
) : (
<i className={item.menu.iconClass} />
)}
</i>
{item.badge && (
<div className="theia-badge-decorator-sidebar">{item.badge}</div>
)}
</div>
);
}
}

View File

@ -1,6 +1,7 @@
import { injectable } from '@theia/core/shared/inversify';
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
import { DefaultDebugSessionFactory as TheiaDefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
import {
DebugAdapterPath,
@ -12,6 +13,7 @@ import { DebugSession } from './debug-session';
@injectable()
export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory {
override get(
manager: DebugSessionManager,
sessionId: string,
options: DebugConfigurationSessionOptions,
parentSession?: DebugSession
@ -20,12 +22,12 @@ export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory
sessionId,
() =>
new Promise<DebugChannel>((resolve) =>
this.connectionProvider.openChannel(
this.connectionProvider.listen(
`${DebugAdapterPath}/${sessionId}`,
(wsChannel) => {
(_, wsChannel) => {
resolve(new ForwardingDebugChannel(wsChannel));
},
{ reconnecting: false }
false
)
),
this.getTraceOutputChannel()
@ -35,6 +37,9 @@ export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory
sessionId,
options,
parentSession,
this.testService,
options.testRun,
manager,
connection,
this.terminalService,
this.editorManager,

View File

@ -1,21 +0,0 @@
import { injectable, postConstruct } from '@theia/core/shared/inversify';
import { EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser/editor-command';
@injectable()
export class EditorCommandContribution extends TheiaEditorCommandContribution {
@postConstruct()
protected override init(): void {
// Workaround for https://github.com/eclipse-theia/theia/issues/8722.
this.editorPreferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => {
if (preferenceName === 'files.autoSave') {
const autoSaveWasOnBeforeChange = !oldValue || oldValue !== 'off';
const autoSaveIsOnAfterChange = !newValue || newValue !== 'off';
if (!autoSaveWasOnBeforeChange && autoSaveIsOnAfterChange) {
this.shell.saveAll();
}
}
}
);
}
}

View File

@ -1,19 +1,18 @@
import { injectable } from '@theia/core/shared/inversify';
import { TextEditor } from '@theia/editor/lib/browser';
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
@injectable()
export class EditorContribution extends TheiaEditorContribution {
protected override updateLanguageStatus(
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
editor: TextEditor | undefined
// eslint-disable-next-line @typescript-eslint/no-unused-vars
..._: Parameters<TheiaEditorContribution['updateLanguageStatus']>
): void {
// NOOP
}
protected override updateEncodingStatus(
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
editor: TextEditor | undefined
// eslint-disable-next-line @typescript-eslint/no-unused-vars
..._: Parameters<TheiaEditorContribution['updateEncodingStatus']>
): void {
// https://github.com/arduino/arduino-ide/issues/1393
// NOOP

View File

@ -36,7 +36,7 @@ export class FileResourceResolver extends TheiaFileResourceResolver {
);
}
return new WriteQueuedFileResource(uri, this.fileService, {
isReadonly: stat?.isReadonly ?? false,
readOnly: stat?.isReadonly ?? false,
shouldOverwrite: () => this.shouldOverwrite(uri),
shouldOpenAsText: (error) => this.shouldOpenAsText(uri, error),
});

View File

@ -2,6 +2,7 @@ import React from '@theia/core/shared/react';
import { NotificationComponent as TheiaNotificationComponent } from '@theia/messages/lib/browser/notification-component';
import { nls } from '@theia/core/lib/common';
import { codicon } from '@theia/core/lib/browser';
import { sanitize } from 'dompurify';
export class NotificationComponent extends TheiaNotificationComponent {
override render(): React.ReactNode {
@ -20,7 +21,7 @@ export class NotificationComponent extends TheiaNotificationComponent {
/>
<div className="theia-notification-message">
<span
dangerouslySetInnerHTML={{ __html: message }}
dangerouslySetInnerHTML={{ __html: sanitize(message) }}
onClick={this.onMessageClick}
/>
</div>

View File

@ -117,7 +117,7 @@ export function maybeUpdateReadOnlyState(
const model = editor.document;
const oldReadOnly = model.readOnly;
const resource = model['resource'];
const newReadOnly = Boolean(resource.isReadonly) || isReadOnly(resource.uri);
const newReadOnly = Boolean(resource.readOnly) || isReadOnly(resource.uri);
if (oldReadOnly !== newReadOnly) {
editor.getControl().updateOptions({ readOnly: newReadOnly });
if (newReadOnly) {

View File

@ -20,7 +20,7 @@ export class MonacoTextModelService extends TheiaMonacoTextModelService {
.getContributions()
.find(({ scheme }) => resource.uri.scheme === scheme);
const readOnly =
Boolean(resource.isReadonly) ||
Boolean(resource.readOnly) ||
this.sketchesServiceClient.isReadOnly(resource.uri);
return factory
? factory.createModel(resource)

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { ThemeService } from '@theia/core/lib/browser/theming';
import {
Disposable,

View File

@ -1,11 +1,9 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import type { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { injectable, interfaces } from '@theia/core/shared/inversify';
import {
PluginContributions,
HostedPluginSupport as TheiaHostedPluginSupport,
} from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { HostedPluginSupport } from '../../hosted/hosted-plugin-support';
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import type { PluginContributions } from '@theia/plugin-ext/lib/hosted/common/hosted-plugin';
import type { HostedPluginSupport } from '../../hosted/hosted-plugin-support';
@injectable()
export class HostedPluginSupportImpl

View File

@ -0,0 +1,9 @@
import { TestViewContribution as TheiaTestViewContribution } from '@theia/test/lib/browser/view/test-view-contribution';
import { injectable } from 'inversify';
@injectable()
export class TestViewContribution extends TheiaTestViewContribution {
override async initializeLayout(): Promise<void> {
// NOOP
}
}

View File

@ -4,6 +4,7 @@ import {
TabBarToolbarRegistry,
TabBarToolbarItem,
ReactTabBarToolbarItem,
RenderedToolbarItem,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { ReactWidget } from '@theia/core/lib/browser';
@ -14,7 +15,7 @@ export const ARDUINO_TOOLBAR_ITEM_CLASS = 'arduino-tool-item';
export namespace ArduinoToolbarComponent {
export interface Props {
side: 'left' | 'right';
items: (TabBarToolbarItem | ReactTabBarToolbarItem)[];
items: TabBarToolbarItem[];
commands: CommandRegistry;
labelParser: LabelParser;
commandIsEnabled: (id: string) => boolean;
@ -34,7 +35,7 @@ export class ArduinoToolbarComponent extends React.Component<
this.state = { tooltip: '' };
}
protected renderItem = (item: TabBarToolbarItem) => {
protected renderItem = (item: RenderedToolbarItem) => {
let innerText = '';
let className = `arduino-tool-icon ${item.id}-icon`;
if (item.text) {
@ -46,7 +47,8 @@ export class ArduinoToolbarComponent extends React.Component<
}
}
}
const command = this.props.commands.getCommand(item.command);
const command =
item.command && this.props.commands.getCommand(item.command);
const cls = `${ARDUINO_TOOLBAR_ITEM_CLASS} ${
TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM
} ${command && this.props.commandIsEnabled(command.id) ? 'enabled' : ''} ${
@ -80,7 +82,9 @@ export class ArduinoToolbarComponent extends React.Component<
const items = [
<React.Fragment key={this.props.side + '-arduino-toolbar-tooltip'}>
{[...this.props.items].map((item) =>
TabBarToolbarItem.is(item) ? this.renderItem(item) : item.render()
ReactTabBarToolbarItem.is(item)
? item.render()
: this.renderItem(item)
)}
</React.Fragment>,
];
@ -94,10 +98,7 @@ export class ArduinoToolbarComponent extends React.Component<
}
export class ArduinoToolbar extends ReactWidget {
protected items = new Map<
string,
TabBarToolbarItem | ReactTabBarToolbarItem
>();
protected items = new Map<string, TabBarToolbarItem>();
constructor(
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
@ -112,9 +113,7 @@ export class ArduinoToolbar extends ReactWidget {
this.tabBarToolbarRegistry.onDidChange(() => this.updateToolbar());
}
protected updateItems(
items: Array<TabBarToolbarItem | ReactTabBarToolbarItem>
): void {
protected updateItems(items: Array<TabBarToolbarItem>): void {
this.items.clear();
const revItems = items
.sort(TabBarToolbarItem.PRIORITY_COMPARATOR)
@ -163,7 +162,7 @@ export class ArduinoToolbar extends ReactWidget {
protected executeCommand = (e: React.MouseEvent<HTMLElement>) => {
const item = this.items.get(e.currentTarget.id);
if (TabBarToolbarItem.is(item)) {
if (item && item.command) {
this.commands.executeCommand(item.command, this, e.target);
}
};

View File

@ -2,7 +2,7 @@ import React from '@theia/core/shared/react';
import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { Installable } from '../../../common/protocol/installable';
import type { ListItemRenderer } from './list-item-renderer';
import { UserAbortError } from './list-widget';
import { UserAbortError } from '../../../common/protocol/progressible';
export class ComponentListItem<
T extends ArduinoComponent

View File

@ -5,7 +5,10 @@ import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { Searchable } from '../../../common/protocol/searchable';
import { ExecuteWithProgress } from '../../../common/protocol/progressible';
import {
ExecuteWithProgress,
UserAbortError,
} from '../../../common/protocol/progressible';
import {
Installable,
libraryInstallFailed,
@ -13,7 +16,7 @@ import {
} from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { SearchBar } from './search-bar';
import { ListWidget, UserAbortError } from './list-widget';
import { ListWidget } from './list-widget';
import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer';
import {

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import {
OpenerOptions,
OpenHandler,

View File

@ -192,10 +192,3 @@ export namespace ListWidget {
readonly defaultSearchOptions: S;
}
}
export class UserAbortError extends Error {
constructor(message = 'User abort') {
super(message);
Object.setPrototypeOf(this, UserAbortError.prototype);
}
}

View File

@ -127,6 +127,9 @@ export class SketchbookTreeModel extends FileTreeModel {
if (preferenceName === 'arduino.sketchbook.showAllFiles') {
this.updateRoot();
}
if (preferenceName === 'arduino.cloud.sharedSpaceID') {
this.updateRoot();
}
})
);

View File

@ -3,7 +3,7 @@ import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { MainMenuManager } from '../../../common/main-menu-manager';
import { ArduinoPreferences } from '../../arduino-preferences';
import { SketchbookWidget } from './sketchbook-widget';

View File

@ -39,3 +39,5 @@ export const noSketchOpened = nls.localize(
'arduino/common/noSketchOpened',
'No sketch opened'
);
export const userAbort = nls.localize('arduino/common/userAbort', 'User abort');

View File

@ -5,14 +5,14 @@ export interface ArduinoDaemon {
* Returns with a promise that resolves with the port
* of the CLI daemon when it's up and running.
*/
getPort(): Promise<string>;
getPort(): Promise<number>;
/**
* Unlike `getPort` this method returns with a promise
* that resolves to `undefined` when the daemon is not running.
* Otherwise resolves to the CLI daemon port.
*/
tryGetPort(): Promise<string | undefined>;
start(): Promise<string>;
tryGetPort(): Promise<number | undefined>;
start(): Promise<number>;
stop(): Promise<void>;
restart(): Promise<string>;
restart(): Promise<number>;
}

View File

@ -1,4 +1,5 @@
import { nls } from '@theia/core/lib/common/nls';
import { FQBN } from 'fqbn';
import type { MaybePromise } from '@theia/core/lib/common/types';
import type URI from '@theia/core/lib/common/uri';
import {
@ -66,7 +67,10 @@ export interface BoardsService
skipPostInstall?: boolean;
}): Promise<void>;
getDetectedPorts(): Promise<DetectedPorts>;
getBoardDetails(options: { fqbn: string }): Promise<BoardDetails | undefined>;
getBoardDetails(options: {
fqbn: string;
forceRefresh?: boolean;
}): Promise<BoardDetails | undefined>;
getBoardPackage(options: {
id: string /* TODO: change to PlatformIdentifier type? */;
}): Promise<BoardsPackage | undefined>;
@ -75,6 +79,9 @@ export interface BoardsService
}): Promise<BoardsPackage | undefined>;
searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]>;
getInstalledBoards(): Promise<BoardWithPackage[]>;
/**
* Returns with all installed platforms including the manually installed ones.
*/
getInstalledPlatforms(): Promise<BoardsPackage[]>;
getBoardUserFields(options: {
fqbn: string;
@ -95,7 +102,7 @@ export interface CheckDebugEnabledParams {
* The FQBN might contain custom board config options. For example, `arduino:esp32:nano_nora:USBMode=hwcdc,option2=value2`.
*/
readonly fqbn: string;
readonly programmer: string;
readonly programmer?: string;
}
export interface BoardSearch extends Searchable.Options {
@ -364,40 +371,6 @@ export interface ConfigOption {
readonly values: ConfigValue[];
}
export namespace ConfigOption {
/**
* Appends the configuration options to the `fqbn` argument.
* Throws an error if the `fqbn` does not have the `segment(':'segment)*` format.
* The provided output format is always segment(':'segment)*(':'option'='value(','option'='value)*)?
*/
export function decorate(
fqbn: string,
configOptions: ConfigOption[]
): string {
if (!configOptions.length) {
return fqbn;
}
const toValue = (values: ConfigValue[]) => {
const selectedValue = values.find(({ selected }) => selected);
if (!selectedValue) {
console.warn(
`None of the config values was selected. Values were: ${JSON.stringify(
values
)}`
);
return undefined;
}
return selectedValue.value;
};
const options = configOptions
.map(({ option, values }) => [option, toValue(values)])
.filter(([, value]) => !!value)
.map(([option, value]) => `${option}=${value}`)
.join(',');
return `${fqbn}:${options}`;
}
export class ConfigOptionError extends Error {
constructor(message: string) {
super(message);
@ -571,28 +544,13 @@ export namespace Board {
}
}
/**
* Throws an error if the `fqbn` argument is not sanitized. A sanitized FQBN has the `VENDOR:ARCHITECTURE:BOARD_ID` construct.
*/
export function assertSanitizedFqbn(fqbn: string): void {
if (fqbn.split(':').length !== 3) {
throw new Error(
`Expected a sanitized FQBN with three segments in the following format: 'VENDOR:ARCHITECTURE:BOARD_ID'. Got ${fqbn} instead.`
);
}
}
/**
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
*/
export function sanitizeFqbn(fqbn: string | undefined): string | undefined {
if (!fqbn) {
return undefined;
}
const [vendor, arch, id] = fqbn.split(':');
return `${vendor}:${arch}:${id}`;
export function sanitizeFqbn(fqbn: string): string {
return new FQBN(fqbn).sanitize().toString();
}
export type PlatformIdentifier = Readonly<{ vendorId: string; arch: string }>;
@ -749,8 +707,8 @@ export function boardIdentifierEquals(
return false; // TODO: This a strict now. Maybe compare name in the future.
}
if (left.fqbn && right.fqbn) {
const leftFqbn = options.looseFqbn ? sanitizeFqbn(left.fqbn) : left.fqbn;
const rightFqbn = options.looseFqbn ? sanitizeFqbn(right.fqbn) : right.fqbn;
const leftFqbn = new FQBN(left.fqbn).toString(options.looseFqbn);
const rightFqbn = new FQBN(right.fqbn).toString(options.looseFqbn);
return leftFqbn === rightFqbn;
}
// No more Genuino hack.

View File

@ -1,4 +1,5 @@
import { ApplicationError } from '@theia/core/lib/common/application-error';
import type { CancellationToken } from '@theia/core/lib/common/cancellation';
import { nls } from '@theia/core/lib/common/nls';
import type {
Location,
@ -7,7 +8,7 @@ import type {
} from '@theia/core/shared/vscode-languageserver-protocol';
import type { CompileSummary as ApiCompileSummary } from 'vscode-arduino-api';
import type { BoardUserField, Installable } from '../../common/protocol/';
import { isPortIdentifier, PortIdentifier, Programmer } from './boards-service';
import { PortIdentifier, Programmer, isPortIdentifier } from './boards-service';
import type { IndexUpdateSummary } from './notification-service';
import type { Sketch } from './sketches-service';
@ -70,6 +71,7 @@ export namespace CoreError {
Upload: 4002,
UploadUsingProgrammer: 4003,
BurnBootloader: 4004,
UploadRequiresProgrammer: 4005,
};
export const VerifyFailed = declareCoreError(Codes.Verify);
export const UploadFailed = declareCoreError(Codes.Upload);
@ -77,6 +79,10 @@ export namespace CoreError {
Codes.UploadUsingProgrammer
);
export const BurnBootloaderFailed = declareCoreError(Codes.BurnBootloader);
export const UploadRequiresProgrammer = declareCoreError(
Codes.UploadRequiresProgrammer
);
export function is(
error: unknown
): error is ApplicationError<number, ErrorLocation[]> {
@ -162,9 +168,18 @@ export function isUploadResponse(arg: unknown): arg is UploadResponse {
export const CoreServicePath = '/services/core-service';
export const CoreService = Symbol('CoreService');
export interface CoreService {
compile(options: CoreService.Options.Compile): Promise<void>;
upload(options: CoreService.Options.Upload): Promise<UploadResponse>;
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
compile(
options: CoreService.Options.Compile,
cancellationToken?: CancellationToken
): Promise<CompileSummary | undefined>;
upload(
options: CoreService.Options.Upload,
cancellationToken?: CancellationToken
): Promise<UploadResponse>;
burnBootloader(
options: CoreService.Options.Bootloader,
cancellationToken?: CancellationToken
): Promise<void>;
/**
* Refreshes the underling core gRPC client for the Arduino CLI.
*/

View File

@ -71,3 +71,4 @@ export interface IDEUpdaterClient {
}
export const SKIP_IDE_VERSION = 'skipIDEVersion';
export const LAST_USED_IDE_VERSION = 'lastUsedIDEVersion';

View File

@ -51,7 +51,7 @@ export interface NotificationServiceClient {
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void;
// Daemon
notifyDaemonDidStart(port: string): void;
notifyDaemonDidStart(port: number): void;
notifyDaemonDidStop(): void;
// CLI config

View File

@ -1,22 +1,48 @@
import { ApplicationError } from '@theia/core/lib/common/application-error';
import type { CancellationToken } from '@theia/core/lib/common/cancellation';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import type { MessageService } from '@theia/core/lib/common/message-service';
import type { Progress } from '@theia/core/lib/common/message-service-protocol';
import { userAbort } from '../nls';
import type { ResponseServiceClient } from './response-service';
export const UserAbortApplicationError = ApplicationError.declare(
9999,
(message: string, uri: string) => {
return {
message,
data: { uri },
};
}
);
export class UserAbortError extends Error {
constructor() {
super(userAbort);
Object.setPrototypeOf(this, UserAbortError.prototype);
}
}
export namespace ExecuteWithProgress {
export async function doWithProgress<T>(options: {
run: ({ progressId }: { progressId: string }) => Promise<T>;
run: ({
progressId,
cancellationToken,
}: {
progressId: string;
cancellationToken?: CancellationToken;
}) => Promise<T>;
messageService: MessageService;
responseService: ResponseServiceClient;
progressText: string;
keepOutput?: boolean;
cancelable?: boolean;
}): Promise<T> {
return withProgress(
options.progressText,
options.messageService,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async (progress, _token) => {
async (progress, token) => {
const progressId = progress.id;
const toDispose = options.responseService.onProgressDidChange(
(progressMessage) => {
@ -30,24 +56,29 @@ export namespace ExecuteWithProgress {
if (!options.keepOutput) {
options.responseService.clearOutput();
}
const result = await options.run({ progressId });
const result = await options.run({
progressId,
cancellationToken: token,
});
return result;
} finally {
toDispose.dispose();
}
}
},
options.cancelable
);
}
export async function withProgress<T>(
text: string,
messageService: MessageService,
cb: (progress: Progress, token: CancellationToken) => Promise<T>
cb: (progress: Progress, token: CancellationToken) => Promise<T>,
cancelable = false
): Promise<T> {
const cancellationSource = new CancellationTokenSource();
const { token } = cancellationSource;
const progress = await messageService.showProgress(
{ text, options: { cancelable: false } },
{ text, options: { cancelable } },
() => cancellationSource.cancel()
);
try {

View File

@ -141,13 +141,14 @@ export interface SketchesService {
/**
* This is the JS/TS re-implementation of [`GenBuildPath`](https://github.com/arduino/arduino-cli/blob/c0d4e4407d80aabad81142693513b3306759cfa6/arduino/sketch/sketch.go#L296-L306) of the CLI.
* Pass in a sketch and get the build temporary folder filesystem path calculated from the main sketch file location. Can be multiple ones. This method does not check the existence of the sketch.
* Since CLI v1.1.0 the default sketch folder is the os user cache dir. See https://github.com/arduino/arduino-cli/pull/2673/commits/d2ffeb06ca6360a211d5aa7ddd11505212ffb1b9
*
* The case sensitivity of the drive letter on Windows matters when the CLI calculates the MD5 hash of the temporary build folder.
* IDE2 does not know and does not want to rely on how the CLI treats the paths: with lowercase or uppercase drive letters.
* Hence, IDE2 has to provide multiple build paths on Windows. This hack will be obsolete when the CLI can provide error codes:
* https://github.com/arduino/arduino-cli/issues/1762.
*/
tempBuildPath(sketch: SketchRef): Promise<string[]>;
getBuildPath(sketch: SketchRef): Promise<string[]>;
}
export interface SketchRef {

View File

@ -1,7 +0,0 @@
export const SurveyNotificationServicePath =
'/services/survey-notification-service';
export const SurveyNotificationService = Symbol('SurveyNotificationService');
export interface SurveyNotificationService {
isFirstInstance(): Promise<boolean>;
}

View File

@ -38,3 +38,26 @@ export function uint8ArrayToString(uint8Array: Uint8Array): string {
export function stringToUint8Array(text: string): Uint8Array {
return Uint8Array.from(text, (char) => char.charCodeAt(0));
}
export function poolWhile(
whileCondition: () => boolean,
intervalMs: number,
timeoutMs: number
): Promise<void> {
return new Promise((resolve, reject) => {
if (!whileCondition) {
resolve();
}
const start = Date.now();
const interval = setInterval(() => {
if (!whileCondition()) {
clearInterval(interval);
resolve();
} else if (Date.now() - start > timeoutMs) {
clearInterval(interval);
reject(new Error('Timed out while polling.'));
}
}, intervalMs);
});
}

View File

@ -38,33 +38,33 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
this.preferencesService.onPreferenceChanged(
debounce((e) => {
if (e.preferenceName === 'window.menuBarVisibility') {
this.setMenuBar();
this.doSetMenuBar();
}
if (this._menu) {
for (const cmd of this._toggledCommands) {
const menuItem = this.findMenuById(this._menu, cmd);
if (this.menu) {
for (const cmd of this.toggledCommands) {
const menuItem = this.findMenuById(this.menu, cmd);
if (menuItem) {
menuItem.checked = this.commandRegistry.isToggled(cmd);
}
}
window.electronArduino.setMenu(this._menu); // calls the IDE2-specific implementation
window.electronArduino.setMenu(this.menu); // calls the IDE2-specific implementation
}
}, 10)
);
this.keybindingRegistry.onKeybindingsChanged(() => {
this.setMenuBar();
this.doSetMenuBar();
});
// #endregion Theia `postConstruct`
this.appStateService.reachedState('ready').then(() => {
this.appReady = true;
if (this.updateWhenReady) {
this.setMenuBar();
this.doSetMenuBar();
}
});
}
override createElectronMenuBar(): MenuDto[] {
this._toggledCommands.clear(); // https://github.com/eclipse-theia/theia/issues/8977
this.toggledCommands.clear(); // https://github.com/eclipse-theia/theia/issues/8977
const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR);
const menu = this.fillMenuTemplate([], menuModel, [], {
rootMenuPath: MAIN_MENU_BAR,
@ -73,11 +73,11 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
menu.unshift(this.createOSXMenu());
}
const escapedMenu = this.escapeAmpersand(menu);
this._menu = escapedMenu;
this.menu = escapedMenu;
return escapedMenu;
}
override async setMenuBar(): Promise<void> {
override async doSetMenuBar(): Promise<void> {
// Avoid updating menu items when the app is not ready.
// Getting the current electron window is not free and synchronous.
// Here, we defer all menu update requests, and fire one when the app is ready.
@ -124,17 +124,17 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
...args
);
if (
this._menu &&
this.menu &&
this.menuCommandExecutor.isVisible(menuPath, commandId, ...args)
) {
const item = this.findMenuById(this._menu, commandId);
const item = this.findMenuById(this.menu, commandId);
if (item) {
item.checked = this.menuCommandExecutor.isToggled(
menuPath,
commandId,
...args
);
window.electronArduino.setMenu(this._menu); // overridden to call the IDE2-specific implementation.
window.electronArduino.setMenu(this.menu); // overridden to call the IDE2-specific implementation.
}
}
}
@ -342,7 +342,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
parentItems.push(menuItem);
if (this.commandRegistry.getToggledHandler(commandId, ...args)) {
this._toggledCommands.add(commandId);
this.toggledCommands.add(commandId);
}
}
return parentItems;

View File

@ -7,6 +7,7 @@ import {
hasStartupTasks,
StartupTasks,
} from '../../../electron-common/startup-task';
import { WindowReloadOptions } from '@theia/core/lib/browser/window/window-service';
@injectable()
export class ElectronWindowService
@ -17,8 +18,12 @@ export class ElectronWindowService
@postConstruct()
protected override init(): void {
// NOOP
// Overridden to avoid calling the zoom level listener in super.
// IDE2 listens on the zoom level changes in `ArduinoFrontendContribution#onStart`
window.electronTheiaCore.onAboutToClose(() => {
this.connectionCloseService.markForClose(this.frontendIdProvider.getId());
});
}
async isFirstWindow(): Promise<boolean> {
@ -38,11 +43,11 @@ export class ElectronWindowService
}
// Overridden to support optional task owner params and make `tsc` happy.
override reload(options?: StartupTasks): void {
override reload(options?: StartupTasks | WindowReloadOptions): void {
if (hasStartupTasks(options)) {
window.electronArduino.requestReload(options);
} else {
window.electronTheiaCore.requestReload();
super.reload(options);
}
}

View File

@ -1,10 +1,10 @@
import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-main/messaging/electron-connection-handler';
import { RpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ElectronMainWindowService } from '@theia/core/lib/electron-common/electron-main-window-service';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler';
import { TheiaMainApi } from '@theia/core/lib/electron-main/electron-api-main';
import {
ElectronMainApplication as TheiaElectronMainApplication,
ElectronMainApplicationContribution,
ElectronMainApplication as TheiaElectronMainApplication,
} from '@theia/core/lib/electron-main/electron-main-application';
import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window';
import { ContainerModule } from '@theia/core/shared/inversify';
@ -36,15 +36,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler<IDEUpdaterClient>(
IDEUpdaterPath,
(client) => {
const server = context.container.get<IDEUpdater>(IDEUpdater);
server.setClient(client);
client.onDidCloseConnection(() => server.disconnectClient(client));
return server;
}
)
new RpcConnectionHandler<IDEUpdaterClient>(IDEUpdaterPath, (client) => {
const server = context.container.get<IDEUpdater>(IDEUpdater);
server.setClient(client);
client.onDidCloseConnection(() => server.disconnectClient(client));
return server;
})
)
.inSingletonScope();

View File

@ -11,16 +11,16 @@ import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { isOSX } from '@theia/core/lib/common/os';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { isObject, MaybePromise, Mutable } from '@theia/core/lib/common/types';
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
import {
ElectronMainExecutionParams,
ElectronMainCommandOptions,
ElectronMainApplication as TheiaElectronMainApplication,
} from '@theia/core/lib/electron-main/electron-main-application';
import type { TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/theia-electron-window';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { URI } from '@theia/core/shared/vscode-uri';
import { log as logToFile, setup as setupFileLog } from 'node-log-rotate';
@ -28,7 +28,9 @@ import { fork } from 'node:child_process';
import { promises as fs, readFileSync, rm, rmSync } from 'node:fs';
import type { AddressInfo } from 'node:net';
import { isAbsolute, join, resolve } from 'node:path';
import type { Argv } from 'yargs';
import { Sketch } from '../../common/protocol';
import { poolWhile } from '../../common/utils';
import {
AppInfo,
appInfoPropertyLiterals,
@ -129,6 +131,11 @@ const APP_STARTED_WITH_CONTENT_TRACE =
typeof process !== 'undefined' &&
process.argv.indexOf('--content-trace') !== -1;
const createYargs: (
argv?: string[],
cwd?: string
) => Argv = require('yargs/yargs');
@injectable()
export class ElectronMainApplication extends TheiaElectronMainApplication {
@inject(IsTempSketch)
@ -171,29 +178,59 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
private readonly scheduledDeletions: Disposable[] = [];
override async start(config: FrontendApplicationConfig): Promise<void> {
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
// See: https://github.com/electron-userland/electron-builder/issues/2468
// Regression in Theia: https://github.com/eclipse-theia/theia/issues/8701
console.log(`${config.applicationName} ${app.getVersion()}`);
app.on('ready', () => app.setName(config.applicationName));
const cwd = process.cwd();
this.attachFileAssociations(cwd);
this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
this._config = await updateFrontendApplicationConfigFromPackageJson(config);
this._appInfo = updateAppInfo(this._appInfo, this._config);
this.hookApplicationEvents();
const [port] = await Promise.all([this.startBackend(), app.whenReady()]);
this.startContentTracing();
this._backendPort.resolve(port);
await Promise.all([
this.attachElectronSecurityToken(port),
this.startContributions(),
]);
return this.launch({
secondInstance: false,
argv: this.processArgv.getProcessArgvWithoutBin(process.argv),
cwd,
});
createYargs(this.argv, process.cwd())
.command(
'$0 [file]',
false,
(cmd) =>
cmd
.option('electronUserData', {
type: 'string',
describe:
'The area where the electron main process puts its data',
})
.positional('file', { type: 'string' }),
async (args) => {
if (args.electronUserData) {
console.info(
`using electron user data area : '${args.electronUserData}'`
);
await fs.mkdir(args.electronUserData, { recursive: true });
app.setPath('userData', args.electronUserData);
}
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
// See: https://github.com/electron-userland/electron-builder/issues/2468
// Regression in Theia: https://github.com/eclipse-theia/theia/issues/8701
console.log(`${config.applicationName} ${app.getVersion()}`);
app.on('ready', () => app.setName(config.applicationName));
const cwd = process.cwd();
this.attachFileAssociations(cwd);
this.useNativeWindowFrame =
this.getTitleBarStyle(config) === 'native';
this._config = await updateFrontendApplicationConfigFromPackageJson(
config
);
this._appInfo = updateAppInfo(this._appInfo, this._config);
this.hookApplicationEvents();
this.showInitialWindow(undefined);
const [port] = await Promise.all([
this.startBackend(),
app.whenReady(),
]);
this.startContentTracing();
this._backendPort.resolve(port);
await Promise.all([
this.attachElectronSecurityToken(port),
this.startContributions(),
]);
this.handleMainCommand({
file: args.file,
cwd,
secondInstance: false,
});
}
)
.parse();
}
private startContentTracing(): void {
@ -256,6 +293,16 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
);
if (sketchFolderPath) {
this.openFilePromise.reject(new InterruptWorkspaceRestoreError());
// open-file event is triggered before the app is ready and initialWindow is created.
// Wait for initialWindow to be set before opening the sketch on the first instance.
// See https://github.com/arduino/arduino-ide/pull/2693
try {
await app.whenReady();
if (!this.firstWindowId) {
await poolWhile(() => !this.initialWindow, 100, 3000);
}
} catch {}
await this.openSketch(sketchFolderPath);
}
}
@ -284,8 +331,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}
}
protected override async launch(
params: ElectronMainExecutionParams
protected override async handleMainCommand(
options: ElectronMainCommandOptions
): Promise<void> {
try {
// When running on MacOS, we either have to wait until
@ -300,7 +347,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
throw err;
}
if (await this.launchFromArgs(params)) {
if (await this.launchFromArgs(options)) {
// Application has received a file in its arguments and will skip the default application launch
return;
}
@ -314,7 +361,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
`Restoring workspace roots: ${workspaces.map(({ file }) => file)}`
);
for (const workspace of workspaces) {
const resolvedPath = await this.resolvePath(workspace.file, params.cwd);
const resolvedPath = await this.resolvePath(
workspace.file,
options.cwd
);
if (!resolvedPath) {
continue;
}
@ -337,15 +387,20 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}
this.startup = false;
if (useDefault) {
super.launch(params);
super.handleMainCommand(options);
}
}
private get argv(): string[] {
return this.processArgv.getProcessArgvWithoutBin(process.argv).slice();
}
private async launchFromArgs(
params: ElectronMainExecutionParams
params: ElectronMainCommandOptions,
argv?: string[]
): Promise<boolean> {
// Copy to prevent manipulation of original array
const argCopy = [...params.argv];
const argCopy = [...(argv || this.argv)];
let path: string | undefined;
for (const maybePath of argCopy) {
const resolvedPath = await this.resolvePath(maybePath, params.cwd);
@ -383,7 +438,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}
const [uri, electronWindow] = await Promise.all([
this.createWindowUri(),
this.createWindow(options),
this.reuseOrCreateWindow(options),
]);
electronWindow.loadURL(uri.withFragment(encodeURI(file)).toString(true));
return electronWindow;
@ -483,7 +538,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
argv: string[],
cwd: string
): Promise<void> {
if (await this.launchFromArgs({ cwd, argv, secondInstance: true })) {
if (await this.launchFromArgs({ cwd, secondInstance: true }, argv)) {
// Application has received a file in its arguments
return;
}
@ -779,7 +834,7 @@ class InterruptWorkspaceRestoreError extends Error {
// but it's the `package.json` inside the `resources/app/` folder if it's the final bundled app.
// See https://github.com/arduino/arduino-ide/pull/2144#pullrequestreview-1556343430.
async function updateFrontendApplicationConfigFromPackageJson(
config: FrontendApplicationConfig
config: Mutable<FrontendApplicationConfig>
): Promise<FrontendApplicationConfig> {
if (!isProductionMode) {
console.debug(
@ -846,7 +901,11 @@ const fallbackFrontendAppConfig: FrontendApplicationConfig = {
defaultIconTheme: 'none',
validatePreferencesSchema: false,
defaultLocale: '',
electron: {},
electron: {
showWindowEarly: true,
uriScheme: 'arduino-ide',
},
reloadOnReconnect: true,
};
// When the package.json must go from `./lib/backend/electron-main.js` to `./package.json` when the app is webpacked.

View File

@ -8,7 +8,7 @@ import { ElectronArduinoRenderer } from '../electron-arduino';
@injectable()
export class TheiaElectronWindow extends DefaultTheiaElectronWindow {
protected override reload(args?: unknown): void {
this.handleStopRequest(() => {
this.handleStopRequest(async () => {
this.applicationState = 'init';
if (hasStartupTasks(args)) {
const { webContents } = this._window;

View File

@ -0,0 +1,53 @@
import { credentials, makeClientConstructor } from '@grpc/grpc-js';
import * as commandsGrpcPb from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
export interface CreateClientOptions {
/**
* The port to the Arduino CLI daemon.
*/
readonly port: number;
/**
* Defaults to `'localhost'`.
*/
readonly host?: string;
/**
* gRCP channel options. Defaults to `createDefaultChannelOptions` with `'0.0.0'` `appVersion`
*/
readonly channelOptions?: Record<string, unknown>;
}
export function createDefaultChannelOptions(
appVersion = '0.0.0'
): Record<string, unknown> {
return {
'grpc.max_send_message_length': 512 * 1024 * 1024,
'grpc.max_receive_message_length': 512 * 1024 * 1024,
'grpc.primary_user_agent': `arduino-ide/${appVersion}`,
};
}
export function createArduinoCoreServiceClient(
options: CreateClientOptions
): ArduinoCoreServiceClient {
const {
port,
host = 'localhost',
channelOptions = createDefaultChannelOptions(),
} = options;
const address = `${host}:${port}`;
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
const ArduinoCoreServiceClient = makeClientConstructor(
// @ts-expect-error: ignore
commandsGrpcPb['cc.arduino.cli.commands.v1.ArduinoCoreService'],
'ArduinoCoreServiceService'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any;
const client = new ArduinoCoreServiceClient(
address,
credentials.createInsecure(),
channelOptions
) as ArduinoCoreServiceClient;
return client;
}

View File

@ -1,7 +1,7 @@
import { join } from 'node:path';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { spawn, ChildProcess } from 'node:child_process';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { Deferred, retry } from '@theia/core/lib/common/promise-util';
import {
@ -39,11 +39,11 @@ export class ArduinoDaemonImpl
private readonly processUtils: ProcessUtils;
private readonly toDispose = new DisposableCollection();
private readonly onDaemonStartedEmitter = new Emitter<string>();
private readonly onDaemonStartedEmitter = new Emitter<number>();
private readonly onDaemonStoppedEmitter = new Emitter<void>();
private _running = false;
private _port = new Deferred<string>();
private _port = new Deferred<number>();
// Backend application lifecycle.
@ -53,18 +53,18 @@ export class ArduinoDaemonImpl
// Daemon API
async getPort(): Promise<string> {
async getPort(): Promise<number> {
return this._port.promise;
}
async tryGetPort(): Promise<string | undefined> {
async tryGetPort(): Promise<number | undefined> {
if (this._running) {
return this._port.promise;
}
return undefined;
}
async start(): Promise<string> {
async start(): Promise<number> {
try {
this.toDispose.dispose(); // This will `kill` the previously started daemon process, if any.
const cliPath = this.getExecPath();
@ -101,13 +101,13 @@ export class ArduinoDaemonImpl
this.toDispose.dispose();
}
async restart(): Promise<string> {
async restart(): Promise<number> {
return this.start();
}
// Backend only daemon API
get onDaemonStarted(): Event<string> {
get onDaemonStarted(): Event<number> {
return this.onDaemonStartedEmitter.event;
}
@ -150,11 +150,11 @@ export class ArduinoDaemonImpl
protected async spawnDaemonProcess(): Promise<{
daemon: ChildProcess;
port: string;
port: number;
}> {
const args = await this.getSpawnArgs();
const cliPath = this.getExecPath();
const ready = new Deferred<{ daemon: ChildProcess; port: string }>();
const ready = new Deferred<{ daemon: ChildProcess; port: number }>();
const options = {
env: { ...deepClone(process.env), NO_COLOR: String(true) },
};
@ -195,7 +195,13 @@ export class ArduinoDaemonImpl
if (port.length && address.length) {
grpcServerIsReady = true;
ready.resolve({ daemon, port });
const portNumber = Number.parseInt(port, 10);
if (Number.isNaN(portNumber)) {
ready.reject(
new Error(`Received a NaN port from the CLI: ${port}`)
);
}
ready.resolve({ daemon, port: portNumber });
}
}
});
@ -225,7 +231,7 @@ export class ArduinoDaemonImpl
return ready.promise;
}
private fireDaemonStarted(port: string): void {
private fireDaemonStarted(port: number): void {
this._running = true;
this._port.resolve(port);
this.onDaemonStartedEmitter.fire(port);
@ -238,7 +244,7 @@ export class ArduinoDaemonImpl
}
this._running = false;
this._port.reject(); // Reject all pending.
this._port = new Deferred<string>();
this._port = new Deferred<number>();
this.onDaemonStoppedEmitter.fire();
this.notificationService.notifyDaemonDidStop();
}

View File

@ -105,23 +105,22 @@ import { ClangFormatter } from './clang-formatter';
import { FormatterPath } from '../common/protocol/formatter';
import { HostedPluginLocalizationService } from './theia/plugin-ext/hosted-plugin-localization-service';
import { HostedPluginLocalizationService as TheiaHostedPluginLocalizationService } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-localization-service';
import { SurveyNotificationServiceImpl } from './survey-service-impl';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { IsTempSketch } from './is-temp-sketch';
import { rebindNsfwFileSystemWatcher } from './theia/filesystem/nsfw-bindings';
import { MessagingContribution } from './theia/core/messaging-contribution';
import { WebsocketEndpoint } from './theia/core/websocket-endpoint';
import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
import { HostedPluginReader } from './theia/plugin-ext/plugin-reader';
import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader';
import { PluginDeployer } from '@theia/plugin-ext/lib/common/plugin-protocol';
import {
PluginDeployer,
PluginScanner,
} from '@theia/plugin-ext/lib/common/plugin-protocol';
import {
LocalDirectoryPluginDeployerResolverWithFallback,
PluginDeployer_GH_12064,
} from './theia/plugin-ext/plugin-deployer';
import { SettingsReader } from './settings-reader';
import { VsCodePluginScanner } from './theia/plugin-ext-vscode/scanner-vscode';
import { rebindParcelFileSystemWatcher } from './theia/filesystem/parcel-bindings';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
@ -301,7 +300,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
rebindNsfwFileSystemWatcher(rebind);
rebindParcelFileSystemWatcher(rebind);
// Output service per connection.
bind(ConnectionContainerModule).toConstantValue(
@ -379,23 +378,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
HostedPluginLocalizationService
);
// Survey notification bindings
// It's currently unused. https://github.com/arduino/arduino-ide/pull/1150
bind(SurveyNotificationServiceImpl).toSelf().inSingletonScope();
bind(SurveyNotificationService).toService(SurveyNotificationServiceImpl);
bind(ConnectionHandler)
.toDynamicValue(
({ container }) =>
new JsonRpcConnectionHandler(SurveyNotificationServicePath, () =>
container.get<SurveyNotificationService>(SurveyNotificationService)
)
)
.inSingletonScope();
bind(IsTempSketch).toSelf().inSingletonScope();
rebind(MessagingService.Identifier)
.to(MessagingContribution)
.inSingletonScope();
rebind(MessagingService.Identifier).to(WebsocketEndpoint).inSingletonScope();
// Removed undesired contributions from VS Code extensions
// Such as the RTOS view from the `cortex-debug` extension
@ -410,6 +394,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
bind(SettingsReader).toSelf().inSingletonScope();
// To read the enablement property of the viewsWelcome
// https://github.com/eclipse-theia/theia/issues/14309
bind(VsCodePluginScanner).toSelf().inSingletonScope();
rebind(PluginScanner).toService(VsCodePluginScanner);
});
function bindChildLogger(bind: interfaces.Bind, name: string): void {

View File

@ -267,24 +267,12 @@ export class BoardDiscovery
const { port, boards } = detectedPort;
const key = Port.keyOf(port);
if (eventType === EventType.Add) {
const alreadyDetectedPort = newState[key];
if (alreadyDetectedPort) {
console.warn(
`Detected a new port that has been already discovered. The old value will be overridden. Old value: ${JSON.stringify(
alreadyDetectedPort
)}, new value: ${JSON.stringify(detectedPort)}`
);
}
// Note that, the serial discovery might detect port details (such as addressLabel) in chunks.
// For example, first, the Teensy 4.1 port is detected with the `[no_device] Triple Serial` address label,
// Then, when more refinements are available, the same port is detected with `/dev/cu.usbmodem127902301 Triple Serial` address label.
// In such cases, an `add` event is received from the CLI, and the the detected port is overridden in the state.
newState[key] = { port, boards };
} else if (eventType === EventType.Remove) {
const alreadyDetectedPort = newState[key];
if (!alreadyDetectedPort) {
console.warn(
`Detected a port removal but it has not been discovered. This is most likely a bug! Detected port was: ${JSON.stringify(
detectedPort
)}`
);
}
delete newState[key];
}
}

View File

@ -1,9 +1,9 @@
import { ILogger } from '@theia/core/lib/common/logger';
import { nls } from '@theia/core/lib/common/nls';
import { notEmpty } from '@theia/core/lib/common/objects';
import { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Board,
BoardDetails,
BoardSearch,
BoardUserField,
@ -32,11 +32,9 @@ import {
BoardListAllResponse,
BoardSearchRequest,
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
import { Platform } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { PlatformSummary } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import {
PlatformInstallRequest,
PlatformListRequest,
PlatformListResponse,
PlatformSearchRequest,
PlatformSearchResponse,
PlatformUninstallRequest,
@ -75,7 +73,11 @@ export class BoardsServiceImpl
async getBoardDetails(options: {
fqbn: string;
forceRefresh?: boolean;
}): Promise<BoardDetails | undefined> {
if (options.forceRefresh) {
await this.refresh();
}
const coreClient = await this.coreClient;
const { client, instance } = coreClient;
const { fqbn } = options;
@ -178,7 +180,7 @@ export class BoardsServiceImpl
const req = new IsDebugSupportedRequest()
.setInstance(instance)
.setFqbn(fqbn)
.setProgrammer(programmer);
.setProgrammer(programmer ?? '');
try {
const debugFqbn = await new Promise<string>((resolve, reject) =>
client.isDebugSupported(req, (err, resp) => {
@ -247,24 +249,22 @@ export class BoardsServiceImpl
async getInstalledPlatforms(): Promise<BoardsPackage[]> {
const { instance, client } = await this.coreClient;
return new Promise<BoardsPackage[]>((resolve, reject) => {
client.platformList(
new PlatformListRequest().setInstance(instance),
(err, response) => {
if (err) {
reject(err);
return;
}
resolve(
response
.getInstalledPlatformsList()
.map((platform, _, installedPlatforms) =>
toBoardsPackage(platform, installedPlatforms)
)
);
}
);
});
const resp = await new Promise<PlatformSearchResponse>(
(resolve, reject) => {
client.platformSearch(
new PlatformSearchRequest()
.setInstance(instance)
.setManuallyInstalled(true), // include core manually installed to the sketchbook
(err, resp) => (err ? reject(err) : resolve(resp))
);
}
);
const searchOutput = resp.getSearchOutputList();
return searchOutput
.map((message) => message.toObject(false))
.filter((summary) => summary.installedVersion) // only installed ones
.map(createBoardsPackage)
.filter(notEmpty);
}
private async handleListBoards(
@ -287,12 +287,28 @@ export class BoardsServiceImpl
for (const board of resp.getBoardsList()) {
const platform = board.getPlatform();
if (platform) {
const platformId = platform.getId();
const metadata = platform.getMetadata();
if (!metadata) {
console.warn(
`Platform metadata is missing for platform: ${JSON.stringify(
platform.toObject(false)
)}. Skipping`
);
continue;
}
const platformId = metadata.getId();
const release = platform.getRelease();
if (!release) {
console.warn(
`Platform release is missing for platform: ${platformId}. Skipping`
);
continue;
}
const fqbn = board.getFqbn() || undefined; // prefer undefined over empty string
const parsedPlatformId = createPlatformIdentifier(platformId);
if (!parsedPlatformId) {
console.warn(
`Could not create platform identifier from platform ID input: ${platform.getId()}. Skipping`
`Could not create platform identifier from platform ID input: ${platformId}. Skipping`
);
continue;
}
@ -319,8 +335,8 @@ export class BoardsServiceImpl
name: board.getName(),
fqbn: board.getFqbn(),
packageId: parsedPlatformId,
packageName: platform.getName(),
manuallyInstalled: platform.getManuallyInstalled(),
packageName: release.getName(),
manuallyInstalled: metadata.getManuallyInstalled(),
});
}
}
@ -375,89 +391,25 @@ export class BoardsServiceImpl
const coreClient = await this.coreClient;
const { client, instance } = coreClient;
const installedPlatformsReq = new PlatformListRequest();
installedPlatformsReq.setInstance(instance);
const installedPlatformsResp = await new Promise<PlatformListResponse>(
(resolve, reject) => {
client.platformList(installedPlatformsReq, (err, resp) => {
!!err ? reject(err) : resolve(resp);
});
}
);
const installedPlatforms =
installedPlatformsResp.getInstalledPlatformsList();
const req = new PlatformSearchRequest();
req.setSearchArgs(options.query || '');
req.setAllVersions(true);
req.setInstance(instance);
// `core search` returns with all platform versions when the command is executed via gRPC or with `--format json`
// The `--all` flag is applicable only when filtering for the human-readable (`--format text`) output of the CLI
const resp = await new Promise<PlatformSearchResponse>(
(resolve, reject) => {
client.platformSearch(req, (err, resp) => {
!!err ? reject(err) : resolve(resp);
});
client.platformSearch(
new PlatformSearchRequest()
.setInstance(instance)
.setSearchArgs(options.query ?? ''),
(err, resp) => (err ? reject(err) : resolve(resp))
);
}
);
const packages = new Map<string, BoardsPackage>();
// We must group the cores by ID, and sort platforms by, first the installed version, then version alphabetical order.
// Otherwise we lose the FQBN information.
const groupedById: Map<string, Platform[]> = new Map();
for (const platform of resp.getSearchOutputList()) {
const id = platform.getId();
const idGroup = groupedById.get(id);
if (idGroup) {
idGroup.push(platform);
} else {
groupedById.set(id, [platform]);
}
}
const installedAwareVersionComparator = (
left: Platform,
right: Platform
) => {
// XXX: we cannot rely on `platform.getInstalled()`, it is always an empty string.
const leftInstalled = !!installedPlatforms.find(
(ip) =>
ip.getId() === left.getId() && ip.getInstalled() === left.getLatest()
);
const rightInstalled = !!installedPlatforms.find(
(ip) =>
ip.getId() === right.getId() &&
ip.getInstalled() === right.getLatest()
);
if (leftInstalled && !rightInstalled) {
return -1;
}
if (!leftInstalled && rightInstalled) {
return 1;
}
const invertedVersionComparator =
Installable.Version.COMPARATOR(left.getLatest(), right.getLatest()) *
-1;
// Higher version comes first.
return invertedVersionComparator;
};
for (const value of groupedById.values()) {
value.sort(installedAwareVersionComparator);
}
for (const value of groupedById.values()) {
for (const platform of value) {
const id = platform.getId();
const pkg = packages.get(id);
if (pkg) {
pkg.availableVersions.push(platform.getLatest());
pkg.availableVersions.sort(Installable.Version.COMPARATOR).reverse();
} else {
packages.set(id, toBoardsPackage(platform, installedPlatforms));
}
}
}
const filter = this.typePredicate(options);
const boardsPackages = [...packages.values()].filter(filter);
const typeFilter = this.typePredicate(options);
const searchOutput = resp.getSearchOutputList();
const boardsPackages = searchOutput
.map((message) => message.toObject(false))
.map(createBoardsPackage)
.filter(notEmpty)
.filter(typeFilter);
return sortComponents(boardsPackages, boardsPackageSortGroup);
}
@ -624,36 +576,52 @@ function boardsPackageSortGroup(boardsPackage: BoardsPackage): SortGroup {
return types.join('-') as SortGroup;
}
function toBoardsPackage(
platform: Platform,
installedPlatforms: Platform[]
): BoardsPackage {
let installedVersion: string | undefined;
const matchingPlatform = installedPlatforms.find(
(ip) => ip.getId() === platform.getId()
);
if (!!matchingPlatform) {
installedVersion = matchingPlatform.getInstalled();
function createBoardsPackage(
summary: PlatformSummary.AsObject
): BoardsPackage | undefined {
if (!isPlatformSummaryWithMetadata(summary)) {
return undefined;
}
return {
id: platform.getId(),
name: platform.getName(),
author: platform.getMaintainer(),
availableVersions: [platform.getLatest()],
description: platform
.getBoardsList()
.map((b) => b.getName())
.join(', '),
types: platform.getTypeList(),
deprecated: platform.getDeprecated(),
const versionReleaseMap = new Map(summary.releasesMap);
const actualRelease =
versionReleaseMap.get(summary.installedVersion) ??
versionReleaseMap.get(summary.latestVersion);
if (!actualRelease) {
return undefined;
}
const { name, typesList, boardsList, deprecated, compatible } = actualRelease;
if (!compatible) {
return undefined; // never show incompatible platforms
}
const { id, website, maintainer } = summary.metadata;
const availableVersions = Array.from(versionReleaseMap.keys())
.sort(Installable.Version.COMPARATOR)
.reverse();
const boardsPackage: Mutable<BoardsPackage> = {
id,
name,
summary: nls.localize(
'arduino/component/boardsIncluded',
'Boards included in this package:'
),
installedVersion,
boards: platform
.getBoardsList()
.map((b) => <Board>{ name: b.getName(), fqbn: b.getFqbn() }),
moreInfoLink: platform.getWebsite(),
description: boardsList.map(({ name }) => name).join(', '),
boards: boardsList,
types: typesList,
moreInfoLink: website,
author: maintainer,
deprecated,
availableVersions,
};
if (summary.installedVersion) {
boardsPackage.installedVersion = summary.installedVersion;
}
return boardsPackage;
}
type PlatformSummaryWithMetadata = PlatformSummary.AsObject &
Required<Pick<PlatformSummary.AsObject, 'metadata'>>;
function isPlatformSummaryWithMetadata(
summary: PlatformSummary.AsObject
): summary is PlatformSummaryWithMetadata {
return Boolean(summary.metadata);
}

View File

@ -1,6 +1,6 @@
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { MaybePromise } from '@theia/core/lib/common/types';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { constants, promises as fs } from 'node:fs';
import { join } from 'node:path';

View File

@ -1,6 +1,6 @@
import { notEmpty } from '@theia/core/lib/common/objects';
import { nls } from '@theia/core/lib/common/nls';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { FileUri } from '@theia/core/lib/common/file-uri';
import {
Range,
Position,

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