diff --git a/.travis.yml b/.travis.yml index 07226606..5d9c8e3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ matrix: install: - git submodule update --init --recursive - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then curl -fsSL https://bootstrap.pypa.io/get-pip.py | sudo python; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then sudo pip install "tox==3.0.0"; else pip install -U tox; fi + - pip install -U tox # ChipKIT issue: install 32-bit support for GCC PIC32 - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install libc6-i386; fi diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index d1efd097..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "python.pythonPath": "${workspaceRoot}/.tox/develop/bin/python", - "python.formatting.provider": "yapf", - "files.exclude": { - "**/*.pyc": true, - "*.egg-info": true, - ".cache": true, - "build": true, - "dist": true - }, - "editor.rulers": [79], - "restructuredtext.builtDocumentationPath": "${workspaceRoot}/docs/_build/html", - "restructuredtext.confPath": "${workspaceRoot}/docs", - "restructuredtext.linter.executablePath": "${workspaceRoot}/.tox/docs/bin/restructuredtext-lint" -} diff --git a/Makefile b/Makefile index 6df42c42..bc8ba602 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ yapf: yapf --recursive --in-place platformio/ test: - py.test -v -s -n 3 --dist=loadscope tests --ignore tests/test_examples.py --ignore tests/test_pkgmanifest.py + py.test --verbose --capture=no --exitfirst -n 3 --dist=loadscope tests --ignore tests/test_examples.py --ignore tests/test_pkgmanifest.py before-commit: isort yapf lint test diff --git a/examples b/examples index 45ec933e..89963fed 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 45ec933e2858ae117c31d7783d66b83d96a6e609 +Subproject commit 89963fed9ebd0e7123d58dad5f7fc5020769ca63 diff --git a/platformio/builder/main.py b/platformio/builder/main.py index 8358d52e..e25d4af3 100644 --- a/platformio/builder/main.py +++ b/platformio/builder/main.py @@ -16,7 +16,7 @@ import base64 import json import sys from os import environ -from os.path import expanduser, join +from os.path import join from time import time from SCons.Script import ARGUMENTS # pylint: disable=import-error @@ -33,76 +33,29 @@ from platformio import util from platformio.compat import PY2, path_to_unicode from platformio.proc import get_pythonexe_path from platformio.project import helpers as project_helpers -from platformio.project.config import ProjectConfig AllowSubstExceptions(NameError) -# allow common variables from INI file -commonvars = Variables(None) -commonvars.AddVariables( +# append CLI arguments to build environment +clivars = Variables(None) +clivars.AddVariables( ("PLATFORM_MANIFEST",), ("BUILD_SCRIPT",), - ("EXTRA_SCRIPTS",), + ("PROJECT_CONFIG",), ("PIOENV",), ("PIOTEST",), - ("PIOPLATFORM",), - ("PIOFRAMEWORK",), - - # build options - ("BUILD_FLAGS",), - ("SRC_BUILD_FLAGS",), - ("BUILD_UNFLAGS",), - ("SRC_FILTER",), - - # library options - ("LIB_LDF_MODE",), - ("LIB_COMPAT_MODE",), - ("LIB_DEPS",), - ("LIB_IGNORE",), - ("LIB_EXTRA_DIRS",), - ("LIB_ARCHIVE",), - - # board options - ("BOARD",), - # deprecated options, use board_{object.path} instead - ("BOARD_MCU",), - ("BOARD_F_CPU",), - ("BOARD_F_FLASH",), - ("BOARD_FLASH_MODE",), - # end of deprecated options - - # upload options - ("UPLOAD_PORT",), - ("UPLOAD_PROTOCOL",), - ("UPLOAD_SPEED",), - ("UPLOAD_FLAGS",), - ("UPLOAD_RESETMETHOD",), - - # test options - ("TEST_BUILD_PROJECT_SRC",), - - # debug options - ("DEBUG_TOOL",), - ("DEBUG_SVD_PATH",), - + ("UPLOAD_PORT",) ) # yapf: disable -MULTILINE_VARS = [ - "EXTRA_SCRIPTS", "PIOFRAMEWORK", "BUILD_FLAGS", "SRC_BUILD_FLAGS", - "BUILD_UNFLAGS", "UPLOAD_FLAGS", "SRC_FILTER", "LIB_DEPS", "LIB_IGNORE", - "LIB_EXTRA_DIRS" -] - DEFAULT_ENV_OPTIONS = dict( tools=[ "ar", "gas", "gcc", "g++", "gnulink", "platformio", "pioplatform", - "piowinhooks", "piolib", "pioupload", "piomisc", "pioide" - ], # yapf: disable + "pioproject", "piowinhooks", "piolib", "pioupload", "piomisc", "pioide" + ], toolpath=[join(util.get_source_dir(), "builder", "tools")], - variables=commonvars, + variables=clivars, # Propagating External Environment - PIOVARIABLES=list(commonvars.keys()), ENV=environ, UNIX_TIME=int(time()), PROJECT_DIR=project_helpers.get_project_dir(), @@ -136,38 +89,22 @@ if not int(ARGUMENTS.get("PIOVERBOSE", 0)): env = DefaultEnvironment(**DEFAULT_ENV_OPTIONS) -# decode common variables -for k in list(commonvars.keys()): - if k in env: - env[k] = base64.b64decode(env[k]) - if isinstance(env[k], bytes): - env[k] = env[k].decode() - if k in MULTILINE_VARS: - env[k] = ProjectConfig.parse_multi_values(env[k]) - if env.GetOption('clean'): env.PioClean(env.subst("$BUILD_DIR")) env.Exit(0) elif not int(ARGUMENTS.get("PIOVERBOSE", 0)): print("Verbose mode can be enabled via `-v, --verbose` option") -# Handle custom variables from system environment -for var in ("BUILD_FLAGS", "SRC_BUILD_FLAGS", "SRC_FILTER", "EXTRA_SCRIPTS", - "UPLOAD_PORT", "UPLOAD_FLAGS", "LIB_EXTRA_DIRS"): - k = "PLATFORMIO_%s" % var - if k not in environ: - continue - if var in ("UPLOAD_PORT", ): - env[var] = environ.get(k) - continue - env.Append(**{var: ProjectConfig.parse_multi_values(environ.get(k))}) +# Load variables from CLI +for key in list(clivars.keys()): + if key in env: + env[key] = base64.b64decode(env[key]) + if isinstance(env[key], bytes): + env[key] = env[key].decode() -env.Prepend(LIBSOURCE_DIRS=env.get("LIB_EXTRA_DIRS", [])) -env['LIBSOURCE_DIRS'] = [ - expanduser(d) if d.startswith("~") else d for d in env['LIBSOURCE_DIRS'] -] - -env.LoadPioPlatform(commonvars) +env.GetProjectConfig().validate([env['PIOENV']], silent=True) +env.LoadProjectOptions() +env.LoadPioPlatform() env.SConscriptChdir(0) env.SConsignFile( diff --git a/platformio/builder/tools/pioide.py b/platformio/builder/tools/pioide.py index 12ebefba..4c42ee88 100644 --- a/platformio/builder/tools/pioide.py +++ b/platformio/builder/tools/pioide.py @@ -113,7 +113,7 @@ def _dump_defines(env): def _get_svd_path(env): - svd_path = env.subst("$DEBUG_SVD_PATH") + svd_path = env.GetProjectOption("debug_svd_path") if svd_path: return abspath(svd_path) @@ -139,8 +139,7 @@ def DumpIDEData(env, projenv): LINTCXXCOM = "$CXXFLAGS $CCFLAGS $CPPFLAGS" data = { - "libsource_dirs": - [env.subst(l) for l in env.get("LIBSOURCE_DIRS", [])], + "libsource_dirs": [env.subst(l) for l in env.GetLibSourceDirs()], "defines": _dump_defines(env), "includes": diff --git a/platformio/builder/tools/piolib.py b/platformio/builder/tools/piolib.py index b63dd0f9..dba7d108 100644 --- a/platformio/builder/tools/piolib.py +++ b/platformio/builder/tools/piolib.py @@ -23,8 +23,8 @@ import os import re import sys from glob import glob -from os.path import (basename, commonprefix, dirname, isdir, isfile, join, - realpath, sep) +from os.path import (basename, commonprefix, dirname, expanduser, isdir, + isfile, join, realpath, sep) import SCons.Scanner # pylint: disable=import-error from SCons.Script import ARGUMENTS # pylint: disable=import-error @@ -207,17 +207,18 @@ class LibBuilderBase(object): @property def lib_archive(self): - return self.env.get("LIB_ARCHIVE", "") != "false" + return self.env.GetProjectOption("lib_archive", True) @property def lib_ldf_mode(self): return self.validate_ldf_mode( - self.env.get("LIB_LDF_MODE", self.LDF_MODE_DEFAULT)) + self.env.GetProjectOption("lib_ldf_mode", self.LDF_MODE_DEFAULT)) @property def lib_compat_mode(self): return self.validate_compat_mode( - self.env.get("LIB_COMPAT_MODE", self.COMPAT_MODE_DEFAULT)) + self.env.GetProjectOption("lib_compat_mode", + self.COMPAT_MODE_DEFAULT)) @property def depbuilders(self): @@ -867,7 +868,7 @@ class ProjectAsLibBuilder(LibBuilderBase): pass def process_dependencies(self): # pylint: disable=too-many-branches - uris = self.env.get("LIB_DEPS", []) + uris = self.env.GetProjectOption("lib_deps", []) if not uris: return storage_dirs = [] @@ -907,6 +908,14 @@ class ProjectAsLibBuilder(LibBuilderBase): return result +def GetLibSourceDirs(env): + items = env.GetProjectOption("lib_extra_dirs", []) + items.extend(env['LIBSOURCE_DIRS']) + return [ + expanduser(item) if item.startswith("~") else item for item in items + ] + + def GetLibBuilders(env): # pylint: disable=too-many-branches if "__PIO_LIB_BUILDERS" in DefaultEnvironment(): @@ -920,7 +929,7 @@ def GetLibBuilders(env): # pylint: disable=too-many-branches def _check_lib_builder(lb): compat_mode = lb.lib_compat_mode - if lb.name in env.get("LIB_IGNORE", []): + if lb.name in env.GetProjectOption("lib_ignore", []): if verbose: sys.stderr.write("Ignored library %s\n" % lb.path) return None @@ -939,7 +948,7 @@ def GetLibBuilders(env): # pylint: disable=too-many-branches return True found_incompat = False - for libs_dir in env['LIBSOURCE_DIRS']: + for libs_dir in env.GetLibSourceDirs(): libs_dir = env.subst(libs_dir) if not isdir(libs_dir): continue @@ -1038,6 +1047,7 @@ def exists(_): def generate(env): + env.AddMethod(GetLibSourceDirs) env.AddMethod(GetLibBuilders) env.AddMethod(ConfigureProjectLibBuilder) return env diff --git a/platformio/builder/tools/piomisc.py b/platformio/builder/tools/piomisc.py index bfc41a76..3d434955 100644 --- a/platformio/builder/tools/piomisc.py +++ b/platformio/builder/tools/piomisc.py @@ -322,7 +322,7 @@ def ProcessTest(env): def GetExtraScripts(env, scope): items = [] - for item in env.get("EXTRA_SCRIPTS", []): + for item in env.GetProjectOption("extra_scripts", []): if scope == "post" and ":" not in item: items.append(item) elif item.startswith("%s:" % scope): diff --git a/platformio/builder/tools/pioplatform.py b/platformio/builder/tools/pioplatform.py index cc61a709..144f1f4b 100644 --- a/platformio/builder/tools/pioplatform.py +++ b/platformio/builder/tools/pioplatform.py @@ -14,7 +14,6 @@ from __future__ import absolute_import -import base64 import sys from os.path import isdir, isfile, join @@ -23,6 +22,7 @@ from SCons.Script import COMMAND_LINE_TARGETS # pylint: disable=import-error from platformio import exception, util from platformio.compat import WINDOWS from platformio.managers.platform import PlatformFactory +from platformio.project.config import ProjectOptions # pylint: disable=too-many-branches, too-many-locals @@ -33,10 +33,10 @@ def initPioPlatform(name): def PioPlatform(env): - variables = {} - for name in env['PIOVARIABLES']: - if name in env: - variables[name.lower()] = env[name] + variables = env.GetProjectOptions(as_dict=True) + if "framework" in variables: + # support PIO Core 3.0 dev/platforms + variables['pioframework'] = variables['framework'] p = initPioPlatform(env['PLATFORM_MANIFEST']) p.configure_default_packages(variables, COMMAND_LINE_TARGETS) return p @@ -63,7 +63,7 @@ def GetFrameworkScript(env, framework): return script_path -def LoadPioPlatform(env, variables): +def LoadPioPlatform(env): p = env.PioPlatform() installed_packages = p.get_installed_packages() @@ -92,36 +92,25 @@ def LoadPioPlatform(env, variables): env.Prepend(LIBPATH=[join(p.get_dir(), "ldscripts")]) if "BOARD" not in env: - # handle _MCU and _F_CPU variables for AVR native - for key, value in variables.UnknownVariables().items(): - if not key.startswith("BOARD_"): - continue - value = base64.b64decode(value) - if isinstance(value, bytes): - value = value.decode() - env.Replace(**{key.upper().replace("BUILD.", ""): value}) return - # update board manifest with a custom data + # update board manifest with overridden data from INI config board_config = env.BoardConfig() - for key, value in variables.UnknownVariables().items(): - if not key.startswith("BOARD_"): - continue - value = base64.b64decode(value) - if isinstance(value, bytes): - value = value.decode() - board_config.update(key.lower()[6:], value) + for option, value in env.GetProjectOptions(): + if option.startswith("board_"): + board_config.update(option.lower()[6:], value) - # update default environment variables - for key in list(variables.keys()): - if key in env or \ - not any([key.startswith("BOARD_"), key.startswith("UPLOAD_")]): + # load default variables from board config + for option_meta in ProjectOptions.values(): + if not option_meta.buildenvvar or option_meta.buildenvvar in env: continue - _opt, _val = key.lower().split("_", 1) - if _opt == "board": - _opt = "build" - if _val in board_config.get(_opt): - env.Replace(**{key: board_config.get("%s.%s" % (_opt, _val))}) + data_path = (option_meta.name[6:] + if option_meta.name.startswith("board_") else + option_meta.name.replace("_", ".")) + try: + env[option_meta.buildenvvar] = board_config.get(data_path) + except KeyError: + pass if "build.ldscript" in board_config: env.Replace(LDSCRIPT_PATH=board_config.get("build.ldscript")) @@ -165,7 +154,7 @@ def PrintConfiguration(env): data = [ "CURRENT(%s)" % board_config.get_debug_tool_name( - env.subst("$DEBUG_TOOL")) + env.GetProjectOption("debug_tool")) ] onboard = [] external = [] diff --git a/platformio/builder/tools/pioproject.py b/platformio/builder/tools/pioproject.py new file mode 100644 index 00000000..5797755d --- /dev/null +++ b/platformio/builder/tools/pioproject.py @@ -0,0 +1,49 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +from platformio.project.config import ProjectConfig, ProjectOptions + + +def GetProjectConfig(env): + return ProjectConfig.get_instance(env['PROJECT_CONFIG']) + + +def GetProjectOptions(env, as_dict=False): + return env.GetProjectConfig().items(env=env['PIOENV'], as_dict=as_dict) + + +def GetProjectOption(env, option, default=None): + return env.GetProjectConfig().get("env:" + env['PIOENV'], option, default) + + +def LoadProjectOptions(env): + for option, value in env.GetProjectOptions(): + option_meta = ProjectOptions.get("env." + option) + if not option_meta or not option_meta.buildenvvar: + continue + env[option_meta.buildenvvar] = value + + +def exists(_): + return True + + +def generate(env): + env.AddMethod(GetProjectConfig) + env.AddMethod(GetProjectOptions) + env.AddMethod(GetProjectOption) + env.AddMethod(LoadProjectOptions) + return env diff --git a/platformio/commands/device.py b/platformio/commands/device.py index c5bde2bd..992ad9b2 100644 --- a/platformio/commands/device.py +++ b/platformio/commands/device.py @@ -163,22 +163,18 @@ def device_list( # pylint: disable=too-many-branches "--environment", help="Load configuration from `platformio.ini` and specified environment") def device_monitor(**kwargs): # pylint: disable=too-many-branches - custom_monitor_flags = [] + env_options = {} try: env_options = get_project_options(kwargs['project_dir'], kwargs['environment']) - if "monitor_flags" in env_options: - custom_monitor_flags = ProjectConfig.parse_multi_values( - env_options['monitor_flags']) - if env_options: - for k in ("port", "speed", "rts", "dtr"): - k2 = "monitor_%s" % k - if k == "speed": - k = "baud" - if kwargs[k] is None and k2 in env_options: - kwargs[k] = env_options[k2] - if k != "port": - kwargs[k] = int(kwargs[k]) + for k in ("port", "speed", "rts", "dtr"): + k2 = "monitor_%s" % k + if k == "speed": + k = "baud" + if kwargs[k] is None and k2 in env_options: + kwargs[k] = env_options[k2] + if k != "port": + kwargs[k] = int(kwargs[k]) except exception.NotPlatformIOProject: pass @@ -187,12 +183,12 @@ def device_monitor(**kwargs): # pylint: disable=too-many-branches if len(ports) == 1: kwargs['port'] = ports[0]['port'] - sys.argv = ["monitor"] + custom_monitor_flags + sys.argv = ["monitor"] + env_options.get("monitor_flags", []) for k, v in kwargs.items(): if k in ("port", "baud", "rts", "dtr", "environment", "project_dir"): continue k = "--" + k.replace("_", "-") - if k in custom_monitor_flags: + if k in env_options.get("monitor_flags", []): continue if isinstance(v, bool): if v: diff --git a/platformio/commands/home/rpc/handlers/project.py b/platformio/commands/home/rpc/handlers/project.py index aee042e2..2b396094 100644 --- a/platformio/commands/home/rpc/handlers/project.py +++ b/platformio/commands/home/rpc/handlers/project.py @@ -41,13 +41,11 @@ class ProjectRPC(object): def _get_project_data(project_dir): data = {"boards": [], "envLibdepsDirs": [], "libExtraDirs": []} config = ProjectConfig(join(project_dir, "platformio.ini")) - config.validate(validate_options=False) + config.validate(silent=True) libdeps_dir = get_project_libdeps_dir() - if config.has_section("platformio") and \ - config.has_option("platformio", "lib_extra_dirs"): - data['libExtraDirs'].extend( - config.getlist("platformio", "lib_extra_dirs")) + data['libExtraDirs'].extend( + config.get("platformio", "lib_extra_dirs", [])) for section in config.sections(): if not section.startswith("env:"): @@ -55,9 +53,8 @@ class ProjectRPC(object): data['envLibdepsDirs'].append(join(libdeps_dir, section[4:])) if config.has_option(section, "board"): data['boards'].append(config.get(section, "board")) - if config.has_option(section, "lib_extra_dirs"): - data['libExtraDirs'].extend( - config.getlist(section, "lib_extra_dirs")) + data['libExtraDirs'].extend( + config.get(section, "lib_extra_dirs", [])) # skip non existing folders and resolve full path for key in ("envLibdepsDirs", "libExtraDirs"): @@ -232,11 +229,9 @@ class ProjectRPC(object): project_description = None try: config = ProjectConfig(join(project_dir, "platformio.ini")) - config.validate(validate_options=False) - if config.has_section("platformio") and \ - config.has_option("platformio", "description"): - project_description = config.get( - "platformio", "description") + config.validate(silent=True) + project_description = config.get("platformio", + "description") except exception.PlatformIOProjectException: continue diff --git a/platformio/commands/init.py b/platformio/commands/init.py index 7232a5ef..35e459bf 100644 --- a/platformio/commands/init.py +++ b/platformio/commands/init.py @@ -70,7 +70,6 @@ def cli( project_option, env_prefix, silent): - if not silent: if project_dir == getcwd(): click.secho( diff --git a/platformio/commands/lib.py b/platformio/commands/lib.py index ae00902f..63b89950 100644 --- a/platformio/commands/lib.py +++ b/platformio/commands/lib.py @@ -109,9 +109,8 @@ def cli(ctx, **options): continue storage_dir = join(libdeps_dir, env) ctx.meta[CTX_META_STORAGE_DIRS_KEY].append(storage_dir) - if config.has_option("env:" + env, "lib_deps"): - ctx.meta[CTX_META_STORAGE_LIBDEPS_KEY][ - storage_dir] = config.getlist("env:" + env, "lib_deps") + ctx.meta[CTX_META_STORAGE_LIBDEPS_KEY][storage_dir] = config.get( + "env:" + env, "lib_deps", []) @cli.command("install", short_help="Install library") @@ -175,8 +174,7 @@ def lib_install( # pylint: disable=too-many-arguments if project_environments and env not in project_environments: continue config.expand_interpolations = False - lib_deps = (config.getlist("env:" + env, "lib_deps") - if config.has_option("env:" + env, "lib_deps") else []) + lib_deps = config.get("env:" + env, "lib_deps", []) for library in libraries: if library in lib_deps: continue diff --git a/platformio/commands/run.py b/platformio/commands/run.py deleted file mode 100644 index 41fca7e0..00000000 --- a/platformio/commands/run.py +++ /dev/null @@ -1,352 +0,0 @@ -# Copyright (c) 2014-present PlatformIO -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from os import getcwd, makedirs -from os.path import getmtime, isdir, isfile, join -from time import time - -import click - -from platformio import exception, telemetry, util -from platformio.commands.device import device_monitor as cmd_device_monitor -from platformio.commands.lib import (CTX_META_STORAGE_DIRS_KEY, - CTX_META_STORAGE_LIBDEPS_KEY) -from platformio.commands.lib import lib_install as cmd_lib_install -from platformio.commands.platform import \ - platform_install as cmd_platform_install -from platformio.managers.platform import PlatformFactory -from platformio.project.config import ProjectConfig -from platformio.project.helpers import ( - calculate_project_hash, find_project_dir_above, get_project_build_dir, - get_project_dir, get_project_libdeps_dir) - -# pylint: disable=too-many-arguments,too-many-locals,too-many-branches - - -@click.command("run", short_help="Process project environments") -@click.option("-e", "--environment", multiple=True) -@click.option("-t", "--target", multiple=True) -@click.option("--upload-port") -@click.option( - "-d", - "--project-dir", - default=getcwd, - type=click.Path( - exists=True, - file_okay=True, - dir_okay=True, - writable=True, - resolve_path=True)) -@click.option( - "-c", - "--project-conf", - type=click.Path( - exists=True, - file_okay=True, - dir_okay=False, - readable=True, - resolve_path=True)) -@click.option("-s", "--silent", is_flag=True) -@click.option("-v", "--verbose", is_flag=True) -@click.option("--disable-auto-clean", is_flag=True) -@click.pass_context -def cli(ctx, environment, target, upload_port, project_dir, project_conf, - silent, verbose, disable_auto_clean): - # find project directory on upper level - if isfile(project_dir): - project_dir = find_project_dir_above(project_dir) - - with util.cd(project_dir): - # clean obsolete build dir - if not disable_auto_clean: - try: - _clean_build_dir(get_project_build_dir()) - except: # pylint: disable=bare-except - click.secho( - "Can not remove temporary directory `%s`. Please remove " - "it manually to avoid build issues" % - get_project_build_dir(force=True), - fg="yellow") - - config = ProjectConfig.get_instance( - project_conf or join(project_dir, "platformio.ini")) - config.validate(environment) - - _handle_legacy_libdeps(project_dir, config) - - results = [] - start_time = time() - default_envs = config.default_envs() - for envname in config.envs(): - skipenv = any([ - environment and envname not in environment, not environment - and default_envs and envname not in default_envs - ]) - if skipenv: - results.append((envname, None)) - continue - - if not silent and any( - status is not None for (_, status) in results): - click.echo() - - options = config.items(env=envname, as_dict=True) - if "piotest" not in options and "piotest" in ctx.meta: - options['piotest'] = ctx.meta['piotest'] - - ep = EnvironmentProcessor(ctx, envname, options, target, - upload_port, silent, verbose) - result = (envname, ep.process()) - results.append(result) - if result[1] and "monitor" in ep.get_build_targets() and \ - "nobuild" not in ep.get_build_targets(): - ctx.invoke( - cmd_device_monitor, - environment=environment[0] if environment else None) - - found_error = any(status is False for (_, status) in results) - - if (found_error or not silent) and len(results) > 1: - click.echo() - print_summary(results, start_time) - - if found_error: - raise exception.ReturnErrorCode(1) - return True - - -class EnvironmentProcessor(object): - - DEFAULT_DUMP_OPTIONS = ("platform", "framework", "board") - - IGNORE_BUILD_OPTIONS = [ - "test_transport", "test_filter", "test_ignore", "test_port", - "test_speed", "debug_port", "debug_init_cmds", "debug_extra_cmds", - "debug_server", "debug_init_break", "debug_load_cmd", - "debug_load_mode", "monitor_port", "monitor_speed", "monitor_rts", - "monitor_dtr" - ] - - REMAPED_OPTIONS = {"framework": "pioframework", "platform": "pioplatform"} - - def __init__( - self, # pylint: disable=R0913 - cmd_ctx, - name, - options, - targets, - upload_port, - silent, - verbose): - self.cmd_ctx = cmd_ctx - self.name = name - self.options = options - self.targets = targets - self.upload_port = upload_port - self.silent = silent - self.verbose = verbose - - def process(self): - terminal_width, _ = click.get_terminal_size() - start_time = time() - env_dump = [] - - for k, v in self.options.items(): - self.options[k] = self.options[k].strip() - if self.verbose or k in self.DEFAULT_DUMP_OPTIONS: - env_dump.append("%s: %s" % (k, ", ".join( - ProjectConfig.parse_multi_values(v)))) - - if not self.silent: - click.echo("Processing %s (%s)" % (click.style( - self.name, fg="cyan", bold=True), "; ".join(env_dump))) - click.secho("-" * terminal_width, bold=True) - - result = self._run() - is_error = result['returncode'] != 0 - - if self.silent and not is_error: - return True - - if is_error or "piotest_processor" not in self.cmd_ctx.meta: - print_header( - "[%s] Took %.2f seconds" % ( - (click.style("ERROR", fg="red", bold=True) if is_error else - click.style("SUCCESS", fg="green", bold=True)), - time() - start_time), - is_error=is_error) - - return not is_error - - def get_build_variables(self): - variables = {"pioenv": self.name} - if self.upload_port: - variables['upload_port'] = self.upload_port - for k, v in self.options.items(): - if k in self.REMAPED_OPTIONS: - k = self.REMAPED_OPTIONS[k] - if k in self.IGNORE_BUILD_OPTIONS: - continue - if k == "targets" or (k == "upload_port" and self.upload_port): - continue - variables[k] = v - return variables - - def get_build_targets(self): - targets = [] - if self.targets: - targets = [t for t in self.targets] - elif "targets" in self.options: - targets = self.options['targets'].split(", ") - return targets - - def _run(self): - if "platform" not in self.options: - raise exception.UndefinedEnvPlatform(self.name) - - build_vars = self.get_build_variables() - build_targets = self.get_build_targets() - - telemetry.on_run_environment(self.options, build_targets) - - # skip monitor target, we call it above - if "monitor" in build_targets: - build_targets.remove("monitor") - if "nobuild" not in build_targets: - # install dependent libraries - if "lib_install" in self.options: - _autoinstall_libdeps(self.cmd_ctx, self.name, [ - int(d.strip()) - for d in self.options['lib_install'].split(",") - if d.strip() - ], self.verbose) - if "lib_deps" in self.options: - _autoinstall_libdeps( - self.cmd_ctx, self.name, - ProjectConfig.parse_multi_values(self.options['lib_deps']), - self.verbose) - - try: - p = PlatformFactory.newPlatform(self.options['platform']) - except exception.UnknownPlatform: - self.cmd_ctx.invoke( - cmd_platform_install, - platforms=[self.options['platform']], - skip_default_package=True) - p = PlatformFactory.newPlatform(self.options['platform']) - - return p.run(build_vars, build_targets, self.silent, self.verbose) - - -def _handle_legacy_libdeps(project_dir, config): - legacy_libdeps_dir = join(project_dir, ".piolibdeps") - if (not isdir(legacy_libdeps_dir) - or legacy_libdeps_dir == get_project_libdeps_dir()): - return - if not config.has_section("env"): - config.add_section("env") - lib_extra_dirs = [] - if config.has_option("env", "lib_extra_dirs"): - lib_extra_dirs = config.getlist("env", "lib_extra_dirs") - lib_extra_dirs.append(legacy_libdeps_dir) - config.set("env", "lib_extra_dirs", lib_extra_dirs) - click.secho( - "DEPRECATED! A legacy library storage `{0}` has been found in a " - "project. \nPlease declare project dependencies in `platformio.ini`" - " file using `lib_deps` option and remove `{0}` folder." - "\nMore details -> http://docs.platformio.org/page/projectconf/" - "section_env_library.html#lib-deps".format(legacy_libdeps_dir), - fg="yellow") - - -def _autoinstall_libdeps(ctx, envname, libraries, verbose=False): - if not libraries: - return - libdeps_dir = join(get_project_libdeps_dir(), envname) - ctx.meta.update({ - CTX_META_STORAGE_DIRS_KEY: [libdeps_dir], - CTX_META_STORAGE_LIBDEPS_KEY: { - libdeps_dir: libraries - } - }) - try: - ctx.invoke(cmd_lib_install, silent=not verbose) - except exception.InternetIsOffline as e: - click.secho(str(e), fg="yellow") - - -def _clean_build_dir(build_dir): - # remove legacy ".pioenvs" folder - legacy_build_dir = join(get_project_dir(), ".pioenvs") - if isdir(legacy_build_dir) and legacy_build_dir != build_dir: - util.rmtree_(legacy_build_dir) - - structhash_file = join(build_dir, "structure.hash") - proj_hash = calculate_project_hash() - - # if project's config is modified - if (isdir(build_dir) and getmtime( - join(get_project_dir(), "platformio.ini")) > getmtime(build_dir)): - util.rmtree_(build_dir) - - # check project structure - if isdir(build_dir) and isfile(structhash_file): - with open(structhash_file) as f: - if f.read() == proj_hash: - return - util.rmtree_(build_dir) - - if not isdir(build_dir): - makedirs(build_dir) - - with open(structhash_file, "w") as f: - f.write(proj_hash) - - -def print_header(label, is_error=False, fg=None): - terminal_width, _ = click.get_terminal_size() - width = len(click.unstyle(label)) - half_line = "=" * int((terminal_width - width - 2) / 2) - click.secho( - "%s %s %s" % (half_line, label, half_line), fg=fg, err=is_error) - - -def print_summary(results, start_time): - print_header("[%s]" % click.style("SUMMARY")) - - envname_max_len = 0 - for (envname, _) in results: - if len(envname) > envname_max_len: - envname_max_len = len(envname) - - successed = True - for (envname, status) in results: - status_str = click.style("SUCCESS", fg="green") - if status is False: - successed = False - status_str = click.style("ERROR", fg="red") - elif status is None: - status_str = click.style("SKIP", fg="yellow") - - format_str = ( - "Environment {0:<" + str(envname_max_len + 9) + "}\t[{1}]") - click.echo( - format_str.format(click.style(envname, fg="cyan"), status_str), - err=status is False) - - print_header( - "[%s] Took %.2f seconds" % ( - (click.style("SUCCESS", fg="green", bold=True) if successed else - click.style("ERROR", fg="red", bold=True)), time() - start_time), - is_error=not successed) diff --git a/platformio/commands/run/__init__.py b/platformio/commands/run/__init__.py new file mode 100644 index 00000000..b9e69521 --- /dev/null +++ b/platformio/commands/run/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from platformio.commands.run.command import cli +from platformio.commands.run.helpers import print_header diff --git a/platformio/commands/run/command.py b/platformio/commands/run/command.py new file mode 100644 index 00000000..1913314b --- /dev/null +++ b/platformio/commands/run/command.py @@ -0,0 +1,119 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import getcwd +from os.path import isfile, join +from time import time + +import click + +from platformio import exception, util +from platformio.commands.device import device_monitor as cmd_device_monitor +from platformio.commands.run.helpers import ( + _clean_build_dir, _handle_legacy_libdeps, print_summary) +from platformio.commands.run.processor import EnvironmentProcessor +from platformio.project.config import ProjectConfig +from platformio.project.helpers import (find_project_dir_above, + get_project_build_dir) + +# pylint: disable=too-many-arguments,too-many-locals,too-many-branches + + +@click.command("run", short_help="Process project environments") +@click.option("-e", "--environment", multiple=True) +@click.option("-t", "--target", multiple=True) +@click.option("--upload-port") +@click.option( + "-d", + "--project-dir", + default=getcwd, + type=click.Path( + exists=True, + file_okay=True, + dir_okay=True, + writable=True, + resolve_path=True)) +@click.option( + "-c", + "--project-conf", + type=click.Path( + exists=True, + file_okay=True, + dir_okay=False, + readable=True, + resolve_path=True)) +@click.option("-s", "--silent", is_flag=True) +@click.option("-v", "--verbose", is_flag=True) +@click.option("--disable-auto-clean", is_flag=True) +@click.pass_context +def cli(ctx, environment, target, upload_port, project_dir, project_conf, + silent, verbose, disable_auto_clean): + # find project directory on upper level + if isfile(project_dir): + project_dir = find_project_dir_above(project_dir) + + with util.cd(project_dir): + # clean obsolete build dir + if not disable_auto_clean: + try: + _clean_build_dir(get_project_build_dir()) + except: # pylint: disable=bare-except + click.secho( + "Can not remove temporary directory `%s`. Please remove " + "it manually to avoid build issues" % + get_project_build_dir(force=True), + fg="yellow") + + config = ProjectConfig.get_instance( + project_conf or join(project_dir, "platformio.ini")) + config.validate(environment) + + _handle_legacy_libdeps(project_dir, config) + + results = [] + start_time = time() + default_envs = config.default_envs() + for envname in config.envs(): + skipenv = any([ + environment and envname not in environment, not environment + and default_envs and envname not in default_envs + ]) + if skipenv: + results.append((envname, None)) + continue + + if not silent and any( + status is not None for (_, status) in results): + click.echo() + + ep = EnvironmentProcessor(ctx, envname, config, target, + upload_port, silent, verbose) + result = (envname, ep.process()) + results.append(result) + + if result[1] and "monitor" in ep.get_build_targets() and \ + "nobuild" not in ep.get_build_targets(): + ctx.invoke( + cmd_device_monitor, + environment=environment[0] if environment else None) + + found_error = any(status is False for (_, status) in results) + + if (found_error or not silent) and len(results) > 1: + click.echo() + print_summary(results, start_time) + + if found_error: + raise exception.ReturnErrorCode(1) + return True diff --git a/platformio/commands/run/helpers.py b/platformio/commands/run/helpers.py new file mode 100644 index 00000000..fd1ac7e3 --- /dev/null +++ b/platformio/commands/run/helpers.py @@ -0,0 +1,127 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import makedirs +from os.path import getmtime, isdir, isfile, join +from time import time + +import click + +from platformio import exception, util +from platformio.commands.lib import (CTX_META_STORAGE_DIRS_KEY, + CTX_META_STORAGE_LIBDEPS_KEY) +from platformio.commands.lib import lib_install as cmd_lib_install +from platformio.project.helpers import ( + calculate_project_hash, get_project_dir, get_project_libdeps_dir) + + +def _handle_legacy_libdeps(project_dir, config): + legacy_libdeps_dir = join(project_dir, ".piolibdeps") + if (not isdir(legacy_libdeps_dir) + or legacy_libdeps_dir == get_project_libdeps_dir()): + return + if not config.has_section("env"): + config.add_section("env") + lib_extra_dirs = config.get("env", "lib_extra_dirs", []) + lib_extra_dirs.append(legacy_libdeps_dir) + config.set("env", "lib_extra_dirs", lib_extra_dirs) + click.secho( + "DEPRECATED! A legacy library storage `{0}` has been found in a " + "project. \nPlease declare project dependencies in `platformio.ini`" + " file using `lib_deps` option and remove `{0}` folder." + "\nMore details -> http://docs.platformio.org/page/projectconf/" + "section_env_library.html#lib-deps".format(legacy_libdeps_dir), + fg="yellow") + + +def _autoinstall_libdeps(ctx, envname, libraries, verbose=False): + if not libraries: + return + libdeps_dir = join(get_project_libdeps_dir(), envname) + ctx.meta.update({ + CTX_META_STORAGE_DIRS_KEY: [libdeps_dir], + CTX_META_STORAGE_LIBDEPS_KEY: { + libdeps_dir: libraries + } + }) + try: + ctx.invoke(cmd_lib_install, silent=not verbose) + except exception.InternetIsOffline as e: + click.secho(str(e), fg="yellow") + + +def _clean_build_dir(build_dir): + # remove legacy ".pioenvs" folder + legacy_build_dir = join(get_project_dir(), ".pioenvs") + if isdir(legacy_build_dir) and legacy_build_dir != build_dir: + util.rmtree_(legacy_build_dir) + + structhash_file = join(build_dir, "structure.hash") + proj_hash = calculate_project_hash() + + # if project's config is modified + if (isdir(build_dir) and getmtime( + join(get_project_dir(), "platformio.ini")) > getmtime(build_dir)): + util.rmtree_(build_dir) + + # check project structure + if isdir(build_dir) and isfile(structhash_file): + with open(structhash_file) as f: + if f.read() == proj_hash: + return + util.rmtree_(build_dir) + + if not isdir(build_dir): + makedirs(build_dir) + + with open(structhash_file, "w") as f: + f.write(proj_hash) + + +def print_header(label, is_error=False, fg=None): + terminal_width, _ = click.get_terminal_size() + width = len(click.unstyle(label)) + half_line = "=" * int((terminal_width - width - 2) / 2) + click.secho( + "%s %s %s" % (half_line, label, half_line), fg=fg, err=is_error) + + +def print_summary(results, start_time): + print_header("[%s]" % click.style("SUMMARY")) + + succeeded_nums = 0 + failed_nums = 0 + envname_max_len = max( + [len(click.style(envname, fg="cyan")) for (envname, _) in results]) + for (envname, status) in results: + if status is False: + failed_nums += 1 + status_str = click.style("FAILED", fg="red") + elif status is None: + status_str = click.style("IGNORED", fg="yellow") + else: + succeeded_nums += 1 + status_str = click.style("SUCCESS", fg="green") + + format_str = "Environment {0:<%d}\t[{1}]" % envname_max_len + click.echo( + format_str.format(click.style(envname, fg="cyan"), status_str), + err=status is False) + + print_header( + "%s%d succeeded in %.2f seconds" % + ("%d failed, " % failed_nums if failed_nums else "", succeeded_nums, + time() - start_time), + is_error=failed_nums, + fg="red" if failed_nums else "green") diff --git a/platformio/commands/run/processor.py b/platformio/commands/run/processor.py new file mode 100644 index 00000000..6d57f257 --- /dev/null +++ b/platformio/commands/run/processor.py @@ -0,0 +1,115 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from time import time + +import click + +from platformio import exception, telemetry +from platformio.commands.platform import \ + platform_install as cmd_platform_install +from platformio.commands.run.helpers import _autoinstall_libdeps, print_header +from platformio.managers.platform import PlatformFactory + +# pylint: disable=too-many-instance-attributes + + +class EnvironmentProcessor(object): + + DEFAULT_PRINT_OPTIONS = ("platform", "framework", "board") + + def __init__( # pylint: disable=too-many-arguments + self, cmd_ctx, name, config, targets, upload_port, silent, + verbose): + self.cmd_ctx = cmd_ctx + self.name = name + self.config = config + self.targets = targets + self.upload_port = upload_port + self.silent = silent + self.verbose = verbose + self.options = config.items(env=name, as_dict=True) + + def process(self): + terminal_width, _ = click.get_terminal_size() + start_time = time() + env_dump = [] + + for k, v in self.options.items(): + if self.verbose or k in self.DEFAULT_PRINT_OPTIONS: + env_dump.append( + "%s: %s" % (k, ", ".join(v) if isinstance(v, list) else v)) + + if not self.silent: + click.echo("Processing %s (%s)" % (click.style( + self.name, fg="cyan", bold=True), "; ".join(env_dump))) + click.secho("-" * terminal_width, bold=True) + + result = self._run_platform() + is_error = result['returncode'] != 0 + + if self.silent and not is_error: + return True + + if is_error or "piotest_processor" not in self.cmd_ctx.meta: + print_header( + "[%s] Took %.2f seconds" % ( + (click.style("ERROR", fg="red", bold=True) if is_error else + click.style("SUCCESS", fg="green", bold=True)), + time() - start_time), + is_error=is_error) + + return not is_error + + def get_build_variables(self): + variables = {"pioenv": self.name, "project_config": self.config.path} + if "piotest" in self.cmd_ctx.meta: + variables['piotest'] = self.cmd_ctx.meta['piotest'] + if self.upload_port: + # override upload port with a custom from CLI + variables['upload_port'] = self.upload_port + return variables + + def get_build_targets(self): + if self.targets: + return [t for t in self.targets] + return self.config.get("env:" + self.name, "targets", []) + + def _run_platform(self): + if "platform" not in self.options: + raise exception.UndefinedEnvPlatform(self.name) + + build_vars = self.get_build_variables() + build_targets = self.get_build_targets() + + telemetry.on_run_environment(self.options, build_targets) + + # skip monitor target, we call it above + if "monitor" in build_targets: + build_targets.remove("monitor") + if "nobuild" not in build_targets and "lib_deps" in self.options: + _autoinstall_libdeps( + self.cmd_ctx, self.name, + self.config.get("env:" + self.name, "lib_deps"), self.verbose) + + try: + p = PlatformFactory.newPlatform(self.options['platform']) + except exception.UnknownPlatform: + self.cmd_ctx.invoke( + cmd_platform_install, + platforms=[self.options['platform']], + skip_default_package=True) + p = PlatformFactory.newPlatform(self.options['platform']) + + return p.run(build_vars, build_targets, self.silent, self.verbose) diff --git a/platformio/commands/test/command.py b/platformio/commands/test/command.py index 5f41649b..87a84424 100644 --- a/platformio/commands/test/command.py +++ b/platformio/commands/test/command.py @@ -22,7 +22,7 @@ from time import time import click from platformio import exception, util -from platformio.commands.run import print_header +from platformio.commands.run.helpers import print_header from platformio.commands.test.embedded import EmbeddedTestProcessor from platformio.commands.test.native import NativeTestProcessor from platformio.project.config import ProjectConfig @@ -107,9 +107,8 @@ def cli( # pylint: disable=redefined-builtin # filter and ignore patterns patterns = dict(filter=list(filter), ignore=list(ignore)) for key in patterns: - if config.has_option(section, "test_%s" % key): - patterns[key].extend( - config.getlist(section, "test_%s" % key)) + patterns[key].extend( + config.get(section, "test_%s" % key, [])) skip_conditions = [ environment and envname not in environment, @@ -167,10 +166,11 @@ def cli( # pylint: disable=redefined-builtin passed_nums += 1 status_str = click.style("PASSED", fg="green") + format_str = "test/{:<%d} > {:<%d}\t[{}]" % (testname_max_len, + envname_max_len) click.echo( - ("test/{:<%d} > {:<%d}\t[{}]" % - (testname_max_len, envname_max_len)).format( - testname, click.style(envname, fg="cyan"), status_str), + format_str.format(testname, click.style(envname, fg="cyan"), + status_str), err=status is False) print_header( diff --git a/platformio/commands/test/embedded.py b/platformio/commands/test/embedded.py index 39e8555d..449763db 100644 --- a/platformio/commands/test/embedded.py +++ b/platformio/commands/test/embedded.py @@ -32,7 +32,8 @@ class EmbeddedTestProcessor(TestProcessorBase): target = ["__test"] if self.options['without_uploading']: target.append("checkprogsize") - self.build_or_upload(target) + if not self.build_or_upload(target): + return False if not self.options['without_uploading']: self.print_progress("Uploading... (2/3)") @@ -41,7 +42,8 @@ class EmbeddedTestProcessor(TestProcessorBase): target.append("nobuild") else: target.append("__test") - self.build_or_upload(target) + if not self.build_or_upload(target): + return False if self.options['without_testing']: return None diff --git a/platformio/commands/test/native.py b/platformio/commands/test/native.py index 70505e7b..7367094f 100644 --- a/platformio/commands/test/native.py +++ b/platformio/commands/test/native.py @@ -25,7 +25,8 @@ class NativeTestProcessor(TestProcessorBase): def process(self): if not self.options['without_building']: self.print_progress("Building... (1/2)") - self.build_or_upload(["__test"]) + if not self.build_or_upload(["__test"]): + return False if self.options['without_testing']: return None self.print_progress("Testing... (2/2)") diff --git a/platformio/commands/test/processor.py b/platformio/commands/test/processor.py index e5a4e32d..7b22f5ee 100644 --- a/platformio/commands/test/processor.py +++ b/platformio/commands/test/processor.py @@ -21,7 +21,7 @@ import click from platformio import exception from platformio.commands.run import cli as cmd_run -from platformio.commands.run import print_header +from platformio.commands.run.helpers import print_header from platformio.project.helpers import get_project_test_dir TRANSPORT_OPTIONS = { @@ -82,7 +82,7 @@ class TestProcessorBase(object): def __init__(self, cmd_ctx, testname, envname, options): self.cmd_ctx = cmd_ctx - self.cmd_ctx.meta['piotest_processor'] = True + self.cmd_ctx.meta['piotest_processor'] = True # FIXME self.test_name = testname self.options = options self.env_name = envname @@ -92,9 +92,10 @@ class TestProcessorBase(object): self._outputcpp_generated = False def get_transport(self): - transport = self.env_options.get("framework") if self.env_options.get("platform") == "native": transport = "native" + elif "framework" in self.env_options: + transport = self.env_options.get("framework")[0] if "test_transport" in self.env_options: transport = self.env_options['test_transport'] if transport not in TRANSPORT_OPTIONS: @@ -108,8 +109,9 @@ class TestProcessorBase(object): def print_progress(self, text, is_error=False): click.echo() print_header( - "[test/%s] %s" % (click.style( - self.test_name, fg="yellow", bold=True), text), + "[test/%s > %s] %s" % (click.style(self.test_name, fg="yellow"), + click.style(self.env_name, fg="cyan"), + text), is_error=is_error) def build_or_upload(self, target): @@ -118,19 +120,22 @@ class TestProcessorBase(object): self._outputcpp_generated = True if self.test_name != "*": - self.cmd_ctx.meta['piotest'] = self.test_name + self.cmd_ctx.meta['piotest'] = self.test_name # FIXME if not self.options['verbose']: click.echo("Please wait...") - return self.cmd_ctx.invoke( - cmd_run, - project_dir=self.options['project_dir'], - upload_port=self.options['upload_port'], - silent=not self.options['verbose'], - environment=[self.env_name], - disable_auto_clean="nobuild" in target, - target=target) + try: + return self.cmd_ctx.invoke( + cmd_run, + project_dir=self.options['project_dir'], + upload_port=self.options['upload_port'], + silent=not self.options['verbose'], + environment=[self.env_name], + disable_auto_clean="nobuild" in target, + target=target) + except exception.ReturnErrorCode: + return False def process(self): raise NotImplementedError diff --git a/platformio/managers/platform.py b/platformio/managers/platform.py index 8a5de2ef..81982219 100644 --- a/platformio/managers/platform.py +++ b/platformio/managers/platform.py @@ -29,6 +29,7 @@ from platformio.managers.core import get_core_package_dir from platformio.managers.package import BasePkgManager, PackageManager from platformio.proc import (BuildAsyncPipe, copy_pythonpath_to_osenv, exec_command, get_pythonexe_path) +from platformio.project.config import ProjectConfig from platformio.project.helpers import ( get_project_boards_dir, get_project_core_dir, get_project_packages_dir, get_project_platforms_dir) @@ -358,7 +359,12 @@ class PlatformRunMixin(object): assert isinstance(variables, dict) assert isinstance(targets, list) - self.configure_default_packages(variables, targets) + config = ProjectConfig.get_instance(variables['project_config']) + options = config.items(env=variables['pioenv'], as_dict=True) + if "framework" in options: + # support PIO Core 3.0 dev/platforms + options['pioframework'] = options['framework'] + self.configure_default_packages(options, targets) self.install_packages(silent=True) self.silent = silent @@ -611,12 +617,9 @@ class PlatformBase( # pylint: disable=too-many-public-methods def get_package_type(self, name): return self.packages[name].get("type") - def configure_default_packages(self, variables, targets): + def configure_default_packages(self, options, targets): # enable used frameworks - frameworks = variables.get("pioframework", []) - if not isinstance(frameworks, list): - frameworks = frameworks.split(", ") - for framework in frameworks: + for framework in options.get("framework", []): if not self.frameworks: continue framework = framework.lower().strip() diff --git a/platformio/project/config.py b/platformio/project/config.py index d67df78b..45e9380c 100644 --- a/platformio/project/config.py +++ b/platformio/project/config.py @@ -21,6 +21,7 @@ from os.path import isfile import click from platformio import exception +from platformio.project.options import ProjectOptions try: import ConfigParser as ConfigParser @@ -39,98 +40,6 @@ CONFIG_HEADER = """;PlatformIO Project Configuration File """ -KNOWN_PLATFORMIO_OPTIONS = [ - "description", - "env_default", - "extra_configs", - - # Dirs - "core_dir", - "globallib_dir", - "platforms_dir", - "packages_dir", - "cache_dir", - "workspace_dir", - "build_dir", - "libdeps_dir", - "lib_dir", - "include_dir", - "src_dir", - "test_dir", - "boards_dir", - "data_dir" -] - -KNOWN_ENV_OPTIONS = [ - # Generic - "platform", - "framework", - "board", - "targets", - - # Build - "build_flags", - "src_build_flags", - "build_unflags", - "src_filter", - - # Upload - "upload_port", - "upload_protocol", - "upload_speed", - "upload_flags", - "upload_resetmethod", - - # Monitor - "monitor_port", - "monitor_speed", - "monitor_rts", - "monitor_dtr", - "monitor_flags", - - # Library - "lib_deps", - "lib_ignore", - "lib_extra_dirs", - "lib_ldf_mode", - "lib_compat_mode", - "lib_archive", - - # Test - "piotest", - "test_filter", - "test_ignore", - "test_port", - "test_speed", - "test_transport", - "test_build_project_src", - - # Debug - "debug_tool", - "debug_init_break", - "debug_init_cmds", - "debug_extra_cmds", - "debug_load_cmd", - "debug_load_mode", - "debug_server", - "debug_port", - "debug_svd_path", - - # Other - "extra_scripts" -] - -RENAMED_OPTIONS = { - "lib_use": "lib_deps", - "lib_force": "lib_deps", - "extra_script": "extra_scripts", - "monitor_baud": "monitor_speed", - "board_mcu": "board_build.mcu", - "board_f_cpu": "board_build.f_cpu", - "board_f_flash": "board_build.f_flash", - "board_flash_mode": "board_build.flash_mode" -} - class ProjectConfig(object): @@ -191,11 +100,7 @@ class ProjectConfig(object): return # load extra configs - if (not self._parser.has_section("platformio") - or not self._parser.has_option("platformio", "extra_configs")): - return - extra_configs = self.getlist("platformio", "extra_configs") - for pattern in extra_configs: + for pattern in self.get("platformio", "extra_configs", []): for item in glob.glob(pattern): self.read(item) @@ -212,6 +117,14 @@ class ProjectConfig(object): if option not in options: options.append(option) + # handle system environment variables + scope = section.split(":", 1)[0] + for option_meta in ProjectOptions.values(): + if option_meta.scope != scope or option_meta.name in options: + continue + if option_meta.sysenvvar and option_meta.sysenvvar in os.environ: + options.append(option_meta.name) + return options def has_option(self, section, option): @@ -239,39 +152,84 @@ class ProjectConfig(object): value = "\n" + value # start from a new line self._parser.set(section, option, value) - def get(self, section, option): + def getraw(self, section, option): if not self.expand_interpolations: return self._parser.get(section, option) try: value = self._parser.get(section, option) - except ConfigParser.NoOptionError: + except ConfigParser.NoOptionError as e: + if not section.startswith("env:"): + raise e value = self._parser.get("env", option) - except ConfigParser.Error as e: - raise exception.InvalidProjectConf(self.path, str(e)) if "${" not in value or "}" not in value: return value - return self.VARTPL_RE.sub(self._re_sub_handler, value) + return self.VARTPL_RE.sub(self._re_interpolation_handler, value) - def _re_sub_handler(self, match): + def _re_interpolation_handler(self, match): section, option = match.group(1), match.group(2) if section == "sysenv": return os.getenv(option) - return self.get(section, option) + return self.getraw(section, option) - def getlist(self, section, option): - return self.parse_multi_values(self.get(section, option)) + def get(self, section, option, default=None): + value = None + try: + value = self.getraw(section, option) + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): + pass # handle value from system environment + except ConfigParser.Error as e: + raise exception.InvalidProjectConf(self.path, str(e)) + + option_meta = ProjectOptions.get( + "%s.%s" % (section.split(":", 1)[0], option)) + if not option_meta: + return value or default + + if value and option_meta.multiple: + value = self.parse_multi_values(value) + + if option_meta.sysenvvar: + envvar_value = os.getenv(option_meta.sysenvvar) + if not envvar_value and option_meta.oldnames: + for oldoption in option_meta.oldnames: + envvar_value = os.getenv("PLATFORMIO_" + oldoption.upper()) + if envvar_value: + break + if envvar_value and option_meta.multiple: + value = value or [] + value.extend(self.parse_multi_values(envvar_value)) + elif envvar_value and not value: + value = envvar_value + + # option is not specified by user + if value is None: + return default + + return self._covert_value(value, option_meta.type) + + @staticmethod + def _covert_value(value, to_type): + items = value + if not isinstance(value, (list, tuple)): + items = [value] + for i, v in enumerate(items): + if to_type == bool: + items[i] = v in ("1", "true", "yes", "y") + elif to_type == int: + items[i] = int(v) + elif to_type == float: + items[i] = float(v) + return items if isinstance(value, (list, tuple)) else items[0] def envs(self): return [s[4:] for s in self._parser.sections() if s.startswith("env:")] def default_envs(self): - if not self._parser.has_option("platformio", "env_default"): - return [] - return self.getlist("platformio", "env_default") + return self.get("platformio", "env_default", []) - def validate(self, envs=None, validate_options=True): + def validate(self, envs=None, silent=False): if not isfile(self.path): raise exception.NotPlatformIOProject(self.path) # check envs @@ -283,74 +241,65 @@ class ProjectConfig(object): if unknown: raise exception.UnknownEnvNames(", ".join(unknown), ", ".join(known)) - return self.validate_options() if validate_options else True + return self.validate_options(silent) - def validate_options(self): - return (self._validate_platformio_options() - and self._validate_env_options()) - - def _validate_platformio_options(self): - if not self._parser.has_section("platformio"): - return True - warnings = set() - - # legacy `lib_extra_dirs` - if self._parser.has_option("platformio", "lib_extra_dirs"): + def validate_options(self, silent=False): + warnings = [] + # legacy `lib_extra_dirs` in [platformio] + if (self._parser.has_section("platformio") + and self._parser.has_option("platformio", "lib_extra_dirs")): if not self._parser.has_section("env"): self._parser.add_section("env") self._parser.set("env", "lib_extra_dirs", self._parser.get("platformio", "lib_extra_dirs")) self._parser.remove_option("platformio", "lib_extra_dirs") - warnings.add( - "`lib_extra_dirs` option is deprecated in section " - "`platformio`! Please move it to global `env` section") + warnings.append( + "`lib_extra_dirs` configuration option is deprecated in " + "section [platformio]! Please move it to global `env` section") - unknown = set(k for k, _ in self.items("platformio")) - set( - KNOWN_PLATFORMIO_OPTIONS) - if unknown: - warnings.add( - "Ignore unknown `%s` options in section `[platformio]`" % - ", ".join(unknown)) + warnings.extend(self._validate_unknown_options()) - for warning in warnings: - click.secho("Warning! %s" % warning, fg="yellow") + if not silent: + for warning in warnings: + click.secho("Warning! %s" % warning, fg="yellow") - return True + return warnings - def _validate_env_options(self): - warnings = set() + def _validate_unknown_options(self): + warnings = [] + renamed_options = {} + for option in ProjectOptions.values(): + if option.oldnames: + renamed_options.update( + {name: option.name + for name in option.oldnames}) for section in self._parser.sections(): - if section != "env" and not section.startswith("env:"): - continue for option in self._parser.options(section): # obsolete - if option in RENAMED_OPTIONS: - warnings.add( - "`%s` option in section `[%s]` is deprecated and will " - "be removed in the next release! Please use `%s` " - "instead" % (option, section, RENAMED_OPTIONS[option])) + if option in renamed_options: + warnings.append( + "`%s` configuration option in section [%s] is " + "deprecated and will be removed in the next release! " + "Please use `%s` instead" % (option, section, + renamed_options[option])) # rename on-the-fly - self._parser.set(section, RENAMED_OPTIONS[option], + self._parser.set(section, renamed_options[option], self._parser.get(section, option)) self._parser.remove_option(section, option) continue # unknown + scope = section.split(":", 1)[0] unknown_conditions = [ - option not in KNOWN_ENV_OPTIONS, - not option.startswith("custom_"), - not option.startswith("board_") + ("%s.%s" % (scope, option)) not in ProjectOptions, + scope != "env" or + not option.startswith(("custom_", "board_")) ] # yapf: disable if all(unknown_conditions): - warnings.add( - "Detected non-PlatformIO `%s` option in `[%s]` section" - % (option, section)) - - for warning in warnings: - click.secho("Warning! %s" % warning, fg="yellow") - - return True + warnings.append("Ignore unknown configuration option `%s` " + "in section [%s]" % (option, section)) + return warnings def to_json(self): result = {} diff --git a/platformio/project/helpers.py b/platformio/project/helpers.py index a2acce4d..6106b0a1 100644 --- a/platformio/project/helpers.py +++ b/platformio/project/helpers.py @@ -15,7 +15,7 @@ import os from hashlib import sha1 from os import walk -from os.path import (abspath, dirname, expanduser, isdir, isfile, join, +from os.path import (dirname, expanduser, isdir, isfile, join, realpath, splitdrive) from platformio import __version__ @@ -44,46 +44,31 @@ def find_project_dir_above(path): def get_project_optional_dir(name, default=None): - paths = None + project_dir = get_project_dir() + config = ProjectConfig.get_instance(join(project_dir, "platformio.ini")) + optional_dir = config.get("platformio", name) - # check for system environment variable - var_name = "PLATFORMIO_%s" % name.upper() - if var_name in os.environ: - paths = os.getenv(var_name) - - config = ProjectConfig.get_instance( - join(get_project_dir(), "platformio.ini")) - if (config.has_section("platformio") - and config.has_option("platformio", name)): - paths = config.get("platformio", name) - - if not paths: + if not optional_dir: return default - items = [] - for item in paths.split(", "): - if item.startswith("~"): - item = expanduser(item) - items.append(abspath(item)) - paths = ", ".join(items) - - while "$PROJECT_HASH" in paths: - project_dir = get_project_dir() - paths = paths.replace( + if "$PROJECT_HASH" in optional_dir: + optional_dir = optional_dir.replace( "$PROJECT_HASH", sha1(project_dir if PY2 else project_dir.encode()).hexdigest() [:10]) - return paths + if optional_dir.startswith("~"): + optional_dir = expanduser(optional_dir) + + return realpath(optional_dir) def get_project_core_dir(): + default = join(expanduser("~"), ".platformio") core_dir = get_project_optional_dir( - "core_dir", - get_project_optional_dir("home_dir", - join(expanduser("~"), ".platformio"))) + "core_dir", get_project_optional_dir("home_dir", default)) win_core_dir = None - if WINDOWS: + if WINDOWS and core_dir == default: win_core_dir = splitdrive(core_dir)[0] + "\\.platformio" if isdir(win_core_dir): core_dir = win_core_dir @@ -91,10 +76,12 @@ def get_project_core_dir(): if not isdir(core_dir): try: os.makedirs(core_dir) - except: # pylint: disable=bare-except + except OSError as e: if win_core_dir: os.makedirs(win_core_dir) core_dir = win_core_dir + else: + raise e assert isdir(core_dir) return core_dir diff --git a/platformio/project/options.py b/platformio/project/options.py new file mode 100644 index 00000000..b4bd705a --- /dev/null +++ b/platformio/project/options.py @@ -0,0 +1,198 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=redefined-builtin, too-many-arguments + +from collections import OrderedDict, namedtuple + +ConfigOptionClass = namedtuple("ConfigOption", [ + "scope", "name", "type", "multiple", "sysenvvar", "buildenvvar", "oldnames" +]) + + +def ConfigOption(scope, + name, + type=str, + multiple=False, + sysenvvar=None, + buildenvvar=None, + oldnames=None): + return ConfigOptionClass(scope, name, type, multiple, sysenvvar, + buildenvvar, oldnames) + + +def ConfigPlatformioOption(*args, **kwargs): + return ConfigOption("platformio", *args, **kwargs) + + +def ConfigEnvOption(*args, **kwargs): + return ConfigOption("env", *args, **kwargs) + + +ProjectOptions = OrderedDict([ + ("%s.%s" % (option.scope, option.name), option) for option in [ + # + # [platformio] + # + ConfigPlatformioOption(name="description"), + ConfigPlatformioOption( + name="env_default", + multiple=True, + sysenvvar="PLATFORMIO_ENV_DEFAULT"), + ConfigPlatformioOption(name="extra_configs", multiple=True), + + # Dirs + ConfigPlatformioOption( + name="core_dir", + oldnames=["home_dir"], + sysenvvar="PLATFORMIO_CORE_DIR"), + ConfigPlatformioOption( + name="globallib_dir", sysenvvar="PLATFORMIO_GLOBALLIB_DIR"), + ConfigPlatformioOption( + name="platforms_dir", sysenvvar="PLATFORMIO_PLATFORMS_DIR"), + ConfigPlatformioOption( + name="packages_dir", sysenvvar="PLATFORMIO_PACKAGES_DIR"), + ConfigPlatformioOption( + name="cache_dir", sysenvvar="PLATFORMIO_CACHE_DIR"), + ConfigPlatformioOption( + name="workspace_dir", sysenvvar="PLATFORMIO_WORKSPACE_DIR"), + ConfigPlatformioOption( + name="build_dir", sysenvvar="PLATFORMIO_BUILD_DIR"), + ConfigPlatformioOption( + name="libdeps_dir", sysenvvar="PLATFORMIO_LIBDEPS_DIR"), + ConfigPlatformioOption(name="lib_dir", sysenvvar="PLATFORMIO_LIB_DIR"), + ConfigPlatformioOption( + name="include_dir", sysenvvar="PLATFORMIO_INCLUDE_DIR"), + ConfigPlatformioOption(name="src_dir", sysenvvar="PLATFORMIO_SRC_DIR"), + ConfigPlatformioOption( + name="test_dir", sysenvvar="PLATFORMIO_TEST_DIR"), + ConfigPlatformioOption( + name="boards_dir", sysenvvar="PLATFORMIO_BOARDS_DIR"), + ConfigPlatformioOption( + name="data_dir", sysenvvar="PLATFORMIO_DATA_DIR"), + + # + # [env] + # + + # Generic + ConfigEnvOption(name="platform", buildenvvar="PIOPLATFORM"), + ConfigEnvOption( + name="framework", multiple=True, buildenvvar="PIOFRAMEWORK"), + ConfigEnvOption(name="targets", multiple=True), + + # Board + ConfigEnvOption(name="board", buildenvvar="BOARD"), + ConfigEnvOption( + name="board_build.mcu", + oldnames=["board_mcu"], + buildenvvar="BOARD_MCU"), + ConfigEnvOption( + name="board_build.f_cpu", + oldnames=["board_f_cpu"], + buildenvvar="BOARD_F_CPU"), + ConfigEnvOption( + name="board_build.f_flash", + oldnames=["board_f_flash"], + buildenvvar="BOARD_F_FLASH"), + ConfigEnvOption( + name="board_build.flash_mode", + oldnames=["board_flash_mode"], + buildenvvar="BOARD_FLASH_MODE"), + + # Build + ConfigEnvOption( + name="build_flags", + multiple=True, + sysenvvar="PLATFORMIO_BUILD_FLAGS", + buildenvvar="BUILD_FLAGS"), + ConfigEnvOption( + name="src_build_flags", + multiple=True, + sysenvvar="PLATFORMIO_SRC_BUILD_FLAGS", + buildenvvar="SRC_BUILD_FLAGS"), + ConfigEnvOption( + name="build_unflags", + multiple=True, + sysenvvar="PLATFORMIO_BUILD_UNFLAGS", + buildenvvar="BUILD_UNFLAGS"), + ConfigEnvOption( + name="src_filter", + multiple=True, + sysenvvar="PLATFORMIO_SRC_FILTER", + buildenvvar="SRC_FILTER"), + + # Upload + ConfigEnvOption( + name="upload_port", + sysenvvar="PLATFORMIO_UPLOAD_PORT", + buildenvvar="UPLOAD_PORT"), + ConfigEnvOption(name="upload_protocol", buildenvvar="UPLOAD_PROTOCOL"), + ConfigEnvOption(name="upload_speed", buildenvvar="UPLOAD_SPEED"), + ConfigEnvOption( + name="upload_flags", + multiple=True, + sysenvvar="PLATFORMIO_UPLOAD_FLAGS", + buildenvvar="UPLOAD_FLAGS"), + ConfigEnvOption( + name="upload_resetmethod", buildenvvar="UPLOAD_RESETMETHOD"), + + # Monitor + ConfigEnvOption(name="monitor_port"), + ConfigEnvOption(name="monitor_speed", oldnames=["monitor_baud"]), + ConfigEnvOption(name="monitor_rts"), + ConfigEnvOption(name="monitor_dtr"), + ConfigEnvOption(name="monitor_flags", multiple=True), + + # Library + ConfigEnvOption( + name="lib_deps", + oldnames=["lib_use", "lib_force", "lib_install"], + multiple=True), + ConfigEnvOption(name="lib_ignore", multiple=True), + ConfigEnvOption( + name="lib_extra_dirs", + multiple=True, + sysenvvar="PLATFORMIO_LIB_EXTRA_DIRS"), + ConfigEnvOption(name="lib_ldf_mode"), + ConfigEnvOption(name="lib_compat_mode"), + ConfigEnvOption(name="lib_archive", type=bool), + + # Test + ConfigEnvOption(name="test_filter", multiple=True), + ConfigEnvOption(name="test_ignore", multiple=True), + ConfigEnvOption(name="test_port"), + ConfigEnvOption(name="test_speed"), + ConfigEnvOption(name="test_transport"), + ConfigEnvOption(name="test_build_project_src"), + + # Debug + ConfigEnvOption(name="debug_tool"), + ConfigEnvOption(name="debug_init_break"), + ConfigEnvOption(name="debug_init_cmds", multiple=True), + ConfigEnvOption(name="debug_extra_cmds", multiple=True), + ConfigEnvOption(name="debug_load_cmd"), + ConfigEnvOption(name="debug_load_mode"), + ConfigEnvOption(name="debug_server"), + ConfigEnvOption(name="debug_port"), + ConfigEnvOption(name="debug_svd_path"), + + # Other + ConfigEnvOption( + name="extra_scripts", + oldnames=["extra_script"], + multiple=True, + sysenvvar="PLATFORMIO_EXTRA_SCRIPTS") + ] +]) diff --git a/platformio/telemetry.py b/platformio/telemetry.py index 7564a03d..071db390 100644 --- a/platformio/telemetry.py +++ b/platformio/telemetry.py @@ -310,12 +310,15 @@ def measure_ci(): def on_run_environment(options, targets): - opts = [ - "%s=%s" % (opt, value.replace("\n", ", ") if "\n" in value else value) - for opt, value in sorted(options.items()) - ] + non_sensative_values = ["board", "platform", "framework"] + safe_options = [] + for key, value in sorted(options.items()): + if key in non_sensative_values: + safe_options.append("%s=%s" % (key, value)) + else: + safe_options.append(key) targets = [t.title() for t in targets or ["run"]] - on_event("Env", " ".join(targets), "&".join(opts)) + on_event("Env", " ".join(targets), "&".join(safe_options)) def on_event(category, action, label=None, value=None, screen_name=None): diff --git a/tests/commands/test_ci.py b/tests/commands/test_ci.py index 67ff4420..bce43700 100644 --- a/tests/commands/test_ci.py +++ b/tests/commands/test_ci.py @@ -20,7 +20,7 @@ from platformio.commands.lib import cli as cmd_lib def test_ci_empty(clirunner): result = clirunner.invoke(cmd_ci) - assert result.exit_code == 2 + assert result.exit_code != 0 assert "Invalid value: Missing argument 'src'" in result.output diff --git a/tests/commands/test_init.py b/tests/commands/test_init.py index 800a0b47..7c2e4a48 100644 --- a/tests/commands/test_init.py +++ b/tests/commands/test_init.py @@ -16,7 +16,7 @@ import json from os import getcwd, makedirs from os.path import getsize, isdir, isfile, join -from platformio import exception, util +from platformio import exception from platformio.commands.boards import cli as cmd_boards from platformio.commands.init import cli as cmd_init from platformio.project.config import ProjectConfig @@ -109,13 +109,13 @@ def test_init_special_board(clirunner, validate_cliresult): config = ProjectConfig(join(getcwd(), "platformio.ini")) config.validate() - expected_result = [("platform", str(boards[0]['platform'])), - ("framework", - str(boards[0]['frameworks'][0])), ("board", "uno")] + expected_result = dict(platform=str(boards[0]['platform']), + board="uno", + framework=[str(boards[0]['frameworks'][0])]) assert config.has_section("env:uno") - assert not set(expected_result).symmetric_difference( - set(config.items("env:uno"))) + assert sorted(config.items(env="uno", as_dict=True).items()) == sorted( + expected_result.items()) def test_init_enable_auto_uploading(clirunner, validate_cliresult): @@ -126,11 +126,13 @@ def test_init_enable_auto_uploading(clirunner, validate_cliresult): validate_pioproject(getcwd()) config = ProjectConfig(join(getcwd(), "platformio.ini")) config.validate() - expected_result = [("platform", "atmelavr"), ("framework", "arduino"), - ("board", "uno"), ("targets", "upload")] + expected_result = dict(targets=["upload"], + platform="atmelavr", + board="uno", + framework=["arduino"]) assert config.has_section("env:uno") - assert not set(expected_result).symmetric_difference( - set(config.items("env:uno"))) + assert sorted(config.items(env="uno", as_dict=True).items()) == sorted( + expected_result.items()) def test_init_custom_framework(clirunner, validate_cliresult): @@ -141,11 +143,13 @@ def test_init_custom_framework(clirunner, validate_cliresult): validate_pioproject(getcwd()) config = ProjectConfig(join(getcwd(), "platformio.ini")) config.validate() - expected_result = [("platform", "teensy"), ("framework", "mbed"), - ("board", "teensy31")] + expected_result = dict(platform="teensy", + board="teensy31", + framework=["mbed"]) assert config.has_section("env:teensy31") - assert not set(expected_result).symmetric_difference( - set(config.items("env:teensy31"))) + assert sorted(config.items(env="teensy31", + as_dict=True).items()) == sorted( + expected_result.items()) def test_init_incorrect_board(clirunner): diff --git a/tests/test_projectconf.py b/tests/test_projectconf.py index d58ee0ce..8717e8da 100644 --- a/tests/test_projectconf.py +++ b/tests/test_projectconf.py @@ -14,7 +14,9 @@ import os -from platformio.project.config import ProjectConfig +import pytest + +from platformio.project.config import ConfigParser, ProjectConfig BASE_CONFIG = """ [platformio] @@ -26,7 +28,9 @@ extra_configs = # global options per [env:*] [env] monitor_speed = 115200 -lib_deps = Lib1, Lib2 +lib_deps = + Lib1 + Lib2 lib_ignore = ${custom.lib_ignore} [custom] @@ -46,6 +50,7 @@ build_flags = ${custom.lib_flags} ${custom.debug_flags} [env:extra_2] build_flags = ${custom.debug_flags} ${custom.extra_flags} lib_ignore = ${env.lib_ignore}, Lib3 +upload_port = /dev/extra_2/port """ EXTRA_DEBUG_CONFIG = """ @@ -58,7 +63,7 @@ build_flags = -Og """ -def test_parser(tmpdir): +def test_real_config(tmpdir): tmpdir.join("platformio.ini").write(BASE_CONFIG) tmpdir.join("extra_envs.ini").write(EXTRA_ENVS_CONFIG) tmpdir.join("extra_debug.ini").write(EXTRA_DEBUG_CONFIG) @@ -68,6 +73,16 @@ def test_parser(tmpdir): config = ProjectConfig(tmpdir.join("platformio.ini").strpath) assert config + # unknown section + with pytest.raises(ConfigParser.NoSectionError): + config.getraw("unknown_section", "unknown_option") + # unknown option + with pytest.raises(ConfigParser.NoOptionError): + config.getraw("custom", "unknown_option") + # unknown option even if exists in [env] + with pytest.raises(ConfigParser.NoOptionError): + config.getraw("platformio", "monitor_speed") + # sections assert config.sections() == [ "platformio", "env", "custom", "env:base", "env:extra_1", "env:extra_2" @@ -87,30 +102,76 @@ def test_parser(tmpdir): assert not config.has_option("custom", "monitor_speed") # sysenv - assert config.get("custom", "extra_flags") == "" + assert config.get("custom", "extra_flags") is None + assert config.get("env:base", "build_flags") == ["-D DEBUG=1"] + assert config.get("env:base", "upload_port") is None + assert config.get("env:extra_2", "upload_port") == "/dev/extra_2/port" + os.environ["PLATFORMIO_BUILD_FLAGS"] = "-DSYSENVDEPS1 -DSYSENVDEPS2" + os.environ["PLATFORMIO_UPLOAD_PORT"] = "/dev/sysenv/port" os.environ["__PIO_TEST_CNF_EXTRA_FLAGS"] = "-L /usr/local/lib" assert config.get("custom", "extra_flags") == "-L /usr/local/lib" + assert config.get("env:base", "build_flags") == [ + "-D DEBUG=1 -L /usr/local/lib", "-DSYSENVDEPS1 -DSYSENVDEPS2" + ] + assert config.get("env:base", "upload_port") == "/dev/sysenv/port" + assert config.get("env:extra_2", "upload_port") == "/dev/extra_2/port" + + # getraw + assert config.getraw("env:extra_1", "lib_deps") == "\nLib1\nLib2" + assert config.getraw("env:extra_1", "build_flags") == "-lc -lm -D DEBUG=1" # get assert config.get("custom", "debug_flags") == "-D DEBUG=1" - assert config.get("env:extra_1", "build_flags") == "-lc -lm -D DEBUG=1" - assert config.get("env:extra_2", "build_flags") == "-Og" + assert config.get("env:extra_1", "build_flags") == [ + "-lc -lm -D DEBUG=1", "-DSYSENVDEPS1 -DSYSENVDEPS2" + ] + assert config.get("env:extra_2", "build_flags") == [ + "-Og", "-DSYSENVDEPS1 -DSYSENVDEPS2"] assert config.get("env:extra_2", "monitor_speed") == "115200" - assert config.get("env:base", - "build_flags") == ("-D DEBUG=1 -L /usr/local/lib") + assert config.get("env:base", "build_flags") == ([ + "-D DEBUG=1 -L /usr/local/lib", "-DSYSENVDEPS1 -DSYSENVDEPS2" + ]) # items - assert config.items("custom") == [("debug_flags", "-D DEBUG=1"), - ("lib_flags", "-lc -lm"), - ("extra_flags", "-L /usr/local/lib"), - ("lib_ignore", "LibIgnoreCustom")] - assert config.items(env="extra_1") == [("build_flags", - "-lc -lm -D DEBUG=1"), - ("monitor_speed", "115200"), - ("lib_deps", "Lib1, Lib2"), - ("lib_ignore", "LibIgnoreCustom")] - assert config.items(env="extra_2") == [("build_flags", "-Og"), - ("lib_ignore", - "LibIgnoreCustom, Lib3"), - ("monitor_speed", "115200"), - ("lib_deps", "Lib1, Lib2")] + assert config.items("custom") == [ + ("debug_flags", "-D DEBUG=1"), + ("lib_flags", "-lc -lm"), + ("extra_flags", "-L /usr/local/lib"), + ("lib_ignore", "LibIgnoreCustom") + ] # yapf: disable + assert config.items(env="extra_1") == [ + ("build_flags", ["-lc -lm -D DEBUG=1", "-DSYSENVDEPS1 -DSYSENVDEPS2"]), + ("monitor_speed", "115200"), + ("lib_deps", ["Lib1", "Lib2"]), + ("lib_ignore", ["LibIgnoreCustom"]), + ("upload_port", "/dev/sysenv/port") + ] # yapf: disable + assert config.items(env="extra_2") == [ + ("build_flags", ["-Og", "-DSYSENVDEPS1 -DSYSENVDEPS2"]), + ("lib_ignore", ["LibIgnoreCustom", "Lib3"]), + ("upload_port", "/dev/extra_2/port"), + ("monitor_speed", "115200"), + ("lib_deps", ["Lib1", "Lib2"]) + ] # yapf: disable + + # cleanup system environment variables + del os.environ["PLATFORMIO_BUILD_FLAGS"] + del os.environ["PLATFORMIO_UPLOAD_PORT"] + del os.environ["__PIO_TEST_CNF_EXTRA_FLAGS"] + + +def test_empty_config(): + config = ProjectConfig("/non/existing/platformio.ini") + + # unknown section + with pytest.raises(ConfigParser.NoSectionError): + config.getraw("unknown_section", "unknown_option") + + assert config.sections() == [] + assert config.get("section", "option") is None + assert config.get("section", "option", 13) == 13 + + # sysenv + os.environ["PLATFORMIO_HOME_DIR"] = "/custom/core/dir" + assert config.get("platformio", "core_dir") == "/custom/core/dir" + del os.environ["PLATFORMIO_HOME_DIR"]