Merge branch 'release/v6.1.8'

This commit is contained in:
Ivan Kravets
2023-07-05 15:12:01 +03:00
103 changed files with 969 additions and 699 deletions

View File

@ -6,9 +6,8 @@ What kind of issue is this?
use [Community Forums](https://community.platformio.org) or [Premium Support](https://platformio.org/support) use [Community Forums](https://community.platformio.org) or [Premium Support](https://platformio.org/support)
- [ ] **PlatformIO IDE**. - [ ] **PlatformIO IDE**.
All issues related to PlatformIO IDE should be reported to appropriate repository: All issues related to PlatformIO IDE should be reported to the
[PlatformIO IDE for Atom](https://github.com/platformio/platformio-atom-ide/issues) or [PlatformIO IDE for VSCode](https://github.com/platformio/platformio-vscode-ide/issues) repository
[PlatformIO IDE for VSCode](https://github.com/platformio/platformio-vscode-ide/issues)
- [ ] **Development Platform or Board**. - [ ] **Development Platform or Board**.
All issues (building, uploading, adding new boards, etc.) related to PlatformIO development platforms All issues (building, uploading, adding new boards, etc.) related to PlatformIO development platforms

View File

@ -35,7 +35,7 @@ jobs:
tox -e testcore tox -e testcore
- name: Build Python source tarball - name: Build Python source tarball
run: python setup.py sdist run: python setup.py sdist bdist_wheel
- name: Publish package to PyPI - name: Publish package to PyPI
if: ${{ github.ref == 'refs/heads/master' }} if: ${{ github.ref == 'refs/heads/master' }}

View File

@ -13,11 +13,11 @@ jobs:
folder: "Marlin" folder: "Marlin"
config_dir: "Marlin" config_dir: "Marlin"
env_name: "mega2560" env_name: "mega2560"
# - esphome: - esphome:
# repository: "esphome/esphome" repository: "esphome/esphome"
# folder: "esphome" folder: "esphome"
# config_dir: "esphome" config_dir: "esphome"
# env_name: "esp32-arduino" env_name: "esp32-arduino"
- smartknob: - smartknob:
repository: "scottbez1/smartknob" repository: "scottbez1/smartknob"
folder: "smartknob" folder: "smartknob"

View File

@ -6,12 +6,13 @@ To get started, <a href="https://cla-assistant.io/platformio/platformio-core">si
1. Fork the repository on GitHub 1. Fork the repository on GitHub
2. Clone repository `git clone --recursive https://github.com/YourGithubUsername/platformio-core.git` 2. Clone repository `git clone --recursive https://github.com/YourGithubUsername/platformio-core.git`
3. Run `pip install tox` 3. Run `pip install tox`
4. Go to the root of project where is located `tox.ini` and run `tox -e py37` 4. Go to the root of the PlatformIO Core project where `tox.ini` is located (``cd platformio-core``) and run `tox -e py39`.
You can replace `py39` with your own Python version. For example, `py311` means Python 3.11.
5. Activate current development environment: 5. Activate current development environment:
* Windows: `.tox\py37\Scripts\activate` * Windows: `.tox\py39\Scripts\activate`
* Bash/ZSH: `source .tox/py37/bin/activate` * Bash/ZSH: `source .tox/py39/bin/activate`
* Fish: `source .tox/py37/bin/activate.fish` * Fish: `source .tox/py39/bin/activate.fish`
6. Make changes to code, documentation, etc. 6. Make changes to code, documentation, etc.
7. Lint source code `make before-commit` 7. Lint source code `make before-commit`

View File

@ -15,6 +15,19 @@ PlatformIO Core 6
**A professional collaborative platform for declarative, safety-critical, and test-driven embedded development.** **A professional collaborative platform for declarative, safety-critical, and test-driven embedded development.**
6.1.8 (2023-07-05)
~~~~~~~~~~~~~~~~~~
* Added a new ``--lint`` option to the `pio project config <https://docs.platformio.org/en/latest/core/userguide/project/cmd_config.html>`__ command, enabling users to efficiently perform linting on the |PIOCONF|
* Enhanced the parsing of the |PIOCONF| to provide comprehensive diagnostic information
* Expanded the functionality of the |LIBRARYJSON| manifest by allowing the use of the underscore symbol in the `keywords <https://docs.platformio.org/en/latest/manifests/library-json/fields/keywords.html>`__ field
* Optimized project integration templates to address the issue of long paths on Windows (`issue #4652 <https://github.com/platformio/platformio-core/issues/4652>`_)
* Refactored |UNITTESTING| engine to resolve compiler warnings with "-Wpedantic" option (`pull #4671 <https://github.com/platformio/platformio-core/pull/4671>`_)
* Eliminated erroneous warning regarding the use of obsolete PlatformIO Core when downgrading to the stable version (`issue #4664 <https://github.com/platformio/platformio-core/issues/4664>`_)
* Updated the `pio project metadata <https://docs.platformio.org/en/latest/core/userguide/project/cmd_metadata.html>`__ command to return C/C++ flags as parsed Unix shell arguments when dumping project build metadata
* Resolved a critical issue related to the usage of the ``-include`` flag within the `build_flags <https://docs.platformio.org/en/latest/projectconf/sections/env/options/build/build_flags.html>`__ option, specifically when employing dynamic variables (`issue #4682 <https://github.com/platformio/platformio-core/issues/4682>`_)
* Removed PlatformIO IDE for Atom from the documentation as `Atom has been deprecated <https://github.blog/2022-06-08-sunsetting-atom/>`__
6.1.7 (2023-05-08) 6.1.7 (2023-05-08)
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~

2
docs

Submodule docs updated: 98609771ba...3f462c9ae6

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
VERSION = (6, 1, 7) VERSION = (6, 1, 8)
__version__ = ".".join([str(s) for s in VERSION]) __version__ = ".".join([str(s) for s in VERSION])
__title__ = "platformio" __title__ = "platformio"

View File

@ -14,7 +14,7 @@
import os import os
import sys import sys
from traceback import format_exc import traceback
import click import click
@ -53,13 +53,13 @@ def cli(ctx, force, caller, no_ansi): # pylint: disable=unused-argument
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
pass pass
maintenance.on_platformio_start(ctx, caller) maintenance.on_cmd_start(ctx, caller)
@cli.result_callback() @cli.result_callback()
@click.pass_context @click.pass_context
def process_result(ctx, result, *_, **__): def process_result(*_, **__):
maintenance.on_platformio_end(ctx, result) maintenance.on_cmd_end()
def configure(): def configure():
@ -96,6 +96,7 @@ def main(argv=None):
if argv: if argv:
assert isinstance(argv, list) assert isinstance(argv, list)
sys.argv = argv sys.argv = argv
try: try:
ensure_python3(raise_exception=True) ensure_python3(raise_exception=True)
configure() configure()
@ -106,18 +107,18 @@ def main(argv=None):
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
if not isinstance(exc, exception.ReturnErrorCode): if not isinstance(exc, exception.ReturnErrorCode):
maintenance.on_platformio_exception(exc) maintenance.on_platformio_exception(exc)
error_str = "Error: " error_str = f"{exc.__class__.__name__}: "
if isinstance(exc, exception.PlatformioException): if isinstance(exc, exception.PlatformioException):
error_str += str(exc) error_str += str(exc)
else: else:
error_str += format_exc() error_str += traceback.format_exc()
error_str += """ error_str += """
============================================================ ============================================================
An unexpected error occurred. Further steps: An unexpected error occurred. Further steps:
* Verify that you have the latest version of PlatformIO using * Verify that you have the latest version of PlatformIO using
`pip install -U platformio` command `python -m pip install -U platformio` command
* Try to find answer in FAQ Troubleshooting section * Try to find answer in FAQ Troubleshooting section
https://docs.platformio.org/page/faq/index.html https://docs.platformio.org/page/faq/index.html
@ -129,6 +130,8 @@ An unexpected error occurred. Further steps:
""" """
click.secho(error_str, fg="red", err=True) click.secho(error_str, fg="red", err=True)
exit_code = int(str(exc)) if str(exc).isdigit() else 1 exit_code = int(str(exc)) if str(exc).isdigit() else 1
maintenance.on_platformio_exit()
sys.argv = prev_sys_argv sys.argv = prev_sys_argv
return exit_code return exit_code

View File

@ -16,7 +16,7 @@ import os
import time import time
from platformio import __accounts_api__, app from platformio import __accounts_api__, app
from platformio.exception import PlatformioException from platformio.exception import PlatformioException, UserSideException
from platformio.http import HTTPClient, HTTPClientError from platformio.http import HTTPClient, HTTPClientError
@ -24,11 +24,11 @@ class AccountError(PlatformioException):
MESSAGE = "{0}" MESSAGE = "{0}"
class AccountNotAuthorized(AccountError): class AccountNotAuthorized(AccountError, UserSideException):
MESSAGE = "You are not authorized! Please log in to PlatformIO Account." MESSAGE = "You are not authorized! Please log in to PlatformIO Account."
class AccountAlreadyAuthorized(AccountError): class AccountAlreadyAuthorized(AccountError, UserSideException):
MESSAGE = "You are already authorized with {0} account." MESSAGE = "You are already authorized with {0} account."

View File

@ -18,6 +18,7 @@ import json
import os import os
import platform import platform
import socket import socket
import time
import uuid import uuid
from platformio import __version__, exception, fs, proc from platformio import __version__, exception, fs, proc
@ -68,18 +69,23 @@ SESSION_VARS = {
"command_ctx": None, "command_ctx": None,
"caller_id": None, "caller_id": None,
"custom_project_conf": None, "custom_project_conf": None,
"pause_telemetry": False,
} }
def resolve_state_path(conf_option_dir, file_name, ensure_dir_exists=True):
state_dir = ProjectConfig.get_instance().get("platformio", conf_option_dir)
if ensure_dir_exists and not os.path.isdir(state_dir):
os.makedirs(state_dir)
return os.path.join(state_dir, file_name)
class State: class State:
def __init__(self, path=None, lock=False): def __init__(self, path=None, lock=False):
self.path = path self.path = path
self.lock = lock self.lock = lock
if not self.path: if not self.path:
core_dir = ProjectConfig.get_instance().get("platformio", "core_dir") self.path = resolve_state_path("core_dir", "appstate.json")
if not os.path.isdir(core_dir):
os.makedirs(core_dir)
self.path = os.path.join(core_dir, "appstate.json")
self._storage = {} self._storage = {}
self._lockfile = None self._lockfile = None
self.modified = False self.modified = False
@ -248,6 +254,7 @@ def get_cid():
cid = str(cid) cid = str(cid)
if IS_WINDOWS or os.getuid() > 0: # pylint: disable=no-member if IS_WINDOWS or os.getuid() > 0: # pylint: disable=no-member
set_state_item("cid", cid) set_state_item("cid", cid)
set_state_item("created_at", int(time.time()))
return cid return cid

View File

@ -1,6 +0,0 @@
% for include in filter_includes(includes):
-I{{include}}
% end
% for define in defines:
-D{{!define}}
% end

View File

@ -1,9 +0,0 @@
% _defines = " ".join(["-D%s" % d.replace(" ", "\\\\ ") for d in defines])
{
"execPath": "{{ cxx_path }}",
"gccDefaultCFlags": "-fsyntax-only {{! to_unix_path(cc_flags).replace(' -MMD ', ' ').replace('"', '\\"') }} {{ !_defines.replace('"', '\\"') }}",
"gccDefaultCppFlags": "-fsyntax-only {{! to_unix_path(cxx_flags).replace(' -MMD ', ' ').replace('"', '\\"') }} {{ !_defines.replace('"', '\\"') }}",
"gccErrorLimit": 15,
"gccIncludePaths": "{{ ','.join(filter_includes(includes)) }}",
"gccSuppressWarnings": false
}

View File

@ -1,3 +0,0 @@
.pio
.clang_complete
.gcc-flags.json

View File

@ -1 +0,0 @@
{{cc_flags.replace('-mlongcalls', '-mlong-calls')}}

View File

@ -1 +0,0 @@
{{cxx_flags.replace('-mlongcalls', '-mlong-calls')}}

View File

@ -72,7 +72,8 @@ DEFAULT_ENV_OPTIONS = dict(
variables=clivars, variables=clivars,
# Propagating External Environment # Propagating External Environment
ENV=os.environ, ENV=os.environ,
UNIX_TIME=int(time()), TIMESTAMP=int(time()),
UNIX_TIME="$TIMESTAMP", # deprecated
BUILD_DIR=os.path.join("$PROJECT_BUILD_DIR", "$PIOENV"), BUILD_DIR=os.path.join("$PROJECT_BUILD_DIR", "$PIOENV"),
BUILD_SRC_DIR=os.path.join("$BUILD_DIR", "src"), BUILD_SRC_DIR=os.path.join("$BUILD_DIR", "src"),
BUILD_TEST_DIR=os.path.join("$BUILD_DIR", "test"), BUILD_TEST_DIR=os.path.join("$BUILD_DIR", "test"),

View File

@ -200,13 +200,16 @@ def ParseFlagsExtended(env, flags): # pylint: disable=too-many-branches
# fix relative CPPPATH & LIBPATH # fix relative CPPPATH & LIBPATH
for k in ("CPPPATH", "LIBPATH"): for k in ("CPPPATH", "LIBPATH"):
for i, p in enumerate(result.get(k, [])): for i, p in enumerate(result.get(k, [])):
p = env.subst(p)
if os.path.isdir(p): if os.path.isdir(p):
result[k][i] = os.path.abspath(p) result[k][i] = os.path.abspath(p)
# fix relative path for "-include" # fix relative path for "-include"
for i, f in enumerate(result.get("CCFLAGS", [])): for i, f in enumerate(result.get("CCFLAGS", [])):
if isinstance(f, tuple) and f[0] == "-include": if isinstance(f, tuple) and f[0] == "-include":
result["CCFLAGS"][i] = (f[0], env.File(os.path.abspath(f[1].get_path()))) p = env.subst(f[1].get_path())
if os.path.exists(p):
result["CCFLAGS"][i] = (f[0], os.path.abspath(p))
return result return result

View File

@ -16,6 +16,7 @@
import glob import glob
import os import os
import click
import SCons.Defaults # pylint: disable=import-error import SCons.Defaults # pylint: disable=import-error
import SCons.Subst # pylint: disable=import-error import SCons.Subst # pylint: disable=import-error
from SCons.Script import COMMAND_LINE_TARGETS # pylint: disable=import-error from SCons.Script import COMMAND_LINE_TARGETS # pylint: disable=import-error
@ -154,8 +155,12 @@ def DumpIntegrationData(*args):
], ],
"defines": dump_defines(projenv), "defines": dump_defines(projenv),
"includes": projenv.DumpIntegrationIncludes(), "includes": projenv.DumpIntegrationIncludes(),
"cc_flags": _subst_cmd(projenv, "$CFLAGS $CCFLAGS $CPPFLAGS"), "cc_flags": click.parser.split_arg_string(
"cxx_flags": _subst_cmd(projenv, "$CXXFLAGS $CCFLAGS $CPPFLAGS"), _subst_cmd(projenv, "$CFLAGS $CCFLAGS $CPPFLAGS")
),
"cxx_flags": click.parser.split_arg_string(
_subst_cmd(projenv, "$CXXFLAGS $CCFLAGS $CPPFLAGS")
),
"cc_path": where_is_program( "cc_path": where_is_program(
globalenv.subst("$CC"), globalenv.subst("${ENV['PATH']}") globalenv.subst("$CC"), globalenv.subst("${ENV['PATH']}")
), ),

View File

@ -60,8 +60,8 @@ class CheckToolBase: # pylint: disable=too-many-instance-attributes
data = load_build_metadata(self.project_dir, self.envname) data = load_build_metadata(self.project_dir, self.envname)
if not data: if not data:
return return
self.cc_flags = click.parser.split_arg_string(data.get("cc_flags", "")) self.cc_flags = data.get("cc_flags", [])
self.cxx_flags = click.parser.split_arg_string(data.get("cxx_flags", "")) self.cxx_flags = data.get("cxx_flags", [])
self.cpp_includes = self._dump_includes(data.get("includes", {})) self.cpp_includes = self._dump_includes(data.get("includes", {}))
self.cpp_defines = data.get("defines", []) self.cpp_defines = data.get("defines", [])
self.cc_path = data.get("cc_path") self.cc_path = data.get("cc_path")

View File

@ -63,6 +63,21 @@ class PlatformioCLI(click.MultiCommand):
] ]
) )
@classmethod
def reveal_cmd_path_args(cls, ctx):
result = []
group = ctx.command
args = cls.leftover_args[::]
while args:
cmd_name = args.pop(0)
next_group = group.get_command(ctx, cmd_name)
if next_group:
group = next_group
result.append(cmd_name)
if not hasattr(group, "get_command"):
break
return result
def invoke(self, ctx): def invoke(self, ctx):
PlatformioCLI.leftover_args = ctx.args PlatformioCLI.leftover_args = ctx.args
if hasattr(ctx, "protected_args"): if hasattr(ctx, "protected_args"):

View File

@ -53,16 +53,15 @@ def cli(dev, verbose):
subprocess.run( subprocess.run(
[python_exe, "-m", "pip", "install", "--upgrade", pkg_spec], [python_exe, "-m", "pip", "install", "--upgrade", pkg_spec],
check=True, check=True,
capture_output=not verbose, stdout=subprocess.PIPE if not verbose else None,
) )
r = subprocess.run( output = subprocess.run(
[python_exe, "-m", "platformio", "--version"], [python_exe, "-m", "platformio", "--version"],
check=True, check=True,
capture_output=True, stdout=subprocess.PIPE,
text=True, ).stdout.decode()
) assert "version" in output
assert "version" in r.stdout actual_version = output.split("version", 1)[1].strip()
actual_version = r.stdout.split("version", 1)[1].strip()
click.secho( click.secho(
"PlatformIO has been successfully upgraded to %s" % actual_version, "PlatformIO has been successfully upgraded to %s" % actual_version,
fg="green", fg="green",

View File

@ -17,6 +17,7 @@
import importlib.util import importlib.util
import inspect import inspect
import locale import locale
import shlex
import sys import sys
from platformio.exception import UserSideException from platformio.exception import UserSideException
@ -29,6 +30,20 @@ else:
from asyncio import get_event_loop as aio_get_running_loop from asyncio import get_event_loop as aio_get_running_loop
if sys.version_info >= (3, 8):
from shlex import join as shlex_join
else:
def shlex_join(split_command):
return " ".join(shlex.quote(arg) for arg in split_command)
if sys.version_info >= (3, 9):
from asyncio import to_thread as aio_to_thread
else:
from starlette.concurrency import run_in_threadpool as aio_to_thread
PY2 = sys.version_info[0] == 2 # DO NOT REMOVE IT. ESP8266/ESP32 depend on it PY2 = sys.version_info[0] == 2 # DO NOT REMOVE IT. ESP8266/ESP32 depend on it
IS_CYGWIN = sys.platform.startswith("cygwin") IS_CYGWIN = sys.platform.startswith("cygwin")
IS_WINDOWS = WINDOWS = sys.platform.startswith("win") IS_WINDOWS = WINDOWS = sys.platform.startswith("win")

View File

@ -30,3 +30,7 @@ class DebugSupportError(DebugError, UserSideException):
class DebugInvalidOptionsError(DebugError, UserSideException): class DebugInvalidOptionsError(DebugError, UserSideException):
pass pass
class DebugInitError(DebugError, UserSideException):
pass

View File

@ -13,13 +13,13 @@
# limitations under the License. # limitations under the License.
import os import os
import re
import signal import signal
import time import time
from platformio import telemetry from platformio import telemetry
from platformio.compat import aio_get_running_loop, is_bytes from platformio.compat import aio_get_running_loop, is_bytes
from platformio.debug import helpers from platformio.debug import helpers
from platformio.debug.exception import DebugInitError
from platformio.debug.process.client import DebugClientProcess from platformio.debug.process.client import DebugClientProcess
@ -130,11 +130,7 @@ class GDBClientProcess(DebugClientProcess):
self._handle_error(data) self._handle_error(data)
# go to init break automatically # go to init break automatically
if self.INIT_COMPLETED_BANNER.encode() in data: if self.INIT_COMPLETED_BANNER.encode() in data:
telemetry.send_event( telemetry.log_debug_started(self.debug_config)
"Debug",
"Started",
telemetry.dump_run_environment(self.debug_config.env_options),
)
self._auto_exec_continue() self._auto_exec_continue()
def console_log(self, msg): def console_log(self, msg):
@ -179,14 +175,7 @@ class GDBClientProcess(DebugClientProcess):
and b"Error in sourced" in self._errors_buffer and b"Error in sourced" in self._errors_buffer
): ):
return return
telemetry.log_debug_exception(
last_erros = self._errors_buffer.decode() DebugInitError(self._errors_buffer.decode()), self.debug_config
last_erros = " ".join(reversed(last_erros.split("\n")))
last_erros = re.sub(r'((~|&)"|\\n\"|\\t)', " ", last_erros, flags=re.M)
err = "%s -> %s" % (
telemetry.dump_run_environment(self.debug_config.env_options),
last_erros,
) )
telemetry.send_exception("DebugInitError: %s" % err)
self.transport.close() self.transport.close()

View File

@ -163,7 +163,7 @@ class SerialPortFinder:
for item in list_serial_ports(as_objects=True): for item in list_serial_ports(as_objects=True):
if item.vid == device.vid and item.pid == device.pid: if item.vid == device.vid and item.pid == device.pid:
candidates.append(item) candidates.append(item)
if len(candidates) == 1: if len(candidates) <= 1:
return device.device return device.device
for item in candidates: for item in candidates:
if ("GDB" if self.prefer_gdb_port else "UART") in item.description: if ("GDB" if self.prefer_gdb_port else "UART") in item.description:

View File

@ -81,7 +81,7 @@ class InvalidSettingValue(UserSideException):
MESSAGE = "Invalid value '{0}' for the setting '{1}'" MESSAGE = "Invalid value '{0}' for the setting '{1}'"
class InvalidJSONFile(PlatformioException): class InvalidJSONFile(ValueError, UserSideException):
MESSAGE = "Could not load broken JSON: {0}" MESSAGE = "Could not load broken JSON: {0}"

View File

@ -12,12 +12,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os
from pathlib import Path from pathlib import Path
from platformio import __version__, app, fs, util from platformio import __version__, app, fs, util
from platformio.home.rpc.handlers.base import BaseRPCHandler from platformio.home.rpc.handlers.base import BaseRPCHandler
from platformio.project.config import ProjectConfig
from platformio.project.helpers import is_platformio_project from platformio.project.helpers import is_platformio_project
@ -32,16 +30,11 @@ class AppRPC(BaseRPCHandler):
"projectsDir", "projectsDir",
] ]
@staticmethod
def get_state_path():
core_dir = ProjectConfig.get_instance().get("platformio", "core_dir")
if not os.path.isdir(core_dir):
os.makedirs(core_dir)
return os.path.join(core_dir, "homestate.json")
@staticmethod @staticmethod
def load_state(): def load_state():
with app.State(AppRPC.get_state_path(), lock=True) as state: with app.State(
app.resolve_state_path("core_dir", "homestate.json"), lock=True
) as state:
storage = state.get("storage", {}) storage = state.get("storage", {})
# base data # base data
@ -81,7 +74,9 @@ class AppRPC(BaseRPCHandler):
@staticmethod @staticmethod
def save_state(state): def save_state(state):
with app.State(AppRPC.get_state_path(), lock=True) as s: with app.State(
app.resolve_state_path("core_dir", "homestate.json"), lock=True
) as s:
s.clear() s.clear()
s.update(state) s.update(state)
storage = s.get("storage", {}) storage = s.get("storage", {})

View File

@ -19,10 +19,10 @@ import shutil
from functools import cmp_to_key from functools import cmp_to_key
import click import click
from starlette.concurrency import run_in_threadpool
from platformio import fs from platformio import fs
from platformio.cache import ContentCache from platformio.cache import ContentCache
from platformio.compat import aio_to_thread
from platformio.device.list.util import list_logical_devices from platformio.device.list.util import list_logical_devices
from platformio.home.rpc.handlers.base import BaseRPCHandler from platformio.home.rpc.handlers.base import BaseRPCHandler
from platformio.http import HTTPSession, ensure_internet_on from platformio.http import HTTPSession, ensure_internet_on
@ -33,12 +33,14 @@ class HTTPAsyncSession(HTTPSession):
self, *args, **kwargs self, *args, **kwargs
): ):
func = super().request func = super().request
return await run_in_threadpool(func, *args, **kwargs) return await aio_to_thread(func, *args, **kwargs)
class OSRPC(BaseRPCHandler): class OSRPC(BaseRPCHandler):
@staticmethod _http_session = None
async def fetch_content(url, data=None, headers=None, cache_valid=None):
@classmethod
async def fetch_content(cls, url, data=None, headers=None, cache_valid=None):
if not headers: if not headers:
headers = { headers = {
"User-Agent": ( "User-Agent": (
@ -57,11 +59,13 @@ class OSRPC(BaseRPCHandler):
# check internet before and resolve issue with 60 seconds timeout # check internet before and resolve issue with 60 seconds timeout
ensure_internet_on(raise_exception=True) ensure_internet_on(raise_exception=True)
session = HTTPAsyncSession() if not cls._http_session:
cls._http_session = HTTPAsyncSession()
if data: if data:
r = await session.post(url, data=data, headers=headers) r = await cls._http_session.post(url, data=data, headers=headers)
else: else:
r = await session.get(url, headers=headers) r = await cls._http_session.get(url, headers=headers)
r.raise_for_status() r.raise_for_status()
result = r.text result = r.text
@ -73,9 +77,9 @@ class OSRPC(BaseRPCHandler):
async def request_content(self, uri, data=None, headers=None, cache_valid=None): async def request_content(self, uri, data=None, headers=None, cache_valid=None):
if uri.startswith("http"): if uri.startswith("http"):
return await self.fetch_content(uri, data, headers, cache_valid) return await self.fetch_content(uri, data, headers, cache_valid)
if os.path.isfile(uri): local_path = uri[7:] if uri.startswith("file://") else uri
with io.open(uri, encoding="utf-8") as fp: with io.open(local_path, encoding="utf-8") as fp:
return fp.read() return fp.read()
return None return None
@staticmethod @staticmethod

View File

@ -22,13 +22,13 @@ import threading
import click import click
from ajsonrpc.core import JSONRPC20DispatchException from ajsonrpc.core import JSONRPC20DispatchException
from starlette.concurrency import run_in_threadpool
from platformio import __main__, __version__, app, fs, proc, util from platformio import __main__, __version__, app, fs, proc, util
from platformio.compat import ( from platformio.compat import (
IS_WINDOWS, IS_WINDOWS,
aio_create_task, aio_create_task,
aio_get_running_loop, aio_get_running_loop,
aio_to_thread,
get_locale_encoding, get_locale_encoding,
is_bytes, is_bytes,
) )
@ -177,7 +177,7 @@ class PIOCoreRPC(BaseRPCHandler):
@staticmethod @staticmethod
async def _call_subprocess(args, options): async def _call_subprocess(args, options):
result = await run_in_threadpool( result = await aio_to_thread(
proc.exec_command, proc.exec_command,
[get_core_fullpath()] + args, [get_core_fullpath()] + args,
cwd=options.get("cwd") or os.getcwd(), cwd=options.get("cwd") or os.getcwd(),
@ -197,7 +197,7 @@ class PIOCoreRPC(BaseRPCHandler):
exit_code, exit_code,
) )
return await run_in_threadpool( return await aio_to_thread(
_thread_safe_call, args=args, cwd=options.get("cwd") or os.getcwd() _thread_safe_call, args=args, cwd=options.get("cwd") or os.getcwd()
) )

View File

@ -12,21 +12,55 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os.path
from platformio.compat import aio_to_thread
from platformio.home.rpc.handlers.base import BaseRPCHandler from platformio.home.rpc.handlers.base import BaseRPCHandler
from platformio.package.manager.platform import PlatformPackageManager from platformio.package.manager.platform import PlatformPackageManager
from platformio.package.manifest.parser import ManifestParserFactory
from platformio.package.meta import PackageSpec
from platformio.platform.factory import PlatformFactory from platformio.platform.factory import PlatformFactory
class PlatformRPC(BaseRPCHandler): class PlatformRPC(BaseRPCHandler):
async def fetch_platforms(self, search_query=None, page=0, force_installed=False):
if force_installed:
return {
"items": await aio_to_thread(
self._load_installed_platforms, search_query
)
}
search_result = await self.factory.manager.dispatcher["registry.call_client"](
method="list_packages",
query=search_query,
qualifiers={
"types": ["platform"],
},
page=page,
)
return {
"page": search_result["page"],
"limit": search_result["limit"],
"total": search_result["total"],
"items": [
{
"id": item["id"],
"ownername": item["owner"]["username"],
"name": item["name"],
"version": item["version"]["name"],
"description": item["description"],
"tier": item["tier"],
}
for item in search_result["items"]
],
}
@staticmethod @staticmethod
def list_installed(options=None): def _load_installed_platforms(search_query=None):
result = [] search_query = (search_query or "").strip()
options = options or {}
def _matchSearchQuery(p): def _matchSearchQuery(p):
searchQuery = options.get("searchQuery")
if not searchQuery:
return True
content_blocks = [p.name, p.title, p.description] content_blocks = [p.name, p.title, p.description]
if p.frameworks: if p.frameworks:
content_blocks.append(" ".join(p.frameworks.keys())) content_blocks.append(" ".join(p.frameworks.keys()))
@ -34,28 +68,73 @@ class PlatformRPC(BaseRPCHandler):
board_data = board.get_brief_data() board_data = board.get_brief_data()
for key in ("id", "mcu", "vendor"): for key in ("id", "mcu", "vendor"):
content_blocks.append(board_data.get(key)) content_blocks.append(board_data.get(key))
return searchQuery.strip() in " ".join(content_blocks) return search_query in " ".join(content_blocks)
items = []
pm = PlatformPackageManager() pm = PlatformPackageManager()
for pkg in pm.get_installed(): for pkg in pm.get_installed():
p = PlatformFactory.new(pkg) p = PlatformFactory.new(pkg)
if not _matchSearchQuery(p): if search_query and not _matchSearchQuery(p):
continue continue
result.append( items.append(
dict( {
__pkg_path=pkg.path, "__pkg_path": pkg.path,
__pkg_meta=pkg.metadata.as_dict(), "ownername": pkg.metadata.spec.owner if pkg.metadata.spec else None,
name=p.name, "name": p.name,
title=p.title, "version": str(pkg.metadata.version),
description=p.description, "title": p.title,
) "description": p.description,
}
) )
return result return items
async def fetch_boards(self, platform_spec):
spec = PackageSpec(platform_spec)
if spec.owner:
return await self.factory.manager.dispatcher["registry.call_client"](
method="get_package",
typex="platform",
owner=spec.owner,
name=spec.name,
extra_path="/boards",
)
return await aio_to_thread(self._load_installed_boards, spec)
@staticmethod @staticmethod
def get_boards(spec): def _load_installed_boards(platform_spec):
p = PlatformFactory.new(spec) p = PlatformFactory.new(platform_spec)
return sorted( return sorted(
[b.get_brief_data() for b in p.get_boards().values()], [b.get_brief_data() for b in p.get_boards().values()],
key=lambda item: item["name"], key=lambda item: item["name"],
) )
async def fetch_examples(self, platform_spec):
spec = PackageSpec(platform_spec)
if spec.owner:
return await self.factory.manager.dispatcher["registry.call_client"](
method="get_package",
typex="platform",
owner=spec.owner,
name=spec.name,
extra_path="/examples",
)
return await aio_to_thread(self._load_installed_examples, spec)
@staticmethod
def _load_installed_examples(platform_spec):
platform = PlatformFactory.new(platform_spec)
platform_dir = platform.get_dir()
parser = ManifestParserFactory.new_from_dir(platform_dir)
result = parser.as_dict().get("examples") or []
for example in result:
example["files"] = [
{
"path": item,
"url": (
"file://%s"
+ os.path.join(platform_dir, "examples", example["name"], item)
),
}
for item in example["files"]
]
return result

View File

@ -15,6 +15,7 @@
import os import os
import shutil import shutil
import time import time
from pathlib import Path
import semantic_version import semantic_version
from ajsonrpc.core import JSONRPC20DispatchException from ajsonrpc.core import JSONRPC20DispatchException
@ -267,15 +268,39 @@ class ProjectRPC(BaseRPCHandler):
) )
return new_project_dir return new_project_dir
async def create_empty(self, configuration, options=None): async def init_v2(self, configuration, options=None):
project_dir = os.path.join(configuration["location"], configuration["name"]) project_dir = os.path.join(configuration["location"], configuration["name"])
if not os.path.isdir(project_dir): if not os.path.isdir(project_dir):
os.makedirs(project_dir) os.makedirs(project_dir)
envclone = os.environ.copy()
envclone["PLATFORMIO_FORCE_ANSI"] = "true"
options = options or {}
options["spawn"] = {"env": envclone, "cwd": project_dir}
args = ["project", "init"]
ide = app.get_session_var("caller_id")
if ide in ProjectGenerator.get_supported_ides():
args.extend(["--ide", ide])
if configuration.get("example"):
await self.factory.notify_clients(
method=options.get("stdoutNotificationMethod"),
params=["Copying example files...\n"],
actor="frontend",
)
await self._pre_init_example(configuration, project_dir)
else:
args.extend(self._pre_init_empty(configuration))
return await self.factory.manager.dispatcher["core.exec"](args, options=options)
@staticmethod
def _pre_init_empty(configuration):
project_options = [] project_options = []
platform = configuration["platform"] platform = configuration["platform"]
board = configuration.get("board", {}).get("id") board_id = configuration.get("board", {}).get("id")
env_name = board or platform["name"] env_name = board_id or platform["name"]
if configuration.get("description"): if configuration.get("description"):
project_options.append(("description", configuration.get("description"))) project_options.append(("description", configuration.get("description")))
try: try:
@ -288,20 +313,25 @@ class ProjectRPC(BaseRPCHandler):
project_options.append( project_options.append(
("platform", "{name} @ {version}".format(**platform)) ("platform", "{name} @ {version}".format(**platform))
) )
if board: if board_id:
project_options.append(("board", board)) project_options.append(("board", board_id))
if configuration.get("framework"): if configuration.get("framework"):
project_options.append(("framework", configuration["framework"]["name"])) project_options.append(("framework", configuration["framework"]["name"]))
args = ["project", "init", "-e", env_name, "--sample-code"] args = ["-e", env_name, "--sample-code"]
ide = app.get_session_var("caller_id")
if ide in ProjectGenerator.get_supported_ides():
args.extend(["--ide", ide])
for name, value in project_options: for name, value in project_options:
args.extend(["-O", f"{name}={value}"]) args.extend(["-O", f"{name}={value}"])
return args
envclone = os.environ.copy() async def _pre_init_example(self, configuration, project_dir):
envclone["PLATFORMIO_FORCE_ANSI"] = "true" for item in configuration["example"]["files"]:
options = options or {} p = Path(project_dir).joinpath(item["path"])
options["spawn"] = {"env": envclone, "cwd": project_dir} if not p.parent.is_dir():
return await self.factory.manager.dispatcher["core.exec"](args, options=options) p.parent.mkdir(parents=True)
p.write_text(
await self.factory.manager.dispatcher["os.request_content"](
item["url"]
),
encoding="utf-8",
)
return []

View File

@ -13,8 +13,8 @@
# limitations under the License. # limitations under the License.
from ajsonrpc.core import JSONRPC20DispatchException from ajsonrpc.core import JSONRPC20DispatchException
from starlette.concurrency import run_in_threadpool
from platformio.compat import aio_to_thread
from platformio.home.rpc.handlers.base import BaseRPCHandler from platformio.home.rpc.handlers.base import BaseRPCHandler
from platformio.registry.client import RegistryClient from platformio.registry.client import RegistryClient
@ -24,7 +24,7 @@ class RegistryRPC(BaseRPCHandler):
async def call_client(method, *args, **kwargs): async def call_client(method, *args, **kwargs):
try: try:
client = RegistryClient() client = RegistryClient()
return await run_in_threadpool(getattr(client, method), *args, **kwargs) return await aio_to_thread(getattr(client, method), *args, **kwargs)
except Exception as exc: # pylint: disable=bare-except except Exception as exc: # pylint: disable=bare-except
raise JSONRPC20DispatchException( raise JSONRPC20DispatchException(
code=5000, message="Registry Call Error", data=str(exc) code=5000, message="Registry Call Error", data=str(exc)

View File

@ -14,6 +14,7 @@
from urllib.parse import parse_qs from urllib.parse import parse_qs
import ajsonrpc.utils
import click import click
from ajsonrpc.core import JSONRPC20Error, JSONRPC20Request from ajsonrpc.core import JSONRPC20Error, JSONRPC20Request
from ajsonrpc.dispatcher import Dispatcher from ajsonrpc.dispatcher import Dispatcher
@ -24,6 +25,10 @@ from platformio.compat import aio_create_task, aio_get_running_loop
from platformio.http import InternetConnectionError from platformio.http import InternetConnectionError
from platformio.proc import force_exit from platformio.proc import force_exit
# Remove this line when PR is merged
# https://github.com/pavlov99/ajsonrpc/pull/22
ajsonrpc.utils.is_invalid_params = lambda: False
class JSONRPCServerFactoryBase: class JSONRPCServerFactoryBase:
connection_nums = 0 connection_nums = 0

View File

@ -18,7 +18,7 @@ import socket
from urllib.parse import urljoin from urllib.parse import urljoin
import requests.adapters import requests.adapters
from requests.packages.urllib3.util.retry import Retry # pylint:disable=import-error from urllib3.util.retry import Retry
from platformio import __check_internet_hosts__, app, util from platformio import __check_internet_hosts__, app, util
from platformio.cache import ContentCache, cleanup_content_cache from platformio.cache import ContentCache, cleanup_content_cache
@ -27,7 +27,7 @@ from platformio.exception import PlatformioException, UserSideException
__default_requests_timeout__ = (10, None) # (connect, read) __default_requests_timeout__ = (10, None) # (connect, read)
class HTTPClientError(PlatformioException): class HTTPClientError(UserSideException):
def __init__(self, message, response=None): def __init__(self, message, response=None):
super().__init__() super().__init__()
self.message = message self.message = message
@ -50,7 +50,10 @@ class HTTPSession(requests.Session):
self._x_base_url = kwargs.pop("x_base_url") if "x_base_url" in kwargs else None self._x_base_url = kwargs.pop("x_base_url") if "x_base_url" in kwargs else None
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.headers.update({"User-Agent": app.get_user_agent()}) self.headers.update({"User-Agent": app.get_user_agent()})
self.verify = app.get_setting("enable_proxy_strict_ssl") try:
self.verify = app.get_setting("enable_proxy_strict_ssl")
except PlatformioException:
self.verify = True
def request( # pylint: disable=signature-differs,arguments-differ def request( # pylint: disable=signature-differs,arguments-differ
self, method, url, *args, **kwargs self, method, url, *args, **kwargs
@ -154,7 +157,10 @@ class HTTPClient:
with ContentCache("http") as cc: with ContentCache("http") as cc:
result = cc.get(cache_key) result = cc.get(cache_key)
if result is not None: if result is not None:
return json.loads(result) try:
return json.loads(result)
except json.JSONDecodeError:
pass
response = self.send_request(method, path, **kwargs) response = self.send_request(method, path, **kwargs)
data = self._parse_json_response(response) data = self._parse_json_response(response)
cc.set(cache_key, response.text, cache_valid) cc.set(cache_key, response.text, cache_valid)

View File

@ -25,23 +25,20 @@ from platformio.cli import PlatformioCLI
from platformio.commands.upgrade import get_latest_version from platformio.commands.upgrade import get_latest_version
from platformio.http import HTTPClientError, InternetConnectionError, ensure_internet_on from platformio.http import HTTPClientError, InternetConnectionError, ensure_internet_on
from platformio.package.manager.core import update_core_packages from platformio.package.manager.core import update_core_packages
from platformio.package.manager.tool import ToolPackageManager
from platformio.package.meta import PackageSpec
from platformio.package.version import pepver_to_semver from platformio.package.version import pepver_to_semver
from platformio.system.prune import calculate_unnecessary_system_data from platformio.system.prune import calculate_unnecessary_system_data
def on_platformio_start(ctx, caller): def on_cmd_start(ctx, caller):
app.set_session_var("command_ctx", ctx) app.set_session_var("command_ctx", ctx)
set_caller(caller) set_caller(caller)
telemetry.on_command() telemetry.on_cmd_start(ctx)
if PlatformioCLI.in_silence(): if PlatformioCLI.in_silence():
return return
after_upgrade(ctx) after_upgrade(ctx)
def on_platformio_end(ctx, result): # pylint: disable=unused-argument def on_cmd_end():
if PlatformioCLI.in_silence(): if PlatformioCLI.in_silence():
return return
@ -60,8 +57,12 @@ def on_platformio_end(ctx, result): # pylint: disable=unused-argument
) )
def on_platformio_exception(e): def on_platformio_exception(exc):
telemetry.on_exception(e) telemetry.log_exception(exc)
def on_platformio_exit():
telemetry.on_exit()
def set_caller(caller=None): def set_caller(caller=None):
@ -79,11 +80,10 @@ def set_caller(caller=None):
class Upgrader: class Upgrader:
def __init__(self, from_version, to_version): def __init__(self, from_version, to_version):
self.from_version = pepver_to_semver(from_version) self.from_version = from_version
self.to_version = pepver_to_semver(to_version) self.to_version = to_version
self._upgraders = [ self._upgraders = [
(semantic_version.Version("4.4.0-a.8"), self._update_pkg_metadata), (semantic_version.Version("6.1.8-a.1"), self._appstate_migration),
] ]
def run(self, ctx): def run(self, ctx):
@ -99,37 +99,43 @@ class Upgrader:
return all(result) return all(result)
@staticmethod @staticmethod
def _update_pkg_metadata(_): def _appstate_migration(_):
pm = ToolPackageManager() state_path = app.resolve_state_path("core_dir", "appstate.json")
for pkg in pm.get_installed(): if not os.path.isfile(state_path):
if not pkg.metadata or pkg.metadata.spec.external or pkg.metadata.spec.id: return True
continue app.delete_state_item("telemetry")
result = pm.search_registry_packages(PackageSpec(name=pkg.metadata.name)) created_at = app.get_state_item("created_at", None)
if len(result) != 1: if not created_at:
continue state_stat = os.stat(state_path)
result = result[0] app.set_state_item(
pkg.metadata.spec = PackageSpec( "created_at",
id=result["id"], int(
owner=result["owner"]["username"], state_stat.st_birthtime
name=result["name"], if hasattr(state_stat, "st_birthtime")
else state_stat.st_ctime
),
) )
pkg.dump_meta()
return True return True
def after_upgrade(ctx): def after_upgrade(ctx):
terminal_width = shutil.get_terminal_size().columns terminal_width = shutil.get_terminal_size().columns
last_version = app.get_state_item("last_version", "0.0.0") last_version_str = app.get_state_item("last_version", "0.0.0")
if last_version == __version__: if last_version_str == __version__:
return return None
if last_version == "0.0.0": if last_version_str == "0.0.0":
app.set_state_item("last_version", __version__) app.set_state_item("last_version", __version__)
elif pepver_to_semver(last_version) > pepver_to_semver(__version__): return print_welcome_banner()
last_version = pepver_to_semver(last_version_str)
current_version = pepver_to_semver(__version__)
if last_version > current_version and not last_version.prerelease:
click.secho("*" * terminal_width, fg="yellow") click.secho("*" * terminal_width, fg="yellow")
click.secho( click.secho(
"Obsolete PIO Core v%s is used (previous was %s)" "Obsolete PIO Core v%s is used (previous was %s)"
% (__version__, last_version), % (__version__, last_version_str),
fg="yellow", fg="yellow",
) )
click.secho("Please remove multiple PIO Cores from a system:", fg="yellow") click.secho("Please remove multiple PIO Cores from a system:", fg="yellow")
@ -139,43 +145,50 @@ def after_upgrade(ctx):
fg="cyan", fg="cyan",
) )
click.secho("*" * terminal_width, fg="yellow") click.secho("*" * terminal_width, fg="yellow")
return return None
else:
click.secho("Please wait while upgrading PlatformIO...", fg="yellow")
# Update PlatformIO's Core packages click.secho("Please wait while upgrading PlatformIO...", fg="yellow")
cleanup_content_cache("http")
update_core_packages()
u = Upgrader(last_version, __version__) # Update PlatformIO's Core packages
if u.run(ctx): cleanup_content_cache("http")
app.set_state_item("last_version", __version__) update_core_packages()
click.secho(
"PlatformIO has been successfully upgraded to %s!\n" % __version__,
fg="green",
)
telemetry.send_event(
category="Auto",
action="Upgrade",
label="%s > %s" % (last_version, __version__),
)
# PlatformIO banner u = Upgrader(last_version, current_version)
if u.run(ctx):
app.set_state_item("last_version", __version__)
click.secho(
"PlatformIO has been successfully upgraded to %s!\n" % __version__,
fg="green",
)
telemetry.log_event(
"pio_upgrade_core",
{
"label": "%s > %s" % (last_version_str, __version__),
"from_version": last_version_str,
"to_version": __version__,
},
)
return print_welcome_banner()
def print_welcome_banner():
terminal_width = shutil.get_terminal_size().columns
click.echo("*" * terminal_width) click.echo("*" * terminal_width)
click.echo("If you like %s, please:" % (click.style("PlatformIO", fg="cyan"))) click.echo("If you like %s, please:" % (click.style("PlatformIO", fg="cyan")))
click.echo(
"- %s us on Twitter to stay up-to-date "
"on the latest project news > %s"
% (
click.style("follow", fg="cyan"),
click.style("https://twitter.com/PlatformIO_Org", fg="cyan"),
)
)
click.echo( click.echo(
"- %s it on GitHub > %s" "- %s it on GitHub > %s"
% ( % (
click.style("star", fg="cyan"), click.style("star", fg="cyan"),
click.style("https://github.com/platformio/platformio", fg="cyan"), click.style("https://github.com/platformio/platformio-core", fg="cyan"),
)
)
click.echo(
"- %s us on LinkedIn to stay up-to-date "
"on the latest project news > %s"
% (
click.style("follow", fg="cyan"),
click.style("https://www.linkedin.com/company/platformio/", fg="cyan"),
) )
) )
if not os.getenv("PLATFORMIO_IDE"): if not os.getenv("PLATFORMIO_IDE"):
@ -228,7 +241,7 @@ def check_platformio_upgrade():
else: else:
click.secho("platformio upgrade", fg="cyan", nl=False) click.secho("platformio upgrade", fg="cyan", nl=False)
click.secho("` or `", fg="yellow", nl=False) click.secho("` or `", fg="yellow", nl=False)
click.secho("pip install -U platformio", fg="cyan", nl=False) click.secho("python -m pip install -U platformio", fg="cyan", nl=False)
click.secho("` command.", fg="yellow") click.secho("` command.", fg="yellow")
click.secho("Changes: ", fg="yellow", nl=False) click.secho("Changes: ", fg="yellow", nl=False)
click.secho("https://docs.platformio.org/en/latest/history.html", fg="cyan") click.secho("https://docs.platformio.org/en/latest/history.html", fg="cyan")

View File

@ -59,7 +59,8 @@ def humanize_package(pkg, spec=None, verbose=False):
if spec and not isinstance(spec, PackageSpec): if spec and not isinstance(spec, PackageSpec):
spec = PackageSpec(spec) spec = PackageSpec(spec)
data = [ data = [
click.style("{name} @ {version}".format(**pkg.metadata.as_dict()), fg="cyan") click.style(pkg.metadata.name, fg="cyan"),
click.style(f"@ {str(pkg.metadata.version)}", bold=True),
] ]
extra_data = ["required: %s" % (spec.humanize() if spec else "Any")] extra_data = ["required: %s" % (spec.humanize() if spec else "Any")]
if verbose: if verbose:
@ -135,20 +136,20 @@ def list_global_packages(options):
("libraries", LibraryPackageManager(options.get("storage_dir"))), ("libraries", LibraryPackageManager(options.get("storage_dir"))),
] ]
only_packages = any( only_packages = any(
options.get(type_) or options.get(f"only_{type_}") for (type_, _) in data options.get(typex) or options.get(f"only_{typex}") for (typex, _) in data
) )
for type_, pm in data: for typex, pm in data:
skip_conds = [ skip_conds = [
only_packages only_packages
and not options.get(type_) and not options.get(typex)
and not options.get(f"only_{type_}"), and not options.get(f"only_{typex}"),
not pm.get_installed(), not pm.get_installed(),
] ]
if any(skip_conds): if any(skip_conds):
continue continue
click.secho(type_.capitalize(), bold=True) click.secho(typex.capitalize(), bold=True)
print_dependency_tree( print_dependency_tree(
pm, filter_specs=options.get(type_), verbose=options.get("verbose") pm, filter_specs=options.get(typex), verbose=options.get("verbose")
) )
click.echo() click.echo()
@ -156,12 +157,12 @@ def list_global_packages(options):
def list_project_packages(options): def list_project_packages(options):
environments = options["environments"] environments = options["environments"]
only_packages = any( only_packages = any(
options.get(type_) or options.get(f"only_{type_}") options.get(typex) or options.get(f"only_{typex}")
for type_ in ("platforms", "tools", "libraries") for typex in ("platforms", "tools", "libraries")
) )
only_platform_packages = any( only_platform_packages = any(
options.get(type_) or options.get(f"only_{type_}") options.get(typex) or options.get(f"only_{typex}")
for type_ in ("platforms", "tools") for typex in ("platforms", "tools")
) )
only_library_packages = options.get("libraries") or options.get("only_libraries") only_library_packages = options.get("libraries") or options.get("only_libraries")

View File

@ -56,7 +56,7 @@ def validate_datetime(ctx, param, value): # pylint: disable=unused-argument
) )
@click.option( @click.option(
"--type", "--type",
"type_", "typex",
type=click.Choice(list(PackageType.items().values())), type=click.Choice(list(PackageType.items().values())),
help="Custom package type", help="Custom package type",
) )
@ -83,7 +83,7 @@ def validate_datetime(ctx, param, value): # pylint: disable=unused-argument
hidden=True, hidden=True,
) )
def package_publish_cmd( # pylint: disable=too-many-arguments, too-many-locals def package_publish_cmd( # pylint: disable=too-many-arguments, too-many-locals
package, owner, type_, released_at, private, notify, no_interactive, non_interactive package, owner, typex, released_at, private, notify, no_interactive, non_interactive
): ):
click.secho("Preparing a package...", fg="cyan") click.secho("Preparing a package...", fg="cyan")
no_interactive = no_interactive or non_interactive no_interactive = no_interactive or non_interactive
@ -103,14 +103,14 @@ def package_publish_cmd( # pylint: disable=too-many-arguments, too-many-locals
p = PackagePacker(package) p = PackagePacker(package)
archive_path = p.pack() archive_path = p.pack()
type_ = type_ or PackageType.from_archive(archive_path) typex = typex or PackageType.from_archive(archive_path)
manifest = ManifestSchema().load_manifest( manifest = ManifestSchema().load_manifest(
ManifestParserFactory.new_from_archive(archive_path).as_dict() ManifestParserFactory.new_from_archive(archive_path).as_dict()
) )
name = manifest.get("name") name = manifest.get("name")
version = manifest.get("version") version = manifest.get("version")
data = [ data = [
("Type:", type_), ("Type:", typex),
("Owner:", owner), ("Owner:", owner),
("Name:", name), ("Name:", name),
("Version:", version), ("Version:", version),
@ -124,13 +124,13 @@ def package_publish_cmd( # pylint: disable=too-many-arguments, too-many-locals
check_archive_file_names(archive_path) check_archive_file_names(archive_path)
# look for duplicates # look for duplicates
check_package_duplicates(owner, type_, name, version, manifest.get("system")) check_package_duplicates(owner, typex, name, version, manifest.get("system"))
if not no_interactive: if not no_interactive:
click.confirm( click.confirm(
"Are you sure you want to publish the %s %s to the registry?\n" "Are you sure you want to publish the %s %s to the registry?\n"
% ( % (
type_, typex,
click.style( click.style(
"%s/%s@%s" % (owner, name, version), "%s/%s@%s" % (owner, name, version),
fg="cyan", fg="cyan",
@ -146,7 +146,7 @@ def package_publish_cmd( # pylint: disable=too-many-arguments, too-many-locals
) )
click.echo("Publishing...") click.echo("Publishing...")
response = RegistryClient().publish_package( response = RegistryClient().publish_package(
owner, type_, archive_path, released_at, private, notify owner, typex, archive_path, released_at, private, notify
) )
if not do_not_pack: if not do_not_pack:
os.remove(archive_path) os.remove(archive_path)

View File

@ -13,10 +13,10 @@
# limitations under the License. # limitations under the License.
from platformio import util from platformio import util
from platformio.exception import PlatformioException, UserSideException from platformio.exception import UserSideException
class PackageException(PlatformioException): class PackageException(UserSideException):
pass pass
@ -51,14 +51,14 @@ class MissingPackageManifestError(ManifestException):
MESSAGE = "Could not find one of '{0}' manifest files in the package" MESSAGE = "Could not find one of '{0}' manifest files in the package"
class UnknownPackageError(UserSideException): class UnknownPackageError(PackageException):
MESSAGE = ( MESSAGE = (
"Could not find the package with '{0}' requirements for your system '%s'" "Could not find the package with '{0}' requirements for your system '%s'"
% util.get_systype() % util.get_systype()
) )
class NotGlobalLibDir(UserSideException): class NotGlobalLibDir(PackageException):
MESSAGE = ( MESSAGE = (
"The `{0}` is not a PlatformIO project.\n\n" "The `{0}` is not a PlatformIO project.\n\n"
"To manage libraries in global storage `{1}`,\n" "To manage libraries in global storage `{1}`,\n"

View File

@ -15,7 +15,7 @@
import os import os
from time import sleep, time from time import sleep, time
from platformio.exception import PlatformioException from platformio.exception import UserSideException
LOCKFILE_TIMEOUT = 3600 # in seconds, 1 hour LOCKFILE_TIMEOUT = 3600 # in seconds, 1 hour
LOCKFILE_DELAY = 0.2 LOCKFILE_DELAY = 0.2
@ -36,11 +36,11 @@ except ImportError:
LOCKFILE_CURRENT_INTERFACE = None LOCKFILE_CURRENT_INTERFACE = None
class LockFileExists(PlatformioException): class LockFileExists(UserSideException):
pass pass
class LockFileTimeoutError(PlatformioException): class LockFileTimeoutError(UserSideException):
pass pass

View File

@ -25,12 +25,13 @@ from platformio.registry.mirror import RegistryFileMirrorIterator
class PackageManagerRegistryMixin: class PackageManagerRegistryMixin:
def install_from_registry(self, spec, search_qualifiers=None): def install_from_registry(self, spec, search_qualifiers=None):
package = version = None
if spec.owner and spec.name and not search_qualifiers: if spec.owner and spec.name and not search_qualifiers:
package = self.fetch_registry_package(spec) package = self.fetch_registry_package(spec)
if not package: if not package:
raise UnknownPackageError(spec.humanize()) raise UnknownPackageError(spec.humanize())
version = self.pick_best_registry_version(package["versions"], spec) version = self.pick_best_registry_version(package["versions"], spec)
else: elif spec.id or spec.name:
packages = self.search_registry_packages(spec, search_qualifiers) packages = self.search_registry_packages(spec, search_qualifiers)
if not packages: if not packages:
raise UnknownPackageError(spec.humanize()) raise UnknownPackageError(spec.humanize())

View File

@ -183,7 +183,7 @@ class ManifestSchema(BaseSchema):
validate=[ validate=[
validate.Length(min=1, max=50), validate.Length(min=1, max=50),
validate.Regexp( validate.Regexp(
r"^[a-z\d\-\+\. ]+$", error="Only [a-z0-9-+. ] chars are allowed" r"^[a-z\d\-_\+\. ]+$", error="Only [a-z0-9+_-. ] chars are allowed"
), ),
] ]
) )
@ -276,9 +276,9 @@ class ManifestSchema(BaseSchema):
@staticmethod @staticmethod
@memoized(expire="1h") @memoized(expire="1h")
def load_spdx_licenses(): def load_spdx_licenses():
version = "3.20" version = "3.21"
spdx_data_url = ( spdx_data_url = (
"https://raw.githubusercontent.com/spdx/license-list-data/" "https://raw.githubusercontent.com/spdx/license-list-data/"
"v%s/json/licenses.json" % version f"v{version}/json/licenses.json"
) )
return json.loads(fetch_remote_content(spdx_data_url)) return json.loads(fetch_remote_content(spdx_data_url))

View File

@ -24,7 +24,7 @@ import semantic_version
from platformio import fs from platformio import fs
from platformio.compat import get_object_members, hashlib_encode_data, string_types from platformio.compat import get_object_members, hashlib_encode_data, string_types
from platformio.package.manifest.parser import ManifestFileType from platformio.package.manifest.parser import ManifestFileType
from platformio.package.version import cast_version_to_semver from platformio.package.version import SemanticVersionError, cast_version_to_semver
from platformio.util import items_in_list from platformio.util import items_in_list
@ -175,7 +175,7 @@ class PackageSpec: # pylint: disable=too-many-instance-attributes
if requirements: if requirements:
try: try:
self.requirements = requirements self.requirements = requirements
except ValueError as exc: except SemanticVersionError as exc:
if not self.name or self.uri or self.raw: if not self.name or self.uri or self.raw:
raise exc raise exc
self.raw = "%s=%s" % (self.name, requirements) self.raw = "%s=%s" % (self.name, requirements)
@ -224,11 +224,14 @@ class PackageSpec: # pylint: disable=too-many-instance-attributes
if not value: if not value:
self._requirements = None self._requirements = None
return return
self._requirements = ( try:
value self._requirements = (
if isinstance(value, semantic_version.SimpleSpec) value
else semantic_version.SimpleSpec(str(value)) if isinstance(value, semantic_version.SimpleSpec)
) else semantic_version.SimpleSpec(str(value))
)
except ValueError as exc:
raise SemanticVersionError(exc) from exc
def humanize(self): def humanize(self):
result = "" result = ""

View File

@ -18,14 +18,10 @@ import subprocess
from urllib.parse import urlparse from urllib.parse import urlparse
from platformio import proc from platformio import proc
from platformio.package.exception import ( from platformio.exception import UserSideException
PackageException,
PlatformioException,
UserSideException,
)
class VCSBaseException(PackageException): class VCSBaseException(UserSideException):
pass pass
@ -74,8 +70,8 @@ class VCSClientBase:
self.get_cmd_output(["--version"]) self.get_cmd_output(["--version"])
else: else:
assert self.run_cmd(["--version"]) assert self.run_cmd(["--version"])
except (AssertionError, OSError, PlatformioException) as exc: except (AssertionError, OSError) as exc:
raise UserSideException( raise VCSBaseException(
"VCS: `%s` client is not installed in your system" % self.command "VCS: `%s` client is not installed in your system" % self.command
) from exc ) from exc
return True return True

View File

@ -16,6 +16,12 @@ import re
import semantic_version import semantic_version
from platformio.exception import UserSideException
class SemanticVersionError(UserSideException):
pass
def cast_version_to_semver(value, force=True, raise_exception=False): def cast_version_to_semver(value, force=True, raise_exception=False):
assert value assert value
@ -29,7 +35,7 @@ def cast_version_to_semver(value, force=True, raise_exception=False):
except ValueError: except ValueError:
pass pass
if raise_exception: if raise_exception:
raise ValueError("Invalid SemVer version %s" % value) raise SemanticVersionError("Invalid SemVer version %s" % value)
# parse commit hash # parse commit hash
if re.match(r"^[\da-f]+$", value, flags=re.I): if re.match(r"^[\da-f]+$", value, flags=re.I):
return semantic_version.Version("0.0.0+sha." + value) return semantic_version.Version("0.0.0+sha." + value)

View File

@ -52,7 +52,6 @@ class PlatformRunMixin:
self.ensure_engine_compatible() self.ensure_engine_compatible()
self.configure_project_packages(variables["pioenv"], targets) self.configure_project_packages(variables["pioenv"], targets)
self._report_non_sensitive_data(variables["pioenv"], targets)
self.silent = silent self.silent = silent
self.verbose = verbose or app.get_setting("force_verbose") self.verbose = verbose or app.get_setting("force_verbose")
@ -64,20 +63,13 @@ class PlatformRunMixin:
if not os.path.isfile(variables["build_script"]): if not os.path.isfile(variables["build_script"]):
raise BuildScriptNotFound(variables["build_script"]) raise BuildScriptNotFound(variables["build_script"])
telemetry.log_platform_run(self, self.config, variables["pioenv"], targets)
result = self._run_scons(variables, targets, jobs) result = self._run_scons(variables, targets, jobs)
assert "returncode" in result assert "returncode" in result
return result return result
def _report_non_sensitive_data(self, env, targets):
options = self.config.items(env=env, as_dict=True)
options["platform_packages"] = [
dict(name=item["name"], version=item["version"])
for item in self.dump_used_packages()
]
options["platform"] = {"name": self.name, "version": self.version}
telemetry.send_run_environment(options, targets)
def _run_scons(self, variables, targets, jobs): def _run_scons(self, variables, targets, jobs):
scons_dir = get_core_package_dir("tool-scons") scons_dir = get_core_package_dir("tool-scons")
args = [ args = [

View File

@ -14,10 +14,10 @@
import os import os
from platformio import fs, telemetry, util from platformio import fs, util
from platformio.compat import MISSING from platformio.compat import MISSING
from platformio.debug.exception import DebugInvalidOptionsError, DebugSupportError from platformio.debug.exception import DebugInvalidOptionsError, DebugSupportError
from platformio.exception import UserSideException from platformio.exception import InvalidJSONFile, UserSideException
from platformio.platform.exception import InvalidBoardManifest from platformio.platform.exception import InvalidBoardManifest
@ -28,7 +28,7 @@ class PlatformBoardConfig:
self.manifest_path = manifest_path self.manifest_path = manifest_path
try: try:
self._manifest = fs.load_json(manifest_path) self._manifest = fs.load_json(manifest_path)
except ValueError as exc: except InvalidJSONFile as exc:
raise InvalidBoardManifest(manifest_path) from exc raise InvalidBoardManifest(manifest_path) from exc
if not set(["name", "url", "vendor"]) <= set(self._manifest): if not set(["name", "url", "vendor"]) <= set(self._manifest):
raise UserSideException( raise UserSideException(
@ -119,7 +119,6 @@ class PlatformBoardConfig:
if tool_name == "custom": if tool_name == "custom":
return tool_name return tool_name
if not debug_tools: if not debug_tools:
telemetry.send_event("Debug", "Request", self.id)
raise DebugSupportError(self._manifest["name"]) raise DebugSupportError(self._manifest["name"])
if tool_name: if tool_name:
if tool_name in debug_tools: if tool_name in debug_tools:

View File

@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from platformio.exception import PlatformioException from platformio.exception import UserSideException
class PlatformException(PlatformioException): class PlatformException(UserSideException):
pass pass

View File

@ -30,16 +30,23 @@ from platformio.project.helpers import is_platformio_project
default=os.getcwd, default=os.getcwd,
type=click.Path(exists=True, file_okay=False, dir_okay=True), type=click.Path(exists=True, file_okay=False, dir_okay=True),
) )
@click.option("--lint", is_flag=True)
@click.option("--json-output", is_flag=True) @click.option("--json-output", is_flag=True)
def project_config_cmd(project_dir, json_output): def project_config_cmd(project_dir, lint, json_output):
if not is_platformio_project(project_dir): if not is_platformio_project(project_dir):
raise NotPlatformIOProjectError(project_dir) raise NotPlatformIOProjectError(project_dir)
with fs.cd(project_dir): with fs.cd(project_dir):
config = ProjectConfig.get_instance() if lint:
return lint_configuration(json_output)
return print_configuration(json_output)
def print_configuration(json_output=False):
config = ProjectConfig.get_instance()
if json_output: if json_output:
return click.echo(config.to_json()) return click.echo(config.to_json())
click.echo( click.echo(
"Computed project configuration for %s" % click.style(project_dir, fg="cyan") "Computed project configuration for %s" % click.style(os.getcwd(), fg="cyan")
) )
for section, options in config.as_tuple(): for section, options in config.as_tuple():
click.secho(section, fg="cyan") click.secho(section, fg="cyan")
@ -55,3 +62,43 @@ def project_config_cmd(project_dir, json_output):
) )
click.echo() click.echo()
return None return None
def lint_configuration(json_output=False):
result = ProjectConfig.lint()
errors = result["errors"]
warnings = result["warnings"]
if json_output:
return click.echo(result)
if not errors and not warnings:
return click.secho(
'The "platformio.ini" configuration file is free from linting errors.',
fg="green",
)
if errors:
click.echo(
tabulate(
[
(
click.style(error["type"], fg="red"),
error["message"],
error.get("source", "") + (f":{error.get('lineno')}")
if "lineno" in error
else "",
)
for error in errors
],
tablefmt="plain",
)
)
if warnings:
click.echo(
tabulate(
[
(click.style("Warning", fg="yellow"), warning)
for warning in warnings
],
tablefmt="plain",
)
)
return None

View File

@ -37,6 +37,7 @@ from platformio.project.helpers import load_build_metadata
@click.option("--json-output", is_flag=True) @click.option("--json-output", is_flag=True)
@click.option("--json-output-path", type=click.Path()) @click.option("--json-output-path", type=click.Path())
def project_metadata_cmd(project_dir, environments, json_output, json_output_path): def project_metadata_cmd(project_dir, environments, json_output, json_output_path):
project_dir = os.path.abspath(project_dir)
with fs.cd(project_dir): with fs.cd(project_dir):
config = ProjectConfig.get_instance() config = ProjectConfig.get_instance()
config.validate(environments) config.validate(environments)

View File

@ -86,7 +86,7 @@ class ProjectConfigBase:
if path and os.path.isfile(path): if path and os.path.isfile(path):
self.read(path, parse_extra) self.read(path, parse_extra)
self._maintain_renaimed_options() self._maintain_renamed_options()
def __getattr__(self, name): def __getattr__(self, name):
return getattr(self._parser, name) return getattr(self._parser, name)
@ -98,7 +98,7 @@ class ProjectConfigBase:
try: try:
self._parser.read(path, "utf-8") self._parser.read(path, "utf-8")
except configparser.Error as exc: except configparser.Error as exc:
raise exception.InvalidProjectConfError(path, str(exc)) raise exception.InvalidProjectConfError(path, str(exc)) from exc
if not parse_extra: if not parse_extra:
return return
@ -110,7 +110,7 @@ class ProjectConfigBase:
for item in glob.glob(pattern, recursive=True): for item in glob.glob(pattern, recursive=True):
self.read(item) self.read(item)
def _maintain_renaimed_options(self): def _maintain_renamed_options(self):
renamed_options = {} renamed_options = {}
for option in ProjectOptions.values(): for option in ProjectOptions.values():
if option.oldnames: if option.oldnames:
@ -324,6 +324,7 @@ class ProjectConfigBase:
f"`${{this.__env__}}` is called from the `{parent_section}` " f"`${{this.__env__}}` is called from the `{parent_section}` "
"section that is not valid PlatformIO environment, see", "section that is not valid PlatformIO environment, see",
option, option,
" ",
section, section,
) )
return parent_section[4:] return parent_section[4:]
@ -332,7 +333,10 @@ class ProjectConfigBase:
value = self.get(section, option) value = self.get(section, option)
except RecursionError as exc: except RecursionError as exc:
raise exception.ProjectOptionValueError( raise exception.ProjectOptionValueError(
"Infinite recursion has been detected", option, section "Infinite recursion has been detected",
option,
" ",
section,
) from exc ) from exc
if isinstance(value, list): if isinstance(value, list):
return "\n".join(value) return "\n".join(value)
@ -359,7 +363,10 @@ class ProjectConfigBase:
if not self.expand_interpolations: if not self.expand_interpolations:
return value return value
raise exception.ProjectOptionValueError( raise exception.ProjectOptionValueError(
exc.format_message(), option, section exc.format_message(),
option,
" (%s) " % option_meta.description,
section,
) )
@staticmethod @staticmethod
@ -424,16 +431,51 @@ class ProjectConfigBase:
return True return True
class ProjectConfigLintMixin:
@classmethod
def lint(cls, path=None):
errors = []
warnings = []
try:
config = cls.get_instance(path)
config.validate(silent=True)
warnings = config.warnings
config.as_tuple()
except Exception as exc: # pylint: disable=broad-exception-caught
if exc.__cause__ is not None:
exc = exc.__cause__
item = {"type": exc.__class__.__name__, "message": str(exc)}
for attr in ("lineno", "source"):
if hasattr(exc, attr):
item[attr] = getattr(exc, attr)
if item["type"] == "ParsingError" and hasattr(exc, "errors"):
for lineno, line in getattr(exc, "errors"):
errors.append(
{
"type": item["type"],
"message": f"Parsing error: {line}",
"lineno": lineno,
"source": item["source"],
}
)
else:
errors.append(item)
return {"errors": errors, "warnings": warnings}
class ProjectConfigDirsMixin: class ProjectConfigDirsMixin:
def get_optional_dir(self, name): def get_optional_dir(self, name):
""" """
Deprecated, used by platformio-node-helpers.project.observer.fetchLibDirs Deprecated, used by platformio-node-helpers.project.observer.fetchLibDirs
PlatformIO IDE for Atom depends on platformio-node-helpers@~7.2.0 PlatformIO IDE for Atom depends on platformio-node-helpers@~7.2.0
PIO Home 3.0 Project Inspection depends on it
""" """
return self.get("platformio", f"{name}_dir") return self.get("platformio", f"{name}_dir")
class ProjectConfig(ProjectConfigBase, ProjectConfigDirsMixin): class ProjectConfig(ProjectConfigBase, ProjectConfigLintMixin, ProjectConfigDirsMixin):
_instances = {} _instances = {}
@staticmethod @staticmethod

View File

@ -51,4 +51,4 @@ class InvalidEnvNameError(ProjectError, UserSideException):
class ProjectOptionValueError(ProjectError, UserSideException): class ProjectOptionValueError(ProjectError, UserSideException):
MESSAGE = "{0} for option `{1}` in section [{2}]" MESSAGE = "{0} for option `{1}`{2}in section [{3}]"

View File

@ -164,6 +164,7 @@ load_project_ide_data = load_build_metadata
def _load_build_metadata(project_dir, env_names, debug=False): def _load_build_metadata(project_dir, env_names, debug=False):
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from platformio import app
from platformio.run.cli import cli as cmd_run from platformio.run.cli import cli as cmd_run
args = ["--project-dir", project_dir, "--target", "__idedata"] args = ["--project-dir", project_dir, "--target", "__idedata"]
@ -171,13 +172,15 @@ def _load_build_metadata(project_dir, env_names, debug=False):
args.extend(["--target", "__debug"]) args.extend(["--target", "__debug"])
for name in env_names: for name in env_names:
args.extend(["-e", name]) args.extend(["-e", name])
app.set_session_var("pause_telemetry", True)
result = CliRunner().invoke(cmd_run, args) result = CliRunner().invoke(cmd_run, args)
app.set_session_var("pause_telemetry", False)
if result.exit_code != 0 and not isinstance( if result.exit_code != 0 and not isinstance(
result.exception, exception.ReturnErrorCode result.exception, exception.ReturnErrorCode
): ):
raise result.exception raise result.exception
if '"includes":' not in result.output: if '"includes":' not in result.output:
raise exception.PlatformioException(result.output) raise exception.UserSideException(result.output)
return _get_cached_build_metadata(project_dir, env_names) return _get_cached_build_metadata(project_dir, env_names)

View File

@ -52,7 +52,7 @@ class ProjectGenerator:
@staticmethod @staticmethod
def get_ide_tpls_dir(): def get_ide_tpls_dir():
return os.path.join(fs.get_assets_dir(), "templates", "ide-projects") return os.path.join(os.path.dirname(__file__), "tpls")
@classmethod @classmethod
def get_supported_ides(cls): def get_supported_ides(cls):

View File

@ -8,6 +8,7 @@
% import os % import os
% import re % import re
% %
% from platformio.compat import shlex_join
% from platformio.project.helpers import load_build_metadata % from platformio.project.helpers import load_build_metadata
% %
% def _normalize_path(path): % def _normalize_path(path):
@ -64,17 +65,16 @@ set(CLION_SVD_FILE_PATH "{{ _normalize_path(svd_path) }}" CACHE FILEPATH "Periph
SET(CMAKE_C_COMPILER "{{ _normalize_path(cc_path) }}") SET(CMAKE_C_COMPILER "{{ _normalize_path(cc_path) }}")
SET(CMAKE_CXX_COMPILER "{{ _normalize_path(cxx_path) }}") SET(CMAKE_CXX_COMPILER "{{ _normalize_path(cxx_path) }}")
SET(CMAKE_CXX_FLAGS "{{ _normalize_path(to_unix_path(cxx_flags)) }}") SET(CMAKE_CXX_FLAGS {{ _normalize_path(to_unix_path(shlex_join(cxx_flags))) }})
SET(CMAKE_C_FLAGS "{{ _normalize_path(to_unix_path(cc_flags)) }}") SET(CMAKE_C_FLAGS {{ _normalize_path(to_unix_path(shlex_join(cc_flags))) }})
% STD_RE = re.compile(r"\-std=[a-z\+]+(\w+)") % cc_stds = [arg for arg in cc_flags if arg.startswith("-std=")]
% cc_stds = STD_RE.findall(cc_flags) % cxx_stds = [arg for arg in cxx_flags if arg.startswith("-std=")]
% cxx_stds = STD_RE.findall(cxx_flags)
% if cc_stds: % if cc_stds:
SET(CMAKE_C_STANDARD {{ cc_stds[-1] }}) SET(CMAKE_C_STANDARD {{ cc_stds[-1][-2:] }})
% end % end
% if cxx_stds: % if cxx_stds:
set(CMAKE_CXX_STANDARD {{ cxx_stds[-1] }}) set(CMAKE_CXX_STANDARD {{ cxx_stds[-1][-2:] }})
% end % end
if (CMAKE_BUILD_TYPE MATCHES "{{ env_name }}") if (CMAKE_BUILD_TYPE MATCHES "{{ env_name }}")

View File

@ -1,6 +1,4 @@
% import re % cxx_stds = [arg for arg in cxx_flags if arg.startswith("-std=")]
% STD_RE = re.compile(r"(\-std=[a-z\+]+\w+)")
% cxx_stds = STD_RE.findall(cxx_flags)
% cxx_std = cxx_stds[-1] if cxx_stds else "" % cxx_std = cxx_stds[-1] if cxx_stds else ""
% %
% if cxx_path.startswith(user_home_dir): % if cxx_path.startswith(user_home_dir):

View File

@ -1,7 +1,9 @@
% from platformio.compat import shlex_join
%
clang clang
{{"%c"}} {{ !cc_flags }} {{"%c"}} {{ shlex_join(cc_flags) }}
{{"%cpp"}} {{ !cxx_flags }} {{"%cpp"}} {{ shlex_join(cxx_flags) }}
% for include in filter_includes(includes): % for include in filter_includes(includes):
-I{{ !include }} -I{{ !include }}

View File

@ -0,0 +1,3 @@
% from platformio.compat import shlex_join
%
{{shlex_join(cc_flags).replace('-mlongcalls', '-mlong-calls')}}

View File

@ -0,0 +1,3 @@
% from platformio.compat import shlex_join
%
{{shlex_join(cxx_flags).replace('-mlongcalls', '-mlong-calls')}}

View File

@ -1,7 +1,9 @@
% from platformio.compat import shlex_join
%
clang clang
{{"%c"}} {{ !cc_flags }} {{"%c"}} {{ shlex_join(cc_flags) }}
{{"%cpp"}} {{ !cxx_flags }} {{"%cpp"}} {{ shlex_join(cxx_flags) }}
% for include in filter_includes(includes): % for include in filter_includes(includes):
-I{{ !include }} -I{{ !include }}

View File

@ -1,7 +1,9 @@
% from platformio.compat import shlex_join
%
clang clang
{{"%c"}} {{ !cc_flags }} {{"%c"}} {{ shlex_join(cc_flags) }}
{{"%cpp"}} {{ !cxx_flags }} {{"%cpp"}} {{ shlex_join(cxx_flags) }}
% for include in filter_includes(includes): % for include in filter_includes(includes):
-I{{ !include }} -I{{ !include }}

View File

@ -1,27 +1,25 @@
% import os % import os
% import platform % import platform
% import re
%
% import click
% %
% systype = platform.system().lower() % systype = platform.system().lower()
% %
% cpp_standards_remap = { % cpp_standards_remap = {
% "0x": "11", % "c++0x": "c++11",
% "1y": "14", % "c++1y": "c++14",
% "1z": "17", % "c++1z": "c++17",
% "2a": "20", % "c++2a": "c++20",
% "2b": "23" % "c++2b": "c++23",
% "gnu++0x": "gnu++11",
% "gnu++1y": "gnu++14",
% "gnu++1z": "gnu++17",
% "gnu++2a": "gnu++20",
% "gnu++2b": "gnu++23"
% } % }
% %
% def _escape(text): % def _escape(text):
% return to_unix_path(text).replace('"', '\\"') % return to_unix_path(text).replace('"', '\\"')
% end % end
% %
% def split_args(args_string):
% return click.parser.split_arg_string(to_unix_path(args_string))
% end
%
% def filter_args(args, allowed, ignore=None): % def filter_args(args, allowed, ignore=None):
% if not allowed: % if not allowed:
% return [] % return []
@ -31,18 +29,18 @@
% result = [] % result = []
% i = 0 % i = 0
% length = len(args) % length = len(args)
% while(i < length): % while(i < length):
% if any(args[i].startswith(f) for f in allowed) and not any( % if any(args[i].startswith(f) for f in allowed) and not any(
% args[i].startswith(f) for f in ignore): % args[i].startswith(f) for f in ignore):
% result.append(args[i]) % result.append(args[i])
% if i + 1 < length and not args[i + 1].startswith("-"): % if i + 1 < length and not args[i + 1].startswith("-"):
% i += 1 % i += 1
% result.append(args[i]) % result.append(args[i])
% end
% end % end
% i += 1 % end
% end % i += 1
% return result % end
% return result
% end % end
% %
% def _find_abs_path(inc, inc_paths): % def _find_abs_path(inc, inc_paths):
@ -76,12 +74,10 @@
% %
% cleaned_includes = filter_includes(includes, ["toolchain"]) % cleaned_includes = filter_includes(includes, ["toolchain"])
% %
% STD_RE = re.compile(r"\-std=[a-z\+]+(\w+)") % cc_stds = [arg[5:] for arg in cc_flags if arg.startswith("-std=")]
% cc_stds = STD_RE.findall(cc_flags) % cxx_stds = [arg[5:] for arg in cxx_flags if arg.startswith("-std=")]
% cxx_stds = STD_RE.findall(cxx_flags)
% cc_m_flags = split_args(cc_flags)
% forced_includes = _find_forced_includes( % forced_includes = _find_forced_includes(
% filter_args(cc_m_flags, ["-include", "-imacros"]), cleaned_includes) % filter_args(cc_flags, ["-include", "-imacros"]), cleaned_includes)
% %
// //
// !!! WARNING !!! AUTO-GENERATED FILE! // !!! WARNING !!! AUTO-GENERATED FILE!
@ -114,10 +110,10 @@
"" ""
], ],
% if cc_stds: % if cc_stds:
"cStandard": "c{{ cc_stds[-1] }}", "cStandard": "{{ cc_stds[-1] }}",
% end % end
% if cxx_stds: % if cxx_stds:
"cppStandard": "c++{{ cpp_standards_remap.get(cxx_stds[-1], cxx_stds[-1]) }}", "cppStandard": "{{ cpp_standards_remap.get(cxx_stds[-1], cxx_stds[-1]) }}",
% end % end
% if forced_includes: % if forced_includes:
"forcedInclude": [ "forcedInclude": [
@ -130,7 +126,7 @@
"compilerPath": "{{ cc_path }}", "compilerPath": "{{ cc_path }}",
"compilerArgs": [ "compilerArgs": [
% for flag in [ % for flag in [
% f for f in filter_args(cc_m_flags, ["-m", "-i", "@"], ["-include", "-imacros"]) % f for f in filter_args(cc_flags, ["-m", "-i", "@"], ["-include", "-imacros"])
% ]: % ]:
"{{ flag }}", "{{ flag }}",
% end % end

View File

@ -142,12 +142,12 @@ class RegistryClient(HTTPClient):
x_with_authorization=self.allowed_private_packages(), x_with_authorization=self.allowed_private_packages(),
) )
def get_package(self, type_, owner, name, version=None, extra_path=None): def get_package(self, typex, owner, name, version=None, extra_path=None):
try: try:
return self.fetch_json_data( return self.fetch_json_data(
"get", "get",
"/v3/packages/{owner}/{type}/{name}{extra_path}".format( "/v3/packages/{owner}/{type}/{name}{extra_path}".format(
type=type_, type=typex,
owner=owner.lower(), owner=owner.lower(),
name=name.lower(), name=name.lower(),
extra_path=extra_path or "", extra_path=extra_path or "",

View File

@ -77,7 +77,6 @@ def system_info_cmd(json_output):
).get_installed() ).get_installed()
), ),
} }
click.echo( click.echo(
json.dumps(data) json.dumps(data)
if json_output if json_output

View File

@ -29,8 +29,10 @@ class ShellType(Enum):
def get_bash_version(): def get_bash_version():
result = subprocess.run(["bash", "--version"], capture_output=True, check=True) output = subprocess.run(
match = re.search(r"version\s+(\d+)\.(\d+)", result.stdout.decode(), re.IGNORECASE) ["bash", "--version"], check=True, stdout=subprocess.PIPE
).stdout.decode()
match = re.search(r"version\s+(\d+)\.(\d+)", output, re.IGNORECASE)
if match: if match:
return (int(match.group(1)), int(match.group(2))) return (int(match.group(1)), int(match.group(2)))
return (0, 0) return (0, 0)

View File

@ -14,299 +14,181 @@
import atexit import atexit
import hashlib import hashlib
import json
import os import os
import queue import queue
import re import re
import shutil
import sys import sys
import threading import threading
import time
import traceback
from collections import deque from collections import deque
from time import sleep, time
from traceback import format_exc
import requests import requests
from platformio import __version__, app, exception, util from platformio import __title__, __version__, app, exception, fs, util
from platformio.cli import PlatformioCLI from platformio.cli import PlatformioCLI
from platformio.compat import hashlib_encode_data, string_types from platformio.compat import hashlib_encode_data
from platformio.http import HTTPSession from platformio.debug.config.base import DebugConfigBase
from platformio.proc import is_ci, is_container from platformio.http import HTTPSession, ensure_internet_on
from platformio.project.helpers import is_platformio_project from platformio.proc import is_ci
KEEP_MAX_REPORTS = 100
SEND_MAX_EVENTS = 25
class TelemetryBase: class MeasurementProtocol:
def __init__(self): def __init__(self, events=None):
self._params = {} self.client_id = app.get_cid()
self._events = events or []
self._user_properties = {}
def __getitem__(self, name): self.set_user_property("systype", util.get_systype())
return self._params.get(name, None) created_at = app.get_state_item("created_at", None)
if created_at:
self.set_user_property("created_at", int(created_at))
def __setitem__(self, name, value): @staticmethod
self._params[name] = value def event_to_dict(name, params, timestamp=None):
event = {"name": name, "params": params}
if timestamp is not None:
event["timestamp"] = timestamp
return event
def __delitem__(self, name): def set_user_property(self, name, value):
if name in self._params: self._user_properties[name] = value
del self._params[name]
def send(self, hittype): def add_event(self, name, params):
raise NotImplementedError() self._events.append(self.event_to_dict(name, params))
def to_payload(self):
class MeasurementProtocol(TelemetryBase): return {
TID = "UA-1768265-9" "client_id": self.client_id,
PARAMS_MAP = { "user_properties": self._user_properties,
"screen_name": "cd", "events": self._events,
"event_category": "ec", }
"event_action": "ea",
"event_label": "el",
"event_value": "ev",
}
def __init__(self):
super().__init__()
self["v"] = 1
self["tid"] = self.TID
self["cid"] = app.get_cid()
try:
self["sr"] = "%dx%d" % shutil.get_terminal_size()
except ValueError:
pass
self._prefill_screen_name()
self._prefill_appinfo()
self._prefill_sysargs()
self._prefill_custom_data()
def __getitem__(self, name):
if name in self.PARAMS_MAP:
name = self.PARAMS_MAP[name]
return super().__getitem__(name)
def __setitem__(self, name, value):
if name in self.PARAMS_MAP:
name = self.PARAMS_MAP[name]
super().__setitem__(name, value)
def _prefill_appinfo(self):
self["av"] = __version__
self["an"] = app.get_user_agent()
def _prefill_sysargs(self):
args = []
for arg in sys.argv[1:]:
arg = str(arg)
if arg == "account": # ignore account cmd which can contain username
return
if any(("@" in arg, "/" in arg, "\\" in arg)):
arg = "***"
args.append(arg.lower())
self["cd3"] = " ".join(args)
def _prefill_custom_data(self):
caller_id = str(app.get_session_var("caller_id"))
self["cd1"] = util.get_systype()
self["cd4"] = 1 if (not is_ci() and (caller_id or not is_container())) else 0
if caller_id:
self["cd5"] = caller_id.lower()
def _prefill_screen_name(self):
def _first_arg_from_list(args_, list_):
for _arg in args_:
if _arg in list_:
return _arg
return None
args = []
for arg in PlatformioCLI.leftover_args:
if not isinstance(arg, string_types):
arg = str(arg)
if not arg.startswith("-"):
args.append(arg.lower())
if not args:
return
cmd_path = args[:1]
if args[0] in (
"access",
"account",
"device",
"org",
"package",
"pkg",
"platform",
"project",
"settings",
"system",
"team",
):
cmd_path = args[:2]
if args[0] == "lib" and len(args) > 1:
lib_subcmds = (
"builtin",
"install",
"list",
"register",
"search",
"show",
"stats",
"uninstall",
"update",
)
sub_cmd = _first_arg_from_list(args[1:], lib_subcmds)
if sub_cmd:
cmd_path.append(sub_cmd)
elif args[0] == "remote" and len(args) > 1:
remote_subcmds = ("agent", "device", "run", "test")
sub_cmd = _first_arg_from_list(args[1:], remote_subcmds)
if sub_cmd:
cmd_path.append(sub_cmd)
if len(args) > 2 and sub_cmd in ("agent", "device"):
remote2_subcmds = ("list", "start", "monitor")
sub_cmd = _first_arg_from_list(args[2:], remote2_subcmds)
if sub_cmd:
cmd_path.append(sub_cmd)
self["screen_name"] = " ".join([p.title() for p in cmd_path])
def _ignore_hit(self):
if not app.get_setting("enable_telemetry"):
return True
if self["ea"] in ("Idedata", "__Idedata"):
return True
return False
def send(self, hittype):
if self._ignore_hit():
return
self["t"] = hittype
# correct queue time
if "qt" in self._params and isinstance(self["qt"], float):
self["qt"] = int((time() - self["qt"]) * 1000)
MPDataPusher().push(self._params)
@util.singleton @util.singleton
class MPDataPusher: class TelemetryLogger:
MAX_WORKERS = 5
def __init__(self): def __init__(self):
self._queue = queue.LifoQueue() self._events = deque()
self._failedque = deque()
self._sender_thread = None
self._sender_queue = queue.Queue()
self._sender_terminated = False
self._http_session = HTTPSession() self._http_session = HTTPSession()
self._http_offline = False self._http_offline = False
self._workers = []
def push(self, item): def close(self):
# if network is off-line self._http_session.close()
if self._http_offline:
if "qt" not in item: def log_event(self, name, params, timestamp=None, instant_sending=False):
item["qt"] = time() if not app.get_setting("enable_telemetry") or app.get_session_var(
self._failedque.append(item) "pause_telemetry"
):
return None
timestamp = timestamp or int(time.time())
self._events.append(
MeasurementProtocol.event_to_dict(name, params, timestamp=timestamp)
)
if self._http_offline: # if network is off-line
return False
if instant_sending:
self.send()
return True
def send(self):
if not self._events or self._sender_terminated:
return return
if not self._sender_thread:
self._queue.put(item) self._sender_thread = threading.Thread(
self._tune_workers() target=self._sender_worker, daemon=True
)
def in_wait(self): self._sender_thread.start()
return self._queue.unfinished_tasks while self._events:
events = []
def get_items(self):
items = list(self._failedque)
try:
while True:
items.append(self._queue.get_nowait())
except queue.Empty:
pass
return items
def _tune_workers(self):
for i, w in enumerate(self._workers):
if not w.is_alive():
del self._workers[i]
need_nums = min(self._queue.qsize(), self.MAX_WORKERS)
active_nums = len(self._workers)
if need_nums <= active_nums:
return
for i in range(need_nums - active_nums):
t = threading.Thread(target=self._worker)
t.daemon = True
t.start()
self._workers.append(t)
def _worker(self):
while True:
try: try:
item = self._queue.get() while len(events) < SEND_MAX_EVENTS:
_item = item.copy() events.append(self._events.popleft())
if "qt" not in _item: except IndexError:
_item["qt"] = time() pass
self._failedque.append(_item) self._sender_queue.put(events)
if self._send_data(item):
self._failedque.remove(_item) def _sender_worker(self):
self._queue.task_done() while True:
except: # pylint: disable=W0702 if self._sender_terminated:
return
try:
events = self._sender_queue.get()
if not self._commit_events(events):
self._events.extend(events)
self._sender_queue.task_done()
except (queue.Empty, ValueError):
pass pass
def _send_data(self, data): def _commit_events(self, events):
if self._http_offline: if self._http_offline:
return False return False
mp = MeasurementProtocol(events)
payload = mp.to_payload()
# print("_commit_payload", payload)
try: try:
r = self._http_session.post( r = self._http_session.post(
"https://ssl.google-analytics.com/collect", "https://telemetry.platformio.org/collect",
data=data, json=payload,
timeout=1, timeout=(2, 5), # connect, read
) )
r.raise_for_status() r.raise_for_status()
return True return True
except requests.exceptions.HTTPError as exc: except requests.exceptions.HTTPError as exc:
# skip Bad Request # skip Bad Request
if 400 >= exc.response.status_code < 500: if exc.response.status_code >= 400 and exc.response.status_code < 500:
return True return True
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
pass pass
self._http_offline = True self._http_offline = True
return False return False
def terminate_sender(self):
self._sender_terminated = True
def on_command(): def is_sending(self):
resend_backuped_reports() return self._sender_queue.unfinished_tasks
mp = MeasurementProtocol() def get_unsent_events(self):
mp.send("screenview") result = list(self._events)
try:
while True:
result.extend(self._sender_queue.get_nowait())
except queue.Empty:
pass
return result
def log_event(name, params, instant_sending=False):
TelemetryLogger().log_event(name, params, instant_sending=instant_sending)
def on_cmd_start(cmd_ctx):
process_postponed_logs()
log_command(cmd_ctx)
def on_exit():
TelemetryLogger().send()
def log_command(ctx):
params = {
"path_args": PlatformioCLI.reveal_cmd_path_args(ctx),
}
if is_ci(): if is_ci():
measure_ci() params["ci_actor"] = resolve_ci_actor() or "Unknown"
log_event("cmd_run", params)
def on_exception(e): def resolve_ci_actor():
skip_conditions = [
isinstance(e, cls)
for cls in (
IOError,
exception.ReturnErrorCode,
exception.UserSideException,
)
]
if any(skip_conditions):
return
is_fatal = any(
[
not isinstance(e, exception.PlatformioException),
"Error" in e.__class__.__name__,
]
)
description = "%s: %s" % (
type(e).__name__,
" ".join(reversed(format_exc().split("\n"))) if is_fatal else str(e),
)
send_exception(description, is_fatal)
def measure_ci():
event = {"category": "CI", "action": "NoName", "label": None}
known_cis = ( known_cis = (
"GITHUB_ACTIONS", "GITHUB_ACTIONS",
"TRAVIS", "TRAVIS",
@ -318,123 +200,184 @@ def measure_ci():
) )
for name in known_cis: for name in known_cis:
if os.getenv(name, "false").lower() == "true": if os.getenv(name, "false").lower() == "true":
event["action"] = name return name
break return None
send_event(**event)
def dump_run_environment(options): def dump_project_env_params(config, env, platform):
non_sensitive_data = [ non_sensitive_data = [
"platform", "platform",
"platform_packages",
"framework", "framework",
"board", "board",
"upload_protocol", "upload_protocol",
"check_tool", "check_tool",
"debug_tool", "debug_tool",
"monitor_filters",
"test_framework", "test_framework",
] ]
safe_options = {k: v for k, v in options.items() if k in non_sensitive_data} section = f"env:{env}"
if is_platformio_project(os.getcwd()): params = {
phash = hashlib.sha1(hashlib_encode_data(app.get_cid())) option: config.get(section, option)
safe_options["pid"] = phash.hexdigest() for option in non_sensitive_data
return json.dumps(safe_options, sort_keys=True, ensure_ascii=False) if config.has_option(section, option)
}
params["pid"] = hashlib.sha1(hashlib_encode_data(config.path)).hexdigest()
params["platform_name"] = platform.name
params["platform_version"] = platform.version
return params
def send_run_environment(options, targets): def log_platform_run(platform, project_config, project_env, targets=None):
send_event( params = dump_project_env_params(project_config, project_env, platform)
"Env", if targets:
" ".join([t.title() for t in targets or ["run"]]), params["targets"] = targets
dump_run_environment(options), log_event("platform_run", params, instant_sending=True)
def log_exception(exc):
skip_conditions = [
isinstance(exc, cls)
for cls in (
IOError,
exception.ReturnErrorCode,
exception.UserSideException,
)
]
skip_conditions.append(not isinstance(exc, Exception))
if any(skip_conditions):
return
is_fatal = any(
[
not isinstance(exc, exception.PlatformioException),
"Error" in exc.__class__.__name__,
]
)
def _strip_module_path(match):
module_path = match.group(1).replace(fs.get_source_dir() + os.sep, "")
sp_folder_name = "site-packages"
sp_pos = module_path.find(sp_folder_name)
if sp_pos != -1:
module_path = module_path[sp_pos + len(sp_folder_name) + 1 :]
module_path = fs.to_unix_path(module_path)
return f'File "{module_path}",'
trace = re.sub(
r'File "([^"]+)",',
_strip_module_path,
traceback.format_exc(),
flags=re.MULTILINE,
)
params = {
"name": exc.__class__.__name__,
"description": str(exc),
"traceback": trace,
"cmd_args": sys.argv[1:],
"is_fatal": is_fatal,
}
log_event("exception", params)
def log_debug_started(debug_config: DebugConfigBase):
log_event(
"debug_started",
dump_project_env_params(
debug_config.project_config, debug_config.env_name, debug_config.platform
),
) )
def send_event(category, action, label=None, value=None, screen_name=None): def log_debug_exception(exc, debug_config: DebugConfigBase):
mp = MeasurementProtocol()
mp["event_category"] = category[:150]
mp["event_action"] = action[:500]
if label:
mp["event_label"] = label[:500]
if value:
mp["event_value"] = int(value)
if screen_name:
mp["screen_name"] = screen_name[:2048]
mp.send("event")
def send_exception(description, is_fatal=False):
# cleanup sensitive information, such as paths # cleanup sensitive information, such as paths
description = description.replace("Traceback (most recent call last):", "") description = fs.to_unix_path(str(exc))
description = description.replace("\\", "/")
description = re.sub( description = re.sub(
r'(^|\s+|")(?:[a-z]\:)?((/[^"/]+)+)(\s+|"|$)', r'(^|\s+|")(?:[a-z]\:)?((/[^"/]+)+)(\s+|"|$)',
lambda m: " %s " % os.path.join(*m.group(2).split("/")[-2:]), lambda m: " %s " % os.path.join(*m.group(2).split("/")[-2:]),
description, description,
re.I | re.M, re.I | re.M,
) )
description = re.sub(r"\s+", " ", description, flags=re.M) params = {
"name": exc.__class__.__name__,
mp = MeasurementProtocol() "description": description.strip(),
mp["exd"] = description[:8192].strip() }
mp["exf"] = 1 if is_fatal else 0 params.update(
mp.send("exception") dump_project_env_params(
debug_config.project_config, debug_config.env_name, debug_config.platform
)
)
log_event("debug_exception", params)
@atexit.register @atexit.register
def _finalize(): def _finalize():
timeout = 1000 # msec timeout = 1000 # msec
elapsed = 0 elapsed = 0
telemetry = TelemetryLogger()
telemetry.terminate_sender()
try: try:
while elapsed < timeout: while elapsed < timeout:
if not MPDataPusher().in_wait(): if not telemetry.is_sending():
break break
sleep(0.2) time.sleep(0.2)
elapsed += 200 elapsed += 200
backup_reports(MPDataPusher().get_items())
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
postpone_events(telemetry.get_unsent_events())
telemetry.close()
def backup_reports(items): def load_postponed_events():
if not items: state_path = app.resolve_state_path(
return "cache_dir", "telemetry.json", ensure_dir_exists=False
)
KEEP_MAX_REPORTS = 100 if not os.path.isfile(state_path):
tm = app.get_state_item("telemetry", {}) return []
if "backup" not in tm: with app.State(state_path) as state:
tm["backup"] = [] return state.get("events", [])
for params in items:
# skip static options
for key in list(params.keys()):
if key in ("v", "tid", "cid", "cd1", "cd2", "sr", "an"):
del params[key]
# store time in UNIX format
if "qt" not in params:
params["qt"] = time()
elif not isinstance(params["qt"], float):
params["qt"] = time() - (params["qt"] / 1000)
tm["backup"].append(params)
tm["backup"] = tm["backup"][KEEP_MAX_REPORTS * -1 :]
app.set_state_item("telemetry", tm)
def resend_backuped_reports(): def save_postponed_events(events):
tm = app.get_state_item("telemetry", {}) state_path = app.resolve_state_path("cache_dir", "telemetry.json")
if "backup" not in tm or not tm["backup"]: if not events:
return False try:
if os.path.isfile(state_path):
for report in tm["backup"]: os.remove(state_path)
mp = MeasurementProtocol() except: # pylint: disable=bare-except
for key, value in report.items(): pass
mp[key] = value return None
mp.send(report["t"]) with app.State(state_path, lock=True) as state:
state["events"] = events
# clean state.modified = True
tm["backup"] = [] return True
app.set_state_item("telemetry", tm)
def postpone_events(events):
if not events:
return None
postponed_events = load_postponed_events() or []
timestamp = int(time.time())
for event in events:
if "timestamp" not in event:
event["timestamp"] = timestamp
postponed_events.append(event)
save_postponed_events(postponed_events[KEEP_MAX_REPORTS * -1 :])
return True
def process_postponed_logs():
if not ensure_internet_on():
return None
events = load_postponed_events()
if not events:
return None
save_postponed_events([]) # clean
telemetry = TelemetryLogger()
for event in events:
if set(["name", "params", "timestamp"]) <= set(event.keys()):
telemetry.log_event(
event["name"],
event["params"],
timestamp=event["timestamp"],
instant_sending=False,
)
return True return True

View File

@ -55,8 +55,8 @@ extern "C"
void unityOutputStart(unsigned long); void unityOutputStart(unsigned long);
void unityOutputChar(unsigned int); void unityOutputChar(unsigned int);
void unityOutputFlush(); void unityOutputFlush(void);
void unityOutputComplete(); void unityOutputComplete(void);
#define UNITY_OUTPUT_START() unityOutputStart((unsigned long) $baudrate) #define UNITY_OUTPUT_START() unityOutputStart((unsigned long) $baudrate)
#define UNITY_OUTPUT_CHAR(c) unityOutputChar(c) #define UNITY_OUTPUT_CHAR(c) unityOutputChar(c)
@ -246,18 +246,20 @@ void unityOutputComplete(void) { unittest_uart_end(); }
unity_h = dst_dir / "unity_config.h" unity_h = dst_dir / "unity_config.h"
if not unity_h.is_file(): if not unity_h.is_file():
unity_h.write_text( unity_h.write_text(
string.Template(self.UNITY_CONFIG_H).substitute( string.Template(self.UNITY_CONFIG_H)
baudrate=self.get_test_speed() .substitute(baudrate=self.get_test_speed())
), .strip()
+ "\n",
encoding="utf8", encoding="utf8",
) )
framework_config = self.get_unity_framework_config() framework_config = self.get_unity_framework_config()
unity_c = dst_dir / ("unity_config.%s" % framework_config.get("language", "c")) unity_c = dst_dir / ("unity_config.%s" % framework_config.get("language", "c"))
if not unity_c.is_file(): if not unity_c.is_file():
unity_c.write_text( unity_c.write_text(
string.Template(self.UNITY_CONFIG_C).substitute( string.Template(self.UNITY_CONFIG_C)
framework_config_code=framework_config["code"] .substitute(framework_config_code=framework_config["code"])
), .strip()
+ "\n",
encoding="utf8", encoding="utf8",
) )

View File

@ -12,13 +12,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import datetime
import functools import functools
import math import math
import platform import platform
import re import re
import shutil import shutil
import time import time
from datetime import datetime
import click import click
@ -169,8 +169,8 @@ def items_in_list(needle, haystack):
def parse_datetime(datestr): def parse_datetime(datestr):
if "T" in datestr and "Z" in datestr: if "T" in datestr and "Z" in datestr:
return datetime.strptime(datestr, "%Y-%m-%dT%H:%M:%SZ") return datetime.datetime.strptime(datestr, "%Y-%m-%dT%H:%M:%SZ")
return datetime.strptime(datestr) return datetime.datetime.strptime(datestr)
def merge_dicts(d1, d2, path=None): def merge_dicts(d1, d2, path=None):

View File

@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
from setuptools import find_packages, setup from setuptools import find_packages, setup
from platformio import ( from platformio import (
@ -25,31 +24,48 @@ from platformio import (
__version__, __version__,
) )
PY36 = sys.version_info < (3, 7) env_marker_below_37 = "python_version < '3.7'"
env_marker_gte_37 = "python_version >= '3.7'"
minimal_requirements = [ minimal_requirements = [
"bottle==0.12.*", "bottle==0.12.*",
"click%s" % ("==8.0.4" if PY36 else ">=8.0.4,<9"), "click==8.0.4; " + env_marker_below_37,
"click==8.1.*; " + env_marker_gte_37,
"colorama", "colorama",
"marshmallow==%s" % ("3.14.1" if PY36 else "3.*"), "marshmallow==3.14.1; " + env_marker_below_37,
"pyelftools>=0.27,<1", "marshmallow==3.19.*; " + env_marker_gte_37,
"pyelftools==0.29",
"pyserial==3.5.*", # keep in sync "device/monitor/terminal.py" "pyserial==3.5.*", # keep in sync "device/monitor/terminal.py"
"requests==2.*", "requests==2.*",
"urllib3<2", # issue 4614: urllib3 v2.0 only supports OpenSSL 1.1.1+
"requests==%s" % ("2.27.1" if PY36 else "2.*"),
"semantic_version==2.10.*", "semantic_version==2.10.*",
"tabulate==%s" % ("0.8.10" if PY36 else "0.9.*"), "tabulate==0.*",
] ]
home_requirements = [ home_requirements = [
"aiofiles==%s" % ("0.8.0" if PY36 else "23.1.*"), "aiofiles>=0.8.0",
"ajsonrpc==1.*", "ajsonrpc==1.2.*",
"starlette==%s" % ("0.19.1" if PY36 else "0.26.*"), "starlette==0.19.1; " + env_marker_below_37,
"uvicorn==%s" % ("0.16.0" if PY36 else "0.22.*"), "starlette==0.28.*; " + env_marker_gte_37,
"wsproto==%s" % ("1.0.0" if PY36 else "1.2.*"), "uvicorn==0.16.0; " + env_marker_below_37,
"uvicorn==0.22.*; " + env_marker_gte_37,
"wsproto==1.0.0; " + env_marker_below_37,
"wsproto==1.2.*; " + env_marker_gte_37,
] ]
# issue 4614: urllib3 v2.0 only supports OpenSSL 1.1.1+
try:
import ssl
if ssl.OPENSSL_VERSION.startswith("OpenSSL ") and ssl.OPENSSL_VERSION_INFO < (
1,
1,
1,
):
minimal_requirements.append("urllib3<2")
except ImportError:
pass
setup( setup(
name=__title__, name=__title__,
version=__version__, version=__version__,
@ -65,11 +81,11 @@ setup(
package_data={ package_data={
"platformio": [ "platformio": [
"assets/system/99-platformio-udev.rules", "assets/system/99-platformio-udev.rules",
"assets/templates/ide-projects/*/*.tpl", "project/integration/tpls/*/*.tpl",
"assets/templates/ide-projects/*/.*.tpl", # include hidden files "project/integration/tpls/*/.*.tpl", # include hidden files
"assets/templates/ide-projects/*/.*/*.tpl", # include hidden folders "project/integration/tpls/*/.*/*.tpl", # include hidden folders
"assets/templates/ide-projects/*/*/*.tpl", # NetBeans "project/integration/tpls/*/*/*.tpl", # NetBeans
"assets/templates/ide-projects/*/*/*/*.tpl", # NetBeans "project/integration/tpls/*/*/*/*.tpl", # NetBeans
] ]
}, },
entry_points={ entry_points={

View File

@ -469,7 +469,7 @@ def test_custom_project_tools(
project_dir = tmp_path / "project" project_dir = tmp_path / "project"
project_dir.mkdir() project_dir.mkdir()
(project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL) (project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL)
spec = "platformio/tool-openocd" spec = "platformio/tool-openocd @ ^2"
result = clirunner.invoke( result = clirunner.invoke(
package_install_cmd, package_install_cmd,
["-d", str(project_dir), "-e", "devkit", "-t", spec], ["-d", str(project_dir), "-e", "devkit", "-t", spec],
@ -503,7 +503,7 @@ def test_custom_project_tools(
# check saved deps # check saved deps
assert config.get("env:devkit", "platform_packages") == [ assert config.get("env:devkit", "platform_packages") == [
"platformio/tool-openocd@^2.1100.211028", "platformio/tool-openocd@^2",
] ]
# install tool without saving to config # install tool without saving to config
@ -518,7 +518,7 @@ def test_custom_project_tools(
PackageSpec("tool-openocd@2.1100.211028"), PackageSpec("tool-openocd@2.1100.211028"),
] ]
assert config.get("env:devkit", "platform_packages") == [ assert config.get("env:devkit", "platform_packages") == [
"platformio/tool-openocd@^2.1100.211028", "platformio/tool-openocd@^2",
] ]
# unknown tool # unknown tool

View File

@ -313,7 +313,7 @@ def test_custom_project_tools(
project_dir = tmp_path / "project" project_dir = tmp_path / "project"
project_dir.mkdir() project_dir.mkdir()
(project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL) (project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL)
spec = "platformio/tool-openocd" spec = "platformio/tool-openocd@^2"
result = clirunner.invoke( result = clirunner.invoke(
package_install_cmd, package_install_cmd,
["-d", str(project_dir), "-e", "devkit", "-t", spec], ["-d", str(project_dir), "-e", "devkit", "-t", spec],
@ -329,7 +329,7 @@ def test_custom_project_tools(
assert not os.path.exists(config.get("platformio", "platforms_dir")) assert not os.path.exists(config.get("platformio", "platforms_dir"))
# check saved deps # check saved deps
assert config.get("env:devkit", "platform_packages") == [ assert config.get("env:devkit", "platform_packages") == [
"platformio/tool-openocd@^2.1100.211028", "platformio/tool-openocd@^2",
] ]
# uninstall # uninstall
result = clirunner.invoke( result = clirunner.invoke(

View File

@ -62,7 +62,7 @@ def test_init_duplicated_boards(clirunner, validate_cliresult, tmpdir):
def test_init_ide_without_board(clirunner, tmpdir): def test_init_ide_without_board(clirunner, tmpdir):
with tmpdir.as_cwd(): with tmpdir.as_cwd():
result = clirunner.invoke(project_init_cmd, ["--ide", "atom"]) result = clirunner.invoke(project_init_cmd, ["--ide", "vscode"])
assert result.exit_code != 0 assert result.exit_code != 0
assert isinstance(result.exception, ProjectEnvsNotAvailableError) assert isinstance(result.exception, ProjectEnvsNotAvailableError)

View File

@ -23,6 +23,10 @@ def test_generic_build(clirunner, validate_cliresult, tmpdir):
("-DTEST_SINGLE_MACRO", "-DTEST_SINGLE_MACRO"), ("-DTEST_SINGLE_MACRO", "-DTEST_SINGLE_MACRO"),
('-DTEST_STR_SPACE="Andrew Smith"', '"-DTEST_STR_SPACE=Andrew Smith"'), ('-DTEST_STR_SPACE="Andrew Smith"', '"-DTEST_STR_SPACE=Andrew Smith"'),
("-Iextra_inc", "-Iextra_inc"), ("-Iextra_inc", "-Iextra_inc"),
(
"-include $PROJECT_DIR/lib/component/component-forced-include.h",
"component-forced-include.h",
),
] ]
tmpdir.join("platformio.ini").write( tmpdir.join("platformio.ini").write(
@ -95,6 +99,10 @@ projenv.Append(CPPDEFINES="POST_SCRIPT_MACRO")
#error "POST_SCRIPT_MACRO" #error "POST_SCRIPT_MACRO"
#endif #endif
#ifndef I_AM_FORCED_COMPONENT_INCLUDE
#error "I_AM_FORCED_COMPONENT_INCLUDE"
#endif
#ifdef COMMENTED_MACRO #ifdef COMMENTED_MACRO
#error "COMMENTED_MACRO" #error "COMMENTED_MACRO"
#endif #endif
@ -124,6 +132,11 @@ void dummy(void);
void dummy(void ) {}; void dummy(void ) {};
""" """
) )
component_dir.join("component-forced-include.h").write(
"""
#define I_AM_FORCED_COMPONENT_INCLUDE
"""
)
result = clirunner.invoke(cmd_run, ["--project-dir", str(tmpdir), "--verbose"]) result = clirunner.invoke(cmd_run, ["--project-dir", str(tmpdir), "--verbose"])
validate_cliresult(result) validate_cliresult(result)

View File

@ -36,13 +36,13 @@ def test_ping_internet_ips():
def test_api_internet_offline(without_internet, isolated_pio_core): def test_api_internet_offline(without_internet, isolated_pio_core):
regclient = RegistryClient() regclient = RegistryClient()
with pytest.raises(http.InternetConnectionError): with pytest.raises(http.InternetConnectionError):
regclient.fetch_json_data("get", "/v2/stats") regclient.fetch_json_data("get", "/v3/search")
def test_api_cache(monkeypatch, isolated_pio_core): def test_api_cache(monkeypatch, isolated_pio_core):
regclient = RegistryClient() regclient = RegistryClient()
api_kwargs = {"method": "get", "path": "/v2/stats", "x_cache_valid": "10s"} api_kwargs = {"method": "get", "path": "/v3/search", "x_cache_valid": "10s"}
result = regclient.fetch_json_data(**api_kwargs) result = regclient.fetch_json_data(**api_kwargs)
assert result and "boards" in result assert result and "total" in result
monkeypatch.setattr(http, "_internet_on", lambda: False) monkeypatch.setattr(http, "_internet_on", lambda: False)
assert regclient.fetch_json_data(**api_kwargs) == result assert regclient.fetch_json_data(**api_kwargs) == result

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