├── .gitattributes ├── .adr-dir ├── requirements-dev.txt ├── src └── mbed_tools │ ├── project │ ├── _internal │ │ ├── templates │ │ │ ├── gitignore.tmpl │ │ │ ├── __init__.py │ │ │ ├── main.tmpl │ │ │ └── CMakeLists.tmpl │ │ ├── __init__.py │ │ ├── render_templates.py │ │ └── progress.py │ ├── __init__.py │ ├── exceptions.py │ └── project.py │ ├── lib │ ├── __init__.py │ ├── exceptions.py │ ├── json_helpers.py │ ├── python_helpers.py │ └── logging.py │ ├── sterm │ └── __init__.py │ ├── __init__.py │ ├── devices │ ├── _internal │ │ ├── linux │ │ │ ├── __init__.py │ │ │ └── device_detector.py │ │ ├── darwin │ │ │ ├── __init__.py │ │ │ ├── ioreg.py │ │ │ ├── diskutil.py │ │ │ └── system_profiler.py │ │ ├── windows │ │ │ ├── __init__.py │ │ │ ├── component_descriptor_utils.py │ │ │ ├── serial_port_data_loader.py │ │ │ ├── logical_disk.py │ │ │ ├── disk_partition_logical_disk_relationships.py │ │ │ ├── usb_controller.py │ │ │ ├── device_detector.py │ │ │ ├── disk_partition.py │ │ │ ├── usb_hub.py │ │ │ ├── device_instance_id.py │ │ │ ├── serial_port.py │ │ │ ├── system_data_loader.py │ │ │ ├── volume_set.py │ │ │ └── usb_hub_data_loader.py │ │ ├── __init__.py │ │ ├── base_detector.py │ │ ├── exceptions.py │ │ ├── detect_candidate_devices.py │ │ ├── candidate_device.py │ │ └── resolve_board.py │ ├── exceptions.py │ ├── __init__.py │ └── devices.py │ ├── build │ ├── _internal │ │ ├── __init__.py │ │ ├── config │ │ │ ├── __init__.py │ │ │ └── config.py │ │ ├── templates │ │ │ ├── __init__.py │ │ │ └── mbed_config.tmpl │ │ ├── write_files.py │ │ └── cmake_file.py │ ├── __init__.py │ ├── exceptions.py │ ├── build.py │ ├── flash.py │ └── config.py │ ├── targets │ ├── _internal │ │ ├── __init__.py │ │ ├── exceptions.py │ │ └── targets_json_parsers │ │ │ └── __init__.py │ ├── exceptions.py │ ├── __init__.py │ ├── get_target.py │ ├── env.py │ ├── boards.py │ └── board.py │ └── cli │ ├── __init__.py │ ├── sterm.py │ ├── main.py │ ├── configure.py │ └── project_management.py ├── docs ├── howto │ └── mbed-tools-howto.md └── architecture │ └── decisions │ ├── 0001-record-architecture-decisions.md │ └── 0002-move-mbed-config-header-defines-to-cmake.md ├── azure-pipelines ├── steps │ ├── override-checkout.yml │ ├── publish-code-coverage-results.yml │ └── determine-current-branch.yml ├── prep-release.yml ├── build-and-deploy.yml └── analyse-and-test.yml ├── tests ├── __init__.py ├── build │ ├── __init__.py │ ├── _internal │ │ ├── __init__.py │ │ ├── config │ │ │ ├── __init__.py │ │ │ └── factories.py │ │ ├── test_write_files.py │ │ └── test_cmake_file.py │ ├── factories.py │ ├── test_build.py │ └── test_flash.py ├── cli │ ├── __init__.py │ ├── test_devices_command_integration.py │ ├── test_sterm.py │ └── test_configure.py ├── ci_scripts │ ├── __init__.py │ └── test_bump_version.py ├── project │ ├── __init__.py │ ├── _internal │ │ ├── __init__.py │ │ ├── test_progress.py │ │ └── test_render_templates.py │ ├── factories.py │ └── test_mbed_project.py ├── devices │ ├── _internal │ │ ├── darwin │ │ │ ├── __init__.py │ │ │ └── test_ioreg.py │ │ ├── linux │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── windows │ │ │ ├── __init__.py │ │ │ ├── test_serial_port.py │ │ │ ├── test_system_data_loader.py │ │ │ ├── test_component_descriptor_utils.py │ │ │ ├── test_windows_component.py │ │ │ └── test_disk_identifier.py │ │ ├── test_detect_candidate_devices.py │ │ └── test_candidate_device.py │ ├── __init__.py │ ├── markers.py │ └── factories.py ├── targets │ ├── __init__.py │ ├── test_config.py │ ├── factories.py │ └── test_get_target.py ├── lib │ ├── test_json_helpers.py │ └── test_python_helpers.py └── regression │ └── test_configure.py ├── requirements-test.txt ├── ci_scripts ├── __init__.py └── prep-release ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── license_config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── scancode.yml ├── pytest.ini ├── news └── README.md ├── MANIFEST.in ├── .pre-commit-config.yaml ├── ci └── test-data │ ├── TARGET_IMAGINARYBOARD │ └── CMakeLists.txt │ └── custom_targets.json ├── .gitignore ├── codecov.yml ├── .codeclimate.yml ├── tox.ini ├── setup.cfg ├── pyproject.toml └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.adr-dir: -------------------------------------------------------------------------------- 1 | docs/architecture/decisions 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | flake8-docstrings 3 | black 4 | mypy 5 | pre-commit 6 | -------------------------------------------------------------------------------- /src/mbed_tools/project/_internal/templates/gitignore.tmpl: -------------------------------------------------------------------------------- 1 | .mbedbuild 2 | cmake_build/ 3 | -------------------------------------------------------------------------------- /docs/howto/mbed-tools-howto.md: -------------------------------------------------------------------------------- 1 | This document has moved to: https://os.mbed.com/docs/mbed-os/latest/build-tools/mbed-cli-2.html 2 | -------------------------------------------------------------------------------- /azure-pipelines/steps/override-checkout.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - checkout: self 3 | submodules: recursive 4 | persistCredentials: true 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /tests/build/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /tests/cli/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /tests/ci_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /tests/project/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /tests/build/_internal/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | coverage[toml] 3 | requests-mock 4 | pytest-cov 5 | factory_boy 6 | boto3 7 | jinja2 8 | PyGithub 9 | mbed-tools-ci-scripts 10 | -------------------------------------------------------------------------------- /tests/project/_internal/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /tests/build/_internal/config/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /tests/devices/_internal/darwin/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /tests/devices/_internal/linux/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | -------------------------------------------------------------------------------- /tests/devices/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """test module.""" 6 | -------------------------------------------------------------------------------- /ci_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Scripts to be run by the CI pipeline.""" 6 | -------------------------------------------------------------------------------- /tests/targets/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Tests for the mbed-targets modules.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Provides a library of common code.""" 6 | -------------------------------------------------------------------------------- /tests/devices/_internal/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Internal implementation tests.""" 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Arm Mbed OS support forum 4 | url: https://forums.mbed.com/ 5 | about: You can ask and answer questions here. 6 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --cov mbed_tools --cov-report xml:coverage/coverage.xml --cov-report=html 3 | junit_family = xunit2 4 | testpaths = tests 5 | norecursedirs = .* dist CVS _darcs {arch} *.egg venv 6 | -------------------------------------------------------------------------------- /src/mbed_tools/sterm/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Package containing sterm functionality.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Exposes the primary interfaces for the library.""" 6 | -------------------------------------------------------------------------------- /tests/devices/_internal/windows/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Tests for Windows implementation.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/linux/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Linux specific device detection.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/darwin/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Darwin specific device detection.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Windows specific device detection.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/build/_internal/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Code not to be accessed by external applications.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/build/_internal/config/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Use this module to assemble build configuration.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Code not to be accessed by external applications.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/project/_internal/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Code not to be accessed by external applications.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/targets/_internal/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Code not to be accessed by external applications.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/build/_internal/templates/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Integration with https://github.com/ARMmbed/mbed-tools.""" 6 | -------------------------------------------------------------------------------- /src/mbed_tools/cli/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """mbed_tools command line interface.""" 6 | 7 | from mbed_tools.cli.main import cli, LOGGER 8 | -------------------------------------------------------------------------------- /src/mbed_tools/project/_internal/templates/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Package containing jinja templates used by mbed_tools.project.""" 6 | -------------------------------------------------------------------------------- /azure-pipelines/steps/publish-code-coverage-results.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - bash: | 3 | bash <(curl -s https://codecov.io/bash) 4 | condition: and(succeeded(), eq(variables['uploadCoverage'], 'true')) 5 | displayName: 'Upload test coverage to codecov.io' 6 | -------------------------------------------------------------------------------- /news/README.md: -------------------------------------------------------------------------------- 1 | # News directory 2 | 3 | This directory comprises information about all the changes that happened since the last release. A file should be added to this directory for each PR. On release of the package the content of the file becomes part of the change log and this directory is reset. 4 | -------------------------------------------------------------------------------- /src/mbed_tools/lib/exceptions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Exceptions raised by mbed tools.""" 6 | 7 | 8 | class ToolsError(Exception): 9 | """Base class for tools errors.""" 10 | -------------------------------------------------------------------------------- /.github/license_config.yml: -------------------------------------------------------------------------------- 1 | license: 2 | main: apache-2.0 3 | category: Permissive 4 | exclude: 5 | extensions: 6 | - yml 7 | - yaml 8 | - html 9 | - rst 10 | - conf 11 | - cfg 12 | - ini 13 | - toml 14 | - in 15 | - txt 16 | langs: 17 | - HTML 18 | -------------------------------------------------------------------------------- /src/mbed_tools/project/_internal/templates/main.tmpl: -------------------------------------------------------------------------------- 1 | /* mbed Microcontroller Library 2 | * Copyright (c) {{year}} ARM Limited 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | #include "mbed.h" 7 | 8 | 9 | int main() 10 | { 11 | printf("Hello, Mbed!\n"); 12 | return 0; 13 | } 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude *.py[cod] __pycache__ *.so 2 | prune .git 3 | prune .github 4 | prune news 5 | prune docs 6 | prune azure-pipelines 7 | prune travis-ci 8 | exclude .pre-commit-config.yaml 9 | exclude .travis.yml 10 | exclude .coverage.yaml 11 | exclude codecov.yml 12 | exclude .codeclimate.yml 13 | exclude .adr-dir 14 | -------------------------------------------------------------------------------- /azure-pipelines/steps/determine-current-branch.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - bash: | 3 | if [[ -z "$(System.PullRequest.SourceBranch)" ]]; then 4 | branch_name="$(Build.SourceBranchName)" 5 | else 6 | branch_name="$(System.PullRequest.SourceBranch)" 7 | fi 8 | echo "Determined branch name: $branch_name" 9 | echo "##vso[task.setvariable variable=current_branch]$branch_name" 10 | displayName: "Set current_branch variable" 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.6.0 4 | hooks: 5 | - id: black 6 | 7 | - repo: https://gitlab.com/pycqa/flake8 8 | rev: 3.8.3 9 | hooks: 10 | - id: flake8 11 | additional_dependencies: [flake8-docstrings] 12 | 13 | - repo: https://github.com/pre-commit/mirrors-mypy 14 | rev: v0.782 15 | hooks: 16 | - id: mypy 17 | entry: mypy src 18 | pass_filenames: false 19 | -------------------------------------------------------------------------------- /ci/test-data/TARGET_IMAGINARYBOARD/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | add_library(mbed-imaginaryboard INTERFACE) 5 | 6 | target_sources(mbed-imaginaryboard 7 | INTERFACE 8 | PeripheralPins.c 9 | ) 10 | 11 | target_include_directories(mbed-imaginaryboard 12 | INTERFACE 13 | . 14 | ) 15 | 16 | target_link_libraries(mbed-imaginaryboard INTERFACE mbed-stm32l475xg) 17 | -------------------------------------------------------------------------------- /tests/lib/test_json_helpers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import json 6 | 7 | import pytest 8 | 9 | from mbed_tools.lib.json_helpers import decode_json_file 10 | 11 | 12 | def test_invalid_json(tmp_path): 13 | lib_json_path = tmp_path / "mbed_lib.json" 14 | lib_json_path.write_text("name") 15 | 16 | with pytest.raises(json.JSONDecodeError): 17 | decode_json_file(lib_json_path) 18 | -------------------------------------------------------------------------------- /tests/devices/markers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Shared pytest functionality.""" 6 | import platform 7 | import unittest 8 | 9 | 10 | windows_only = unittest.skipIf(platform.system() != "Windows", reason="Windows required") 11 | darwin_only = unittest.skipIf(platform.system() != "Darwin", reason="Darwin required") 12 | linux_only = unittest.skipIf(platform.system() != "Linux", reason="Linux required") 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't lock the versions of this library or installation gridlock will ensue 2 | Pipfile.lock 3 | 4 | # PyCharm 5 | .idea/ 6 | 7 | # macOS 8 | .DS_Store 9 | 10 | # Python 11 | *.pyc 12 | __pycache__/ 13 | *.egg-info/ 14 | /build/ 15 | 16 | # Coverage.py 17 | .coverage* 18 | coverage/ 19 | junit/ 20 | htmlcov/ 21 | 22 | # Package 23 | dist/ 24 | release-dist/ 25 | 26 | # Temporary file used by CI 27 | dev-requirements.txt 28 | 29 | # Local docs output 30 | local_docs/ 31 | 32 | # Tox envs 33 | .tox/ 34 | 35 | # User envs 36 | .venv/ 37 | -------------------------------------------------------------------------------- /src/mbed_tools/project/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Creation and management of Mbed OS projects. 6 | 7 | * Creation of a new Mbed OS application. 8 | * Cloning of an existing Mbed OS program. 9 | * Deploy of a specific version of Mbed OS or library. 10 | """ 11 | 12 | from mbed_tools.project.project import initialise_project, import_project, deploy_project, get_known_libs 13 | from mbed_tools.project.mbed_program import MbedProgram 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 6 | 7 | 8 | 9 | ### Test Coverage 10 | 11 | 14 | 15 | - [ ] This change is covered by existing or additional automated tests. 16 | - [ ] Manual testing has been performed (and evidence provided) as automated testing was not feasible. 17 | - [ ] Additional tests are not required for this change (e.g. documentation update). 18 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/base_detector.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Interface for device detectors.""" 6 | from abc import ABC, abstractmethod 7 | from typing import List 8 | 9 | from mbed_tools.devices._internal.candidate_device import CandidateDevice 10 | 11 | 12 | class DeviceDetector(ABC): 13 | """Object in charge of finding USB devices.""" 14 | 15 | @abstractmethod 16 | def find_candidates(self) -> List[CandidateDevice]: 17 | """Returns CandidateDevices.""" 18 | pass 19 | -------------------------------------------------------------------------------- /docs/architecture/decisions/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | Date: 2020-07-23 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | ## Consequences 18 | 19 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 20 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/exceptions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Exceptions internal to the package.""" 6 | 7 | from mbed_tools.lib.exceptions import ToolsError 8 | 9 | 10 | class SystemException(ToolsError): 11 | """Exception with regards to the underlying operating system.""" 12 | 13 | 14 | class NoBoardForCandidate(ToolsError): 15 | """Raised when board data cannot be determined for a candidate.""" 16 | 17 | 18 | class ResolveBoardError(ToolsError): 19 | """There was an error resolving the board for a device.""" 20 | -------------------------------------------------------------------------------- /src/mbed_tools/targets/_internal/exceptions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Exceptions used internally by the mbed-targets package.""" 6 | 7 | from mbed_tools.targets.exceptions import BoardDatabaseError, MbedTargetsError 8 | 9 | 10 | class BoardAPIError(BoardDatabaseError): 11 | """API request failed.""" 12 | 13 | 14 | class ResponseJSONError(BoardDatabaseError): 15 | """HTTP response JSON parsing failed.""" 16 | 17 | 18 | class TargetsJsonConfigurationError(MbedTargetsError): 19 | """The target definition is invalid.""" 20 | -------------------------------------------------------------------------------- /tests/devices/factories.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import factory 6 | import pathlib 7 | 8 | from mbed_tools.devices._internal.candidate_device import CandidateDevice 9 | 10 | 11 | class CandidateDeviceFactory(factory.Factory): 12 | class Meta: 13 | model = CandidateDevice 14 | 15 | product_id = factory.Faker("hexify") 16 | vendor_id = factory.Faker("hexify") 17 | mount_points = [pathlib.Path(".")] 18 | serial_number = factory.Faker("hexify", text=("^" * 20)) # 20 characters serial number 19 | serial_port = None 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: 'enhancement' 5 | --- 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/workflows/scancode.yml: -------------------------------------------------------------------------------- 1 | name: Scancode 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | scancode_job: 7 | runs-on: ubuntu-latest 8 | name: Scan code for licenses 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Scan the code 12 | id: scancode 13 | uses: zephyrproject-rtos/action_scancode@v1 14 | with: 15 | directory-to-scan: 'scan/' 16 | - name: Artifact Upload 17 | uses: actions/upload-artifact@v1 18 | with: 19 | name: scancode 20 | path: ./artifacts 21 | 22 | - name: Verify 23 | run: | 24 | test ! -s ./artifacts/report.txt || (cat ./artifacts/report.txt && exit 1 ) 25 | -------------------------------------------------------------------------------- /ci/test-data/custom_targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "IMAGINARYBOARD": { 3 | "inherits": [ 4 | "MCU_STM32L475xG" 5 | ], 6 | "components_add": [ 7 | "BlueNRG_MS", 8 | "QSPIF" 9 | ], 10 | "extra_labels_add": [ 11 | "CORDIO", 12 | "MX25R6435F" 13 | ], 14 | "supported_form_factors": [ 15 | "ARDUINO_UNO" 16 | ], 17 | "detect_code": [ 18 | "1234" 19 | ], 20 | "device_has_add": [ 21 | "QSPI" 22 | ], 23 | "features": [ 24 | "BLE" 25 | ], 26 | "device_name": "STM32L475VG" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/exceptions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Public exceptions raised by the package.""" 6 | from mbed_tools.lib.exceptions import ToolsError 7 | 8 | 9 | class MbedDevicesError(ToolsError): 10 | """Base public exception for the mbed-devices package.""" 11 | 12 | 13 | class DeviceLookupFailed(MbedDevicesError): 14 | """Failed to look up data associated with the device.""" 15 | 16 | 17 | class NoDevicesFound(MbedDevicesError): 18 | """No Mbed Enabled devices were found.""" 19 | 20 | 21 | class UnknownOSError(MbedDevicesError): 22 | """The current OS is not supported.""" 23 | -------------------------------------------------------------------------------- /src/mbed_tools/lib/json_helpers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Helpers for json related functions.""" 6 | import json 7 | import logging 8 | 9 | from pathlib import Path 10 | from typing import Any 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def decode_json_file(path: Path) -> Any: 16 | """Return the contents of json file.""" 17 | try: 18 | logger.debug(f"Loading JSON file {path}") 19 | return json.loads(path.read_text()) 20 | except json.JSONDecodeError: 21 | logger.error(f"Failed to decode JSON data in the file located at '{path}'") 22 | raise 23 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | notify: 4 | after_n_builds: 3 5 | 6 | coverage: 7 | precision: 2 8 | round: down 9 | range: "0...100" 10 | status: 11 | project: 12 | default: 13 | # basic 14 | target: auto 15 | threshold: 2% 16 | base: auto 17 | # advanced 18 | branches: 19 | - master 20 | if_not_found: success 21 | if_ci_failed: error 22 | informational: true 23 | only_pulls: false 24 | parsers: 25 | gcov: 26 | branch_detection: 27 | conditional: yes 28 | loop: yes 29 | method: no 30 | macro: no 31 | 32 | comment: 33 | layout: "reach,diff,flags,tree" 34 | behavior: default 35 | require_changes: no 36 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | argument-count: 4 | config: 5 | threshold: 4 6 | complex-logic: 7 | config: 8 | threshold: 4 9 | file-lines: 10 | config: 11 | threshold: 250 12 | method-complexity: 13 | config: 14 | threshold: 5 15 | method-count: 16 | config: 17 | threshold: 20 18 | method-lines: 19 | config: 20 | threshold: 25 21 | nested-control-flow: 22 | config: 23 | threshold: 4 24 | return-statements: 25 | config: 26 | threshold: 4 27 | similar-code: 28 | config: 29 | threshold: # language-specific defaults. an override will affect all languages. 30 | identical-code: 31 | config: 32 | threshold: # language-specific defaults. an override will affect all languages. 33 | -------------------------------------------------------------------------------- /src/mbed_tools/targets/exceptions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Public exceptions exposed by the package.""" 6 | 7 | from mbed_tools.lib.exceptions import ToolsError 8 | 9 | 10 | class MbedTargetsError(ToolsError): 11 | """Base exception for mbed-targets.""" 12 | 13 | 14 | class TargetError(ToolsError): 15 | """Target definition cannot be retrieved.""" 16 | 17 | 18 | class UnknownBoard(MbedTargetsError): 19 | """Requested board was not found.""" 20 | 21 | 22 | class UnsupportedMode(MbedTargetsError): 23 | """The Database Mode is unsupported.""" 24 | 25 | 26 | class BoardDatabaseError(MbedTargetsError): 27 | """Failed to get the board data from the database.""" 28 | -------------------------------------------------------------------------------- /src/mbed_tools/project/_internal/templates/CMakeLists.tmpl: -------------------------------------------------------------------------------- 1 | # Copyright (c) {{year}} ARM Limited. All rights reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | cmake_minimum_required(VERSION 3.19.0) 5 | 6 | set(MBED_PATH ${CMAKE_CURRENT_SOURCE_DIR}/mbed-os CACHE INTERNAL "") 7 | set(MBED_CONFIG_PATH ${CMAKE_CURRENT_BINARY_DIR} CACHE INTERNAL "") 8 | set(APP_TARGET {{program_name}}) 9 | 10 | include(${MBED_PATH}/tools/cmake/app.cmake) 11 | 12 | project(${APP_TARGET}) 13 | 14 | add_subdirectory(${MBED_PATH}) 15 | 16 | add_executable(${APP_TARGET} 17 | main.cpp 18 | ) 19 | 20 | target_link_libraries(${APP_TARGET} mbed-os) 21 | 22 | mbed_set_post_build(${APP_TARGET}) 23 | 24 | option(VERBOSE_BUILD "Have a verbose build process") 25 | if(VERBOSE_BUILD) 26 | set(CMAKE_VERBOSE_MAKEFILE ON) 27 | endif() 28 | -------------------------------------------------------------------------------- /src/mbed_tools/build/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Provides the core build system for Mbed OS, which relies on CMake and Ninja as underlying technologies. 6 | 7 | The functionality covered in this package includes the following: 8 | 9 | - Execution of Mbed Pre-Build stages to determine appropriate configuration for Mbed OS and the build process. 10 | - Invocation of the build process for the command line tools and online build service. 11 | - Export of build instructions to third party command line tools and IDEs. 12 | """ 13 | from mbed_tools.build.build import build_project, generate_build_system 14 | from mbed_tools.build.config import generate_config 15 | from mbed_tools.build.flash import flash_binary 16 | -------------------------------------------------------------------------------- /tests/lib/test_python_helpers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import pytest 6 | 7 | from mbed_tools.lib.python_helpers import flatten_nested 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "input_list, expected_result", 12 | ( 13 | ([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]), 14 | ([1, [2], 3, 4, 5], [1, 2, 3, 4, 5]), 15 | ([[1, 2, 3], [4, 5]], [1, 2, 3, 4, 5]), 16 | ([(1, 2, 3), (4, 5)], [1, 2, 3, 4, 5]), 17 | ([[1, [[2, [3]]]], [4, 5]], [1, 2, 3, 4, 5]), 18 | ([["alan", [["bob", ["sally"]]]], ["jim", "jenny"]], ["alan", "bob", "sally", "jim", "jenny"]), 19 | ), 20 | ) 21 | def test_flatten_nested_list(input_list, expected_result): 22 | assert flatten_nested(input_list) == expected_result 23 | -------------------------------------------------------------------------------- /src/mbed_tools/project/exceptions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Public exceptions exposed by the package.""" 6 | 7 | from mbed_tools.lib.exceptions import ToolsError 8 | 9 | 10 | class MbedProjectError(ToolsError): 11 | """Base exception for mbed-project.""" 12 | 13 | 14 | class VersionControlError(MbedProjectError): 15 | """Raised when a source control management operation failed.""" 16 | 17 | 18 | class ExistingProgram(MbedProjectError): 19 | """Raised when a program already exists at a given path.""" 20 | 21 | 22 | class ProgramNotFound(MbedProjectError): 23 | """Raised when an expected program is not found.""" 24 | 25 | 26 | class MbedOSNotFound(MbedProjectError): 27 | """A valid copy of MbedOS was not found.""" 28 | -------------------------------------------------------------------------------- /src/mbed_tools/targets/_internal/targets_json_parsers/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Package for parsing Mbed OS library's targets.json. 6 | 7 | targets.json contains the attribute data for all supported targets. 8 | 9 | Targets are defined using inheritance. Multiple inheritance is allowed (though not encouraged). 10 | 11 | Attributes come in two types - overriding and accumulating. These 12 | use two completely different ways of inheriting so each has its own parser. 13 | 14 | The combined results of these two parsers makes a complete set of attributes for 15 | a target as defined in targets.json. 16 | 17 | For more information about targets.json structure see 18 | https://os.mbed.com/docs/mbed-os/latest/reference/adding-and-configuring-targets.html 19 | """ 20 | -------------------------------------------------------------------------------- /tests/targets/test_config.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import os 6 | from unittest import TestCase, mock 7 | 8 | from mbed_tools.targets.env import env 9 | 10 | 11 | class TestMbedApiAuthToken(TestCase): 12 | @mock.patch.dict(os.environ, {"MBED_API_AUTH_TOKEN": "sometoken"}) 13 | def test_returns_api_token_set_in_env(self): 14 | self.assertEqual(env.MBED_API_AUTH_TOKEN, "sometoken") 15 | 16 | 17 | class TestDatabaseMode(TestCase): 18 | @mock.patch.dict(os.environ, {"MBED_DATABASE_MODE": "ONLINE"}) 19 | def test_returns_database_mode_set_in_env(self): 20 | self.assertEqual(env.MBED_DATABASE_MODE, "ONLINE") 21 | 22 | def test_returns_default_database_mode_if_not_set_in_env(self): 23 | self.assertEqual(env.MBED_DATABASE_MODE, "AUTO") 24 | -------------------------------------------------------------------------------- /tests/devices/_internal/windows/test_serial_port.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | from tests.devices.markers import windows_only 6 | from unittest import TestCase 7 | import random 8 | 9 | 10 | @windows_only 11 | class TestSerialPort(TestCase): 12 | def test_retrieve_port_name(self): 13 | from mbed_tools.devices._internal.windows.serial_port import parse_caption 14 | from mbed_tools.devices._internal.windows.component_descriptor import UNKNOWN_VALUE 15 | 16 | self.assertEqual(UNKNOWN_VALUE, parse_caption(UNKNOWN_VALUE)) 17 | self.assertEqual("COM13", parse_caption("Serial Port for Barcode Scanner (COM13)")) 18 | port_name = f"COM{random.choice(range(0, 1000))}" 19 | self.assertEqual(port_name, parse_caption(f"mbed Serial Port ({port_name})")) 20 | -------------------------------------------------------------------------------- /src/mbed_tools/build/exceptions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Public exceptions raised by the package.""" 6 | from mbed_tools.lib.exceptions import ToolsError 7 | 8 | 9 | class MbedBuildError(ToolsError): 10 | """Base public exception for the mbed-build package.""" 11 | 12 | 13 | class InvalidExportOutputDirectory(MbedBuildError): 14 | """It is not possible to export to the provided output directory.""" 15 | 16 | 17 | class BinaryFileNotFoundError(MbedBuildError): 18 | """The binary file (.bin/.hex) cannot be found in cmake_build directory.""" 19 | 20 | 21 | class DeviceNotFoundError(MbedBuildError): 22 | """The requested device is not connected to your system.""" 23 | 24 | 25 | class InvalidConfigOverride(MbedBuildError): 26 | """A given config setting was invalid.""" 27 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """API to detect any Mbed OS devices connected to the host computer. 6 | 7 | It is expected that this package will be used by developers of Mbed OS tooling rather than by users of Mbed OS. 8 | This package uses the https://github.com/ARMmbed/mbed-targets interface to identify valid Mbed Enabled Devices. 9 | Please see the documentation for mbed-targets for information on configuration options. 10 | 11 | For the command line interface to the API see the package https://github.com/ARMmbed/mbed-tools 12 | """ 13 | from mbed_tools.devices.devices import ( 14 | get_connected_devices, 15 | find_connected_device, 16 | find_all_connected_devices, 17 | ) 18 | from mbed_tools.devices.device import Device 19 | from mbed_tools.devices import exceptions 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: 'bug' 5 | --- 6 | **Describe the bug** 7 | A clear and concise description of what the bug is. 8 | 9 | **To Reproduce** 10 | Steps to reproduce the behavior: 11 | 1. Go to '...' 12 | 2. Click on '....' 13 | 3. Scroll down to '....' 14 | 4. See error 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. Windows] 24 | - Version: [e.g. 2000] 25 | 26 | **Mbed (please complete the following information):** 27 | - Device: [e.g. `DISCO_L475VG_IOT01A`] 28 | - Mbed OS Version: [e.g. 6.8.0] 29 | - Mbed CLI 2 Version: [e.g. 7.3.1] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /tests/targets/factories.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | from mbed_tools.targets import Board 6 | 7 | 8 | def make_board( 9 | board_type="BoardType", 10 | board_name="BoardName", 11 | mbed_os_support=None, 12 | mbed_enabled=None, 13 | product_code="9999", 14 | slug="BoardSlug", 15 | target_type="TargetType", 16 | ): 17 | return Board( 18 | board_type=board_type, 19 | product_code=product_code, 20 | board_name=board_name, 21 | target_type=target_type, 22 | slug=slug, 23 | mbed_os_support=mbed_os_support if mbed_os_support else (), 24 | mbed_enabled=mbed_enabled if mbed_enabled else (), 25 | build_variant=(), 26 | ) 27 | 28 | 29 | def make_dummy_internal_board_data(): 30 | return [dict(attributes=dict(board_type=str(i), board_name=str(i), product_code=str(i))) for i in range(10)] 31 | -------------------------------------------------------------------------------- /src/mbed_tools/lib/python_helpers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Helpers for python language related functions.""" 6 | from typing import Iterable, List 7 | 8 | 9 | def flatten_nested(input_iter: Iterable) -> List: 10 | """Flatten a nested Iterable with arbitrary levels of nesting. 11 | 12 | If the input is an iterator then this function will exhaust it. 13 | 14 | Args: 15 | input_iter: The input Iterable which may or may not be nested. 16 | 17 | Returns: 18 | A flat list created from the input_iter. 19 | If input_iter has no nesting its elements are appended to a list and returned. 20 | """ 21 | output = [] 22 | for elem in input_iter: 23 | if isinstance(elem, Iterable) and not isinstance(elem, str): 24 | output += flatten_nested(elem) 25 | else: 26 | output.append(elem) 27 | 28 | return output 29 | -------------------------------------------------------------------------------- /azure-pipelines/prep-release.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - none 3 | 4 | pr: 5 | - none 6 | 7 | schedules: 8 | - cron: "00 5 * * *" 9 | displayName: "Prepare and tag release" 10 | branches: 11 | include: 12 | - master 13 | always: true 14 | 15 | jobs: 16 | - job: TagRelease 17 | displayName: 'Prepare mbed-tools release' 18 | pool: 19 | vmImage: 'ubuntu-latest' 20 | 21 | steps: 22 | - task: UsePythonVersion@0 23 | inputs: 24 | versionSpec: '3.7' 25 | 26 | - script: | 27 | # Set our user to the release bot account and ensure we've checked 28 | # out the master branch. 29 | git config --global user.name "Monty Bot" 30 | git config --global user.email "monty-bot@arm.com" 31 | git checkout master 32 | 33 | python -m pip install --upgrade tox 34 | 35 | tox -e preprelease 36 | displayName: "Prepare mbed-tools release" 37 | env: 38 | GIT_TOKEN: $(GIT_TOKEN) 39 | -------------------------------------------------------------------------------- /tests/devices/_internal/windows/test_system_data_loader.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import unittest 6 | from unittest.mock import patch 7 | 8 | from tests.devices.markers import windows_only 9 | 10 | 11 | @windows_only 12 | class TestSystemDataLoader(unittest.TestCase): 13 | @patch("mbed_tools.devices._internal.windows.system_data_loader.load_all") 14 | def test_system_data_load(self, load_all): 15 | from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader, SYSTEM_DATA_TYPES 16 | 17 | def mock_system_element_fetcher(arg): 18 | return (arg, list()) 19 | 20 | load_all.side_effect = mock_system_element_fetcher 21 | 22 | loader = SystemDataLoader() 23 | for type in SYSTEM_DATA_TYPES: 24 | self.assertIsNotNone(loader.get_system_data(type)) 25 | self.assertTrue(isinstance(loader.get_system_data(type), list)) 26 | load_all.assert_called() 27 | -------------------------------------------------------------------------------- /src/mbed_tools/targets/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """An abstraction layer describing hardware supported by Mbed OS. 6 | 7 | Querying board database 8 | ----------------------- 9 | 10 | For the interface to query board database, look at `mbed_tools.targets.get_board`. 11 | 12 | Fetching target data 13 | ____________________ 14 | 15 | For the interface to extract target data from their definitions in Mbed OS, 16 | look at `mbed_tools.targets.get_target`. 17 | 18 | Configuration 19 | ------------- 20 | 21 | For details about configuration of this module, look at `mbed_tools.targets.config`. 22 | """ 23 | from mbed_tools.targets import exceptions 24 | from mbed_tools.targets.get_target import ( 25 | get_target_by_name, 26 | get_target_by_board_type, 27 | ) 28 | from mbed_tools.targets.get_board import ( 29 | get_board_by_product_code, 30 | get_board_by_online_id, 31 | get_board_by_jlink_slug, 32 | ) 33 | from mbed_tools.targets.board import Board 34 | -------------------------------------------------------------------------------- /src/mbed_tools/build/_internal/write_files.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Writes out files to specified locations.""" 6 | import pathlib 7 | from mbed_tools.build.exceptions import InvalidExportOutputDirectory 8 | 9 | 10 | def write_file(file_path: pathlib.Path, file_contents: str) -> None: 11 | """Writes out a string to a file. 12 | 13 | If the intermediate directories to the output directory don't exist, 14 | this function will create them. 15 | 16 | This function will overwrite any existing file of the same name in the 17 | output directory. 18 | 19 | Raises: 20 | InvalidExportOutputDirectory: it's not possible to export to the output directory provided 21 | """ 22 | output_directory = file_path.parent 23 | if output_directory.is_file(): 24 | raise InvalidExportOutputDirectory("Output directory cannot be a path to a file.") 25 | 26 | output_directory.mkdir(parents=True, exist_ok=True) 27 | file_path.write_text(file_contents) 28 | -------------------------------------------------------------------------------- /tests/build/factories.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import factory 6 | import pathlib 7 | 8 | from typing import List 9 | 10 | from mbed_tools.devices.device import Device, ConnectedDevices 11 | from mbed_tools.targets.board import Board 12 | 13 | 14 | class BoardFactory(factory.Factory): 15 | class Meta: 16 | model = Board 17 | 18 | board_type = "TEST" 19 | board_name = "" 20 | product_code = "" 21 | target_type = "" 22 | slug = "" 23 | build_variant = [] 24 | mbed_os_support = [] 25 | mbed_enabled = [] 26 | 27 | 28 | class DeviceFactory(factory.Factory): 29 | class Meta: 30 | model = Device 31 | 32 | serial_port = "" 33 | serial_number = "" 34 | mount_points = [pathlib.Path(".")] 35 | mbed_board = BoardFactory() 36 | 37 | 38 | class ConnectedDevicesFactory(factory.Factory): 39 | class Meta: 40 | model = ConnectedDevices 41 | 42 | identified_devices: List[Device] 43 | unidentified_devices: List[Device] 44 | -------------------------------------------------------------------------------- /tests/build/_internal/config/factories.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import factory 6 | 7 | from mbed_tools.build._internal.config.config import Config, Option, Macro 8 | from mbed_tools.build._internal.config.source import Source 9 | 10 | 11 | class SourceFactory(factory.Factory): 12 | class Meta: 13 | model = Source 14 | 15 | human_name = factory.Faker("name") 16 | config = factory.Dict({}) 17 | overrides = factory.Dict({}) 18 | macros = factory.List([]) 19 | 20 | 21 | class OptionFactory(factory.Factory): 22 | class Meta: 23 | model = Option 24 | 25 | value = "0" 26 | macro_name = "MACRO_NAME" 27 | help_text = factory.Faker("text") 28 | set_by = "libname" 29 | key = "key" 30 | 31 | 32 | class MacroFactory(factory.Factory): 33 | class Meta: 34 | model = Macro 35 | 36 | value = "0" 37 | name = "A_MACRO" 38 | set_by = "source" 39 | 40 | 41 | class ConfigFactory(factory.Factory): 42 | class Meta: 43 | model = Config 44 | 45 | options = factory.Dict({}) 46 | macros = factory.Dict({}) 47 | -------------------------------------------------------------------------------- /tests/build/_internal/test_write_files.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import pathlib 6 | import tempfile 7 | from unittest import TestCase 8 | 9 | from mbed_tools.build._internal.write_files import write_file 10 | from mbed_tools.build.exceptions import InvalidExportOutputDirectory 11 | 12 | 13 | class TestWriteFile(TestCase): 14 | def test_writes_content_to_file(self): 15 | with tempfile.TemporaryDirectory() as directory: 16 | content = "Some rendered content" 17 | export_path = pathlib.Path(directory, "output", "some_file.txt") 18 | 19 | write_file(export_path, content) 20 | 21 | created_file = pathlib.Path(export_path) 22 | self.assertEqual(created_file.read_text(), content) 23 | 24 | def test_output_dir_is_file(self): 25 | with tempfile.TemporaryDirectory() as directory: 26 | bad_export_dir = pathlib.Path(directory, "some_file.txt", ".txt") 27 | bad_export_dir.parent.touch() 28 | with self.assertRaises(InvalidExportOutputDirectory): 29 | write_file(bad_export_dir, "some contents") 30 | -------------------------------------------------------------------------------- /docs/architecture/decisions/0002-move-mbed-config-header-defines-to-cmake.md: -------------------------------------------------------------------------------- 1 | # 2. Move mbed config header defines to CMake 2 | 3 | Date: 2020-07-23 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | `mbed_config.h` was created because the command line length limit on older versions of Windows prevented passing many "-D" defines to the compiler and linker. 12 | Additionally, passing many "-D" defines globally is a bad idea in itself. Therefore, `mbed_config.h` was a workaround to make a bad practice work. 13 | On modern versions of Windows, the command line limit isn't an issue. CMake can also do what's necessary to prevent exceeding the command line length limit where needed. 14 | 15 | ## Decision 16 | 17 | We will remove the generation of `mbed_config.h` and simply pass the "-D" defines using CMake directives. This is acheived by moving the "-D" definitions to the tools generated `mbed_config.cmake`. 18 | 19 | ## Consequences 20 | 21 | CMake will be in control of passing the defines to the compiler. The tools generate a single file containing all dynamic configuration parameters. 22 | This model will make it easier to modularise the build system later, as we are removing a "global" header that is included everywhere. 23 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = dev,linting,checknews,py36,py38,py39 3 | minversion = 3.3.0 4 | # Activate isolated build environment. tox will use a virtual environment 5 | # to build a source distribution from the source tree. 6 | isolated_build = True 7 | 8 | [testenv:linting] 9 | skip_install = True 10 | deps = 11 | pre-commit 12 | commands = pre-commit run --all-files 13 | 14 | [testenv] 15 | deps = 16 | -rrequirements-test.txt 17 | commands = pytest {posargs} 18 | 19 | [testenv:dev] 20 | usedevelop = True 21 | envdir = .venv 22 | commands = 23 | deps = 24 | -rrequirements-test.txt 25 | -rrequirements-dev.txt 26 | 27 | [testenv:checknews] 28 | skip_install = True 29 | deps = 30 | towncrier 31 | commands = python -m towncrier.check 32 | 33 | [testenv:preprelease] 34 | usedevelop = True 35 | allowlist_externals = git 36 | passenv = 37 | HOME 38 | GIT_TOKEN 39 | deps = 40 | mbed-tools-ci-scripts 41 | pre-commit 42 | commands = 43 | {toxinidir}/ci_scripts/prep-release 44 | 45 | [testenv:pypirelease] 46 | usedevelop = True 47 | deps = 48 | twine 49 | setuptools>=42 50 | wheel 51 | setuptools_scm[toml]>=3.4 52 | commands = 53 | python setup.py clean --all sdist --formats gztar bdist_wheel 54 | python -m twine check dist/* 55 | python -m twine upload {posargs} dist/* 56 | -------------------------------------------------------------------------------- /azure-pipelines/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - master 5 | tags: 6 | include: 7 | - '*' 8 | 9 | pr: 10 | - none 11 | 12 | stages: 13 | - stage: ReleaseToPyPI 14 | condition: not(and(eq(variables['Build.SourceBranch'], 'refs/heads/master'), contains(variables['Build.SourceVersionMessage'], 'Prepare mbed-tools release'))) 15 | displayName: 'Production Release' 16 | jobs: 17 | - deployment: PyPIRelease 18 | displayName: 'PyPI Release' 19 | environment: 'pypi-release' 20 | strategy: 21 | runOnce: 22 | deploy: 23 | pool: 24 | vmImage: 'ubuntu-latest' 25 | steps: 26 | - task: UsePythonVersion@0 27 | inputs: 28 | versionSpec: '3.7' 29 | 30 | - template: steps/determine-current-branch.yml 31 | 32 | - template: steps/override-checkout.yml 33 | 34 | - script: | 35 | python -m pip install --upgrade tox 36 | tox -e py37 37 | tox -e pypirelease -- -p $TWINE_PASSWORD -u $TWINE_USERNAME 38 | displayName: "PyPI release" 39 | env: 40 | TWINE_USERNAME: $(TWINE_USERNAME) 41 | TWINE_PASSWORD: $(TWINE_PASSWORD) 42 | -------------------------------------------------------------------------------- /tests/project/factories.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | from mbed_tools.project._internal.libraries import MbedLibReference 6 | from mbed_tools.project._internal.project_data import ( 7 | CMAKELISTS_FILE_NAME, 8 | APP_CONFIG_FILE_NAME, 9 | MBED_OS_REFERENCE_FILE_NAME, 10 | ) 11 | 12 | 13 | def make_mbed_program_files(root, config_file_name=APP_CONFIG_FILE_NAME): 14 | if not root.exists(): 15 | root.mkdir() 16 | 17 | (root / MBED_OS_REFERENCE_FILE_NAME).touch() 18 | (root / config_file_name).touch() 19 | (root / CMAKELISTS_FILE_NAME).touch() 20 | 21 | 22 | def make_mbed_lib_reference(root, name="mylib.lib", resolved=False, ref_url=None): 23 | ref_file = root / name 24 | source_dir = ref_file.with_suffix("") 25 | if not root.exists(): 26 | root.mkdir() 27 | 28 | ref_file.touch() 29 | 30 | if resolved: 31 | source_dir.mkdir() 32 | 33 | if ref_url is not None: 34 | ref_file.write_text(ref_url) 35 | 36 | return MbedLibReference(reference_file=ref_file, source_code_path=source_dir) 37 | 38 | 39 | def make_mbed_os_files(root): 40 | if not root.exists(): 41 | root.mkdir() 42 | 43 | targets_dir = root / "targets" 44 | targets_dir.mkdir() 45 | (targets_dir / "targets.json").touch() 46 | -------------------------------------------------------------------------------- /tests/cli/test_devices_command_integration.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | from unittest import TestCase, mock 6 | 7 | import click 8 | from click.testing import CliRunner 9 | 10 | from mbed_tools.cli import cli 11 | from mbed_tools.cli.list_connected_devices import list_connected_devices 12 | from mbed_tools.lib.exceptions import ToolsError 13 | 14 | 15 | class TestDevicesCommandIntegration(TestCase): 16 | def test_devices_is_integrated(self): 17 | self.assertEqual(cli.commands["detect"], list_connected_devices) 18 | 19 | 20 | class TestClickGroupWithExceptionHandling(TestCase): 21 | @mock.patch("mbed_tools.cli.LOGGER.error", autospec=True) 22 | def test_logs_tools_errors(self, logger_error): 23 | def callback(): 24 | raise ToolsError() 25 | 26 | mock_cli = click.Command("test", callback=callback) 27 | cli.add_command(mock_cli, "test") 28 | 29 | runner = CliRunner() 30 | result = runner.invoke(cli, ["test"]) 31 | 32 | self.assertEqual(1, result.exit_code) 33 | logger_error.assert_called_once() 34 | 35 | 36 | class TestVersionCommand(TestCase): 37 | def test_version_command(self): 38 | runner = CliRunner() 39 | result = runner.invoke(cli, ["--version"]) 40 | self.assertTrue(result.output) 41 | self.assertEqual(0, result.exit_code) 42 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/component_descriptor_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Utilities with regards to Win32 component descriptors.""" 6 | from typing import Any, NamedTuple, Union 7 | from collections import OrderedDict 8 | 9 | UNKNOWN_VALUE = "Unknown" 10 | 11 | 12 | def is_undefined_value(value: Any) -> bool: 13 | """Checks whether a field value is considered as undefined or not.""" 14 | return value in [None, UNKNOWN_VALUE, 0, False] 15 | 16 | 17 | def is_undefined_data_object(data_object: NamedTuple) -> bool: 18 | """Checks whether a data object is considered as undefined or not.""" 19 | for f in data_object._fields: 20 | if not is_undefined_value(getattr(data_object, f)): 21 | is_undefined = False 22 | break 23 | else: 24 | is_undefined = True 25 | return is_undefined 26 | 27 | 28 | def retain_value_or_default(value: Any) -> Any: 29 | """Returns the value if set otherwise returns a default value.""" 30 | return UNKNOWN_VALUE if is_undefined_value(value) else value 31 | 32 | 33 | def data_object_to_dict(data_object: NamedTuple) -> dict: 34 | """Gets a data object as a dictionary.""" 35 | internal_dictionary: Union[dict, OrderedDict] = data_object._asdict() 36 | return dict(internal_dictionary) if isinstance(internal_dictionary, OrderedDict) else internal_dictionary 37 | -------------------------------------------------------------------------------- /src/mbed_tools/build/_internal/cmake_file.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Module in charge of CMake file generation.""" 6 | import pathlib 7 | 8 | from typing import Any 9 | 10 | import jinja2 11 | 12 | from mbed_tools.build._internal.config.config import Config 13 | 14 | TEMPLATES_DIRECTORY = pathlib.Path("_internal", "templates") 15 | TEMPLATE_NAME = "mbed_config.tmpl" 16 | 17 | 18 | def render_mbed_config_cmake_template(config: Config, toolchain_name: str, target_name: str) -> str: 19 | """Renders the mbed_config jinja template with the target and project config settings. 20 | 21 | Args: 22 | config: Config object holding information parsed from the mbed config system. 23 | toolchain_name: Name of the toolchain being used. 24 | target_name: Name of the target. 25 | 26 | Returns: 27 | The rendered mbed_config template. 28 | """ 29 | env = jinja2.Environment(loader=jinja2.PackageLoader("mbed_tools.build", str(TEMPLATES_DIRECTORY)),) 30 | env.filters["to_hex"] = to_hex 31 | template = env.get_template(TEMPLATE_NAME) 32 | config["supported_c_libs"] = [x for x in config["supported_c_libs"][toolchain_name.lower()]] 33 | context = {"target_name": target_name, "toolchain_name": toolchain_name, **config} 34 | return template.render(context) 35 | 36 | 37 | def to_hex(s: Any) -> str: 38 | """Filter to convert integers to hex.""" 39 | return hex(int(s, 0)) 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | 4 | [mypy] 5 | # flake8-mypy expects the two following for sensible formatting 6 | show_column_numbers = True 7 | show_error_context = False 8 | 9 | # do not follow imports (except for ones found in typeshed) 10 | follow_imports = skip 11 | 12 | # since we're ignoring imports, writing .mypy_cache doesn't make any sense 13 | cache_dir = /dev/null 14 | 15 | 16 | ignore_missing_imports = True 17 | disallow_untyped_calls = True 18 | warn_return_any = True 19 | strict_optional = True 20 | warn_no_return = True 21 | warn_redundant_casts = True 22 | warn_unused_ignores = True 23 | disallow_untyped_defs = True 24 | check_untyped_defs = True 25 | 26 | [flake8] 27 | exclude = 28 | .git, 29 | .tox, 30 | .venv, 31 | __pycache__, 32 | docs, 33 | news, 34 | dist 35 | ignore = 36 | # H301: one import per line 37 | H301, 38 | # H306: imports not in alphabetical order 39 | H306, 40 | # W503: line break before binary operator (this is no longer PEP8 compliant) 41 | W503, 42 | # E203: whitespace before ':' (this is not PEP8 compliant) 43 | E203, 44 | per-file-ignores = 45 | # Package level init files improve user experience by short cutting imports. 46 | # F401: imported but unused 47 | __init__.py:F401 48 | # Don't require docstrings in tests. 49 | # We evaluate the need for them on case by case basis. 50 | tests/*.py:D1 51 | max-line-length = 120 52 | docstring-convention = google 53 | copyright-check = True 54 | select = E,F,W,C,B,H,D 55 | -------------------------------------------------------------------------------- /tests/project/_internal/test_progress.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | from unittest import TestCase, mock 6 | 7 | from mbed_tools.project._internal.progress import ProgressReporter, ProgressBar 8 | 9 | 10 | class TestProgressBar(TestCase): 11 | @mock.patch("mbed_tools.project._internal.progress.ProgressBar.update") 12 | def test_updates_progress_bar_with_correct_block_size(self, mock_bar_update): 13 | bar = ProgressBar(total=100) 14 | bar.update_progress(1, 1) 15 | 16 | mock_bar_update.assert_called_once_with(1) 17 | 18 | def test_sets_total_attribute_to_value_of_total_size(self): 19 | bar = ProgressBar() 20 | 21 | self.assertIsNone(bar.total) 22 | 23 | bar.update_progress(1, 2, total_size=33) 24 | 25 | self.assertEqual(bar.total, 33) 26 | 27 | 28 | @mock.patch("mbed_tools.project._internal.progress.ProgressBar", autospec=True) 29 | class TestProgressReporter(TestCase): 30 | def test_creates_progress_bar_on_begin_opcode(self, mock_progress_bar): 31 | reporter = ProgressReporter() 32 | reporter._cur_line = "begin" 33 | reporter.update(reporter.BEGIN, 1) 34 | 35 | mock_progress_bar.assert_called_once() 36 | 37 | def test_closes_progress_bar_on_end_opcode(self, mock_progress_bar): 38 | reporter = ProgressReporter() 39 | reporter.bar = mock_progress_bar() 40 | reporter.update(reporter.END, 1) 41 | 42 | reporter.bar.close.assert_called_once() 43 | reporter.bar.update.assert_not_called() 44 | -------------------------------------------------------------------------------- /tests/project/_internal/test_render_templates.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | from pathlib import Path 6 | from unittest import mock 7 | 8 | from mbed_tools.project._internal.render_templates import ( 9 | render_cmakelists_template, 10 | render_main_cpp_template, 11 | render_gitignore_template, 12 | ) 13 | 14 | 15 | @mock.patch("mbed_tools.project._internal.render_templates.datetime") 16 | class TestRenderTemplates: 17 | def test_renders_cmakelists_template(self, mock_datetime, tmp_path): 18 | the_year = 3999 19 | mock_datetime.datetime.now.return_value.year = the_year 20 | program_name = "mytestprogram" 21 | file_path = Path(tmp_path, "mytestpath") 22 | 23 | render_cmakelists_template(file_path, program_name) 24 | output = file_path.read_text() 25 | 26 | assert str(the_year) in output 27 | assert program_name in output 28 | 29 | def test_renders_main_cpp_template(self, mock_datetime, tmp_path): 30 | the_year = 3999 31 | mock_datetime.datetime.now.return_value.year = the_year 32 | file_path = Path(tmp_path, "mytestpath") 33 | 34 | render_main_cpp_template(file_path) 35 | 36 | assert str(the_year) in file_path.read_text() 37 | 38 | def test_renders_gitignore_template(self, _, tmp_path): 39 | file_path = Path(tmp_path, "mytestpath") 40 | 41 | render_gitignore_template(file_path) 42 | 43 | assert "cmake_build" in file_path.read_text() 44 | assert ".mbedbuild" in file_path.read_text() 45 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/detect_candidate_devices.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Detect Mbed devices connected to host computer.""" 6 | import platform 7 | from typing import Iterable 8 | 9 | from mbed_tools.devices._internal.candidate_device import CandidateDevice 10 | from mbed_tools.devices._internal.base_detector import DeviceDetector 11 | from mbed_tools.devices.exceptions import UnknownOSError 12 | 13 | 14 | def detect_candidate_devices() -> Iterable[CandidateDevice]: 15 | """Returns Candidates connected to host computer.""" 16 | detector = _get_detector_for_current_os() 17 | return detector.find_candidates() 18 | 19 | 20 | def _get_detector_for_current_os() -> DeviceDetector: 21 | """Returns DeviceDetector for current operating system.""" 22 | if platform.system() == "Windows": 23 | from mbed_tools.devices._internal.windows.device_detector import WindowsDeviceDetector 24 | 25 | return WindowsDeviceDetector() 26 | if platform.system() == "Linux": 27 | from mbed_tools.devices._internal.linux.device_detector import LinuxDeviceDetector 28 | 29 | return LinuxDeviceDetector() 30 | if platform.system() == "Darwin": 31 | from mbed_tools.devices._internal.darwin.device_detector import DarwinDeviceDetector 32 | 33 | return DarwinDeviceDetector() 34 | 35 | raise UnknownOSError( 36 | f"We have detected the OS you are running is '{platform.system()}'. " 37 | "Unfortunately we haven't implemented device detection support for this OS yet. Sorry!" 38 | ) 39 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/darwin/ioreg.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Interactions with `ioreg`.""" 6 | import plistlib 7 | import subprocess 8 | from typing import Any, Dict, Iterable, List, Optional, cast 9 | from xml.parsers.expat import ExpatError 10 | 11 | 12 | def get_data(device_name: str) -> List[Dict]: 13 | """Returns parsed output of `ioreg` call for a given device name.""" 14 | output = subprocess.check_output(["ioreg", "-a", "-r", "-n", device_name, "-l"]) 15 | if output: 16 | try: 17 | return cast(List[Dict], plistlib.loads(output)) 18 | except ExpatError: 19 | # Some devices seem to produce corrupt data 20 | pass 21 | return [] 22 | 23 | 24 | def get_io_dialin_device(device_name: str) -> Optional[str]: 25 | """Returns the value of "IODialinDevice" for a given device name.""" 26 | ioreg_data = get_data(device_name) 27 | dialin_device: Optional[str] = _find_first_property_value("IODialinDevice", ioreg_data) 28 | return dialin_device 29 | 30 | 31 | def _find_first_property_value(property_name: str, data: Iterable[Dict]) -> Any: 32 | """Finds a first value of a given proprety name in data from `ioreg`, returns None if not found.""" 33 | found_value = None 34 | for item in data: 35 | found_value = item.get( 36 | property_name, 37 | _find_first_property_value(property_name=property_name, data=item.get("IORegistryEntryChildren", [])), 38 | ) 39 | if found_value: 40 | break 41 | return found_value 42 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/serial_port_data_loader.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Loads serial port data.""" 6 | from typing import Optional, Generator, cast, List 7 | 8 | from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader, ComponentsLoader 9 | from mbed_tools.devices._internal.windows.usb_device_identifier import UsbIdentifier, parse_device_id 10 | from mbed_tools.devices._internal.windows.serial_port import SerialPort 11 | 12 | 13 | class SystemSerialPortInformation: 14 | """All information about the serial ports of the current system.""" 15 | 16 | def __init__(self, data_loader: SystemDataLoader) -> None: 17 | """Initialiser.""" 18 | self._serial_port_by_usb_id: Optional[dict] = None 19 | self._data_loader = data_loader 20 | 21 | def _load_data(self) -> None: 22 | self._serial_port_by_usb_id = { 23 | parse_device_id(p.pnp_id): p 24 | for p in cast( 25 | Generator[SerialPort, None, None], ComponentsLoader(self._data_loader, SerialPort).element_generator() 26 | ) 27 | } 28 | 29 | @property 30 | def serial_port_data_by_id(self) -> dict: 31 | """Gets system's serial ports by usb id.""" 32 | if not self._serial_port_by_usb_id: 33 | self._load_data() 34 | return self._serial_port_by_usb_id if self._serial_port_by_usb_id else dict() 35 | 36 | def get_serial_port_information(self, usb_id: UsbIdentifier) -> List[SerialPort]: 37 | """Gets all disk information for a given serial number.""" 38 | port = self.serial_port_data_by_id.get(usb_id) 39 | return [port] if port else list() 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2020 Arm Mbed. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | [build-system] 7 | requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] 8 | build-backend = "setuptools.build_meta" 9 | 10 | [ProjectConfig] 11 | # Only path variables can and must contain 'DIR', 'PATH' or 'ROOT' in their name as 12 | # these tokens are used to identify path variables from other variable types. 13 | PROJECT_ROOT = "." 14 | PROJECT_NAME = "Mbed Tools" 15 | PROJECT_UUID = "a58676f2-0505-4916-84ee-2ef116b07352" 16 | PACKAGE_NAME = "mbed-tools" 17 | NEWS_DIR = "news/" 18 | SOURCE_DIR = "src/mbed_tools" 19 | RELEASE_BRANCH_PATTERN = "^release.*$" 20 | MODULE_TO_DOCUMENT = "mbed_tools" 21 | CHANGELOG_FILE_PATH = "CHANGELOG.md" 22 | 23 | [tool.setuptools_scm] 24 | local_scheme = "no-local-version" 25 | 26 | [tool.towncrier] 27 | directory = "news" 28 | filename = "CHANGELOG.md" 29 | package = "mbed_tools" 30 | title_format = "{version} ({project_date})" 31 | start_string = """ 32 | [//]: # (begin_release_notes) 33 | """ 34 | 35 | [[tool.towncrier.type]] 36 | directory = "major" 37 | name = "Major changes" 38 | showcontent = true 39 | 40 | [[tool.towncrier.type]] 41 | directory = "feature" 42 | name = "Features" 43 | showcontent = true 44 | 45 | [[tool.towncrier.type]] 46 | directory = "bugfix" 47 | name = "Bugfixes" 48 | showcontent = true 49 | 50 | [[tool.towncrier.type]] 51 | directory = "doc" 52 | name = "Improved Documentation" 53 | showcontent = true 54 | 55 | [[tool.towncrier.type]] 56 | directory = "removal" 57 | name = "Deprecations and Removals" 58 | showcontent = true 59 | 60 | [[tool.towncrier.type]] 61 | directory = "misc" 62 | name = "Misc" 63 | showcontent = false 64 | 65 | [tool.black] 66 | diff = true 67 | color = true 68 | line-length = 120 69 | -------------------------------------------------------------------------------- /src/mbed_tools/targets/get_target.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Interface for accessing Targets from Mbed OS's targets.json. 6 | 7 | An instance of `mbed_tools.targets.target.Target` 8 | can be retrieved by calling one of the public functions. 9 | """ 10 | from mbed_tools.targets.exceptions import TargetError 11 | from mbed_tools.targets._internal import target_attributes 12 | 13 | 14 | def get_target_by_name(name: str, targets_json_data: dict) -> dict: 15 | """Returns a dictionary of attributes for the target whose name matches the name given. 16 | 17 | The target is as defined in the targets.json file found in the Mbed OS library. 18 | 19 | Args: 20 | name: the name of the Target to be returned 21 | targets_json_data: target definitions from targets.json 22 | 23 | Raises: 24 | TargetError: an error has occurred while fetching target 25 | """ 26 | try: 27 | return target_attributes.get_target_attributes(targets_json_data, name) 28 | except (FileNotFoundError, target_attributes.TargetAttributesError) as e: 29 | raise TargetError(e) from e 30 | 31 | 32 | def get_target_by_board_type(board_type: str, targets_json_data: dict) -> dict: 33 | """Returns the target whose name matches a board's build_type. 34 | 35 | The target is as defined in the targets.json file found in the Mbed OS library. 36 | 37 | Args: 38 | board_type: a board's board_type (see `mbed_tools.targets.board.Board`) 39 | targets_json_data: target definitions from targets.json 40 | 41 | Raises: 42 | TargetError: an error has occurred while fetching target 43 | """ 44 | return get_target_by_name(board_type, targets_json_data) 45 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/logical_disk.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines a Logical disk.""" 6 | 7 | from typing import NamedTuple, cast 8 | 9 | from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor 10 | 11 | 12 | class LogicalDiskMsdnDefinition(NamedTuple): 13 | """Msdn definition of a logical disk. 14 | 15 | See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-logicaldisk 16 | """ 17 | 18 | Access: int 19 | Availability: int 20 | BlockSize: int 21 | Caption: str 22 | ConfigManagerErrorCode: int 23 | ConfigManagerUserConfig: bool 24 | CreationClassName: str 25 | Description: str 26 | DeviceID: str 27 | ErrorCleared: bool 28 | ErrorDescription: str 29 | ErrorMethodology: str 30 | FreeSpace: int 31 | InstallDate: int 32 | LastErrorCode: int 33 | Name: str 34 | NumberOfBlocks: int 35 | PNPDeviceID: str 36 | PowerManagementCapabilities: list 37 | PowerManagementSupported: bool 38 | Purpose: str 39 | Size: int 40 | Status: str 41 | StatusInfo: int 42 | SystemCreationClassName: str 43 | SystemName: str 44 | 45 | 46 | class LogicalDisk(ComponentDescriptor): 47 | """Logical disk as defined in Windows API. 48 | 49 | See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-logicaldisk 50 | """ 51 | 52 | def __init__(self) -> None: 53 | """Initialiser.""" 54 | super().__init__(LogicalDiskMsdnDefinition, win32_class_name="CIM_LogicalDisk") 55 | 56 | @property 57 | def component_id(self) -> str: 58 | """Returns the device id field.""" 59 | return cast(str, self.get("DeviceID")) 60 | -------------------------------------------------------------------------------- /tests/devices/_internal/windows/test_component_descriptor_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import unittest 6 | from mbed_tools.devices._internal.windows.component_descriptor_utils import ( 7 | is_undefined_data_object, 8 | is_undefined_value, 9 | data_object_to_dict, 10 | UNKNOWN_VALUE, 11 | ) 12 | from collections import namedtuple 13 | import random 14 | 15 | 16 | def generate_valid_values(): 17 | return random.choice(["a test", 4646.454, 54, True]) 18 | 19 | 20 | def generate_undefined_values(): 21 | return random.choice([0, None, False, UNKNOWN_VALUE]) 22 | 23 | 24 | class TestUtilities(unittest.TestCase): 25 | def test_is_value_undefined(self): 26 | self.assertTrue(is_undefined_value(generate_undefined_values())) 27 | self.assertFalse(is_undefined_value(generate_valid_values())) 28 | 29 | def test_is_data_object_undefined(self): 30 | field_number = 30 31 | DataObjectType = namedtuple("data_object_example", [f"field{i}" for i in range(0, field_number)]) 32 | test1 = DataObjectType(*[generate_undefined_values() for i in range(0, field_number)]) 33 | self.assertTrue(is_undefined_data_object(test1)) 34 | test2 = DataObjectType(*[generate_valid_values() for i in range(0, field_number)]) 35 | self.assertFalse(is_undefined_data_object(test2)) 36 | 37 | def test_to_dict(self): 38 | field_number = 30 39 | DataObjectType = namedtuple("data_object_example", [f"field{i}" for i in range(0, field_number)]) 40 | expected_dictionary = { 41 | f"field{i}": random.choice([generate_valid_values(), generate_undefined_values()]) 42 | for i in range(0, field_number) 43 | } 44 | test = DataObjectType(**expected_dictionary) 45 | self.assertDictEqual(data_object_to_dict(test), expected_dictionary) 46 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/disk_partition_logical_disk_relationships.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines a Disk partition/Logical disk relationship.""" 6 | 7 | from typing import NamedTuple 8 | 9 | from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor 10 | 11 | 12 | class DiskToPartitionMsdnDefinition(NamedTuple): 13 | """Msdn definition of a disk partition - logical disk relationship. 14 | 15 | See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logicaldisktopartition 16 | """ 17 | 18 | EndingAddress: int 19 | StartingAddress: int 20 | Antecedent: str 21 | Dependent: str 22 | 23 | 24 | class DiskPartitionLogicalDiskRelationship(ComponentDescriptor): 25 | """Disk partition as defined in Windows API. 26 | 27 | See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logicaldisktopartition 28 | """ 29 | 30 | def __init__(self) -> None: 31 | """Initialiser.""" 32 | super().__init__(DiskToPartitionMsdnDefinition, win32_class_name="Win32_LogicalDiskToPartition") 33 | 34 | @property 35 | def component_id(self) -> str: 36 | """Returns the device id field.""" 37 | return f"{self.get('Antecedent')}->{self.get('Dependent')}" 38 | 39 | @staticmethod 40 | def _parse_reference(ref: str) -> str: 41 | """Parses a Win32 to only retain the value.""" 42 | return ref.replace("'", "").replace('"', "").split("=")[1] 43 | 44 | @property 45 | def logical_disk_id(self) -> str: 46 | """Gets the logical disk id.""" 47 | return DiskPartitionLogicalDiskRelationship._parse_reference(self.get("Dependent")) 48 | 49 | @property 50 | def disk_partition_id(self) -> str: 51 | """Gets the disk partition id.""" 52 | return DiskPartitionLogicalDiskRelationship._parse_reference(self.get("Antecedent")) 53 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/usb_controller.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines a USB controller.""" 6 | 7 | from typing import NamedTuple, cast 8 | 9 | from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor 10 | 11 | 12 | class UsbControllerMsdnDefinition(NamedTuple): 13 | """Msdn definition of a USB controller. 14 | 15 | See https://docs.microsoft.com/en-gb/windows/win32/cimwin32prov/win32-usbcontroller?redirectedfrom=MSDN 16 | Similar to https://docs.microsoft.com/en-gb/windows/win32/cimwin32prov/win32-usbcontrollerdevice 17 | """ 18 | 19 | Availability: int 20 | Caption: str 21 | ConfigManagerErrorCode: int 22 | ConfigManagerUserConfig: bool 23 | CreationClassName: str 24 | Description: str 25 | DeviceID: str 26 | ErrorCleared: bool 27 | ErrorDescription: str 28 | InstallDate: int 29 | LastErrorCode: int 30 | Manufacturer: str 31 | MaxNumberControlled: int 32 | Name: str 33 | PNPDeviceID: str 34 | PowerManagementCapabilities: list 35 | PowerManagementSupported: bool 36 | ProtocolSupported: int 37 | Status: str 38 | StatusInfo: int 39 | SystemCreationClassName: str 40 | SystemName: str 41 | TimeOfLastReset: int 42 | 43 | 44 | class UsbController(ComponentDescriptor): 45 | """USB Controller as defined in Windows API. 46 | 47 | See https://docs.microsoft.com/en-gb/windows/win32/cimwin32prov/win32-usbcontroller?redirectedfrom=MSDN 48 | Similar to https://docs.microsoft.com/en-gb/windows/win32/cimwin32prov/win32-usbcontrollerdevice 49 | """ 50 | 51 | def __init__(self) -> None: 52 | """Initialiser.""" 53 | super().__init__(UsbControllerMsdnDefinition, win32_class_name="Win32_USBController") 54 | 55 | @property 56 | def component_id(self) -> str: 57 | """Returns the device id field.""" 58 | return cast(str, self.get("DeviceID")) 59 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/device_detector.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines a device detector for Windows.""" 6 | from pathlib import Path 7 | from typing import List 8 | 9 | from mbed_tools.devices._internal.base_detector import DeviceDetector 10 | from mbed_tools.devices._internal.candidate_device import CandidateDevice 11 | from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader 12 | from mbed_tools.devices._internal.windows.usb_data_aggregation import SystemUsbData, AggregatedUsbData 13 | 14 | 15 | class WindowsDeviceDetector(DeviceDetector): 16 | """Windows specific implementation of device detection.""" 17 | 18 | def __init__(self) -> None: 19 | """Initialiser.""" 20 | self._data_loader = SystemDataLoader() 21 | 22 | def find_candidates(self) -> List[CandidateDevice]: 23 | """Return a generator of Candidates.""" 24 | return [ 25 | WindowsDeviceDetector.map_to_candidate(usb) 26 | for usb in SystemUsbData(data_loader=self._data_loader).all() 27 | if WindowsDeviceDetector.is_valid_candidate(usb) 28 | ] 29 | 30 | @staticmethod 31 | def map_to_candidate(usb_data: AggregatedUsbData) -> CandidateDevice: 32 | """Maps a USB device to a candidate.""" 33 | serial_port = next(iter(usb_data.get("serial_port")), None) 34 | uid = usb_data.uid 35 | return CandidateDevice( 36 | product_id=uid.product_id, 37 | vendor_id=uid.vendor_id, 38 | mount_points=tuple(Path(disk.component_id) for disk in usb_data.get("disks")), 39 | serial_number=uid.uid.presumed_serial_number, 40 | serial_port=serial_port.port_name if serial_port else None, 41 | ) 42 | 43 | @staticmethod 44 | def is_valid_candidate(usb_data: AggregatedUsbData) -> bool: 45 | """States whether the usb device is a valid candidate or not.""" 46 | return usb_data.is_composite and usb_data.is_associated_with_disk 47 | -------------------------------------------------------------------------------- /src/mbed_tools/project/_internal/render_templates.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Render jinja templates required by the project package.""" 6 | import datetime 7 | 8 | from pathlib import Path 9 | 10 | import jinja2 11 | 12 | TEMPLATES_DIRECTORY = Path("_internal", "templates") 13 | 14 | 15 | def render_cmakelists_template(cmakelists_file: Path, program_name: str) -> None: 16 | """Render CMakeLists.tmpl with the copyright year and program name as the app target name. 17 | 18 | Args: 19 | cmakelists_file: The path where CMakeLists.txt will be written. 20 | program_name: The name of the program, will be used as the app target name. 21 | """ 22 | cmakelists_file.write_text( 23 | render_jinja_template( 24 | "CMakeLists.tmpl", {"program_name": program_name, "year": str(datetime.datetime.now().year)} 25 | ) 26 | ) 27 | 28 | 29 | def render_main_cpp_template(main_cpp: Path) -> None: 30 | """Render a basic main.cpp which prints a hello message and returns. 31 | 32 | Args: 33 | main_cpp: Path where the main.cpp file will be written. 34 | """ 35 | main_cpp.write_text(render_jinja_template("main.tmpl", {"year": str(datetime.datetime.now().year)})) 36 | 37 | 38 | def render_gitignore_template(gitignore: Path) -> None: 39 | """Write out a basic gitignore file ignoring the build and config directory. 40 | 41 | Args: 42 | gitignore: The path where the gitignore file will be written. 43 | """ 44 | gitignore.write_text(render_jinja_template("gitignore.tmpl", {})) 45 | 46 | 47 | def render_jinja_template(template_name: str, context: dict) -> str: 48 | """Render a jinja template. 49 | 50 | Args: 51 | template_name: The name of the template being rendered. 52 | context: Data to render into the jinja template. 53 | """ 54 | env = jinja2.Environment(loader=jinja2.PackageLoader("mbed_tools.project", str(TEMPLATES_DIRECTORY))) 55 | template = env.get_template(template_name) 56 | return template.render(context) 57 | -------------------------------------------------------------------------------- /tests/devices/_internal/test_detect_candidate_devices.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import pytest 6 | from unittest import mock 7 | 8 | from tests.devices.markers import windows_only, darwin_only, linux_only 9 | from mbed_tools.devices._internal.base_detector import DeviceDetector 10 | from mbed_tools.devices.exceptions import UnknownOSError 11 | from mbed_tools.devices._internal.detect_candidate_devices import ( 12 | detect_candidate_devices, 13 | _get_detector_for_current_os, 14 | ) 15 | 16 | 17 | class TestDetectCandidateDevices: 18 | @mock.patch("mbed_tools.devices._internal.detect_candidate_devices._get_detector_for_current_os") 19 | def test_returns_candidates_using_os_specific_detector(self, _get_detector_for_current_os): 20 | detector = mock.Mock(spec_set=DeviceDetector) 21 | _get_detector_for_current_os.return_value = detector 22 | assert detect_candidate_devices() == detector.find_candidates.return_value 23 | 24 | 25 | class TestGetDetectorForCurrentOS: 26 | @windows_only 27 | def test_windows_uses_correct_module(self): 28 | from mbed_tools.devices._internal.windows.device_detector import WindowsDeviceDetector 29 | 30 | assert isinstance(_get_detector_for_current_os(), WindowsDeviceDetector) 31 | 32 | @darwin_only 33 | def test_darwin_uses_correct_module(self): 34 | from mbed_tools.devices._internal.darwin.device_detector import DarwinDeviceDetector 35 | 36 | assert isinstance(_get_detector_for_current_os(), DarwinDeviceDetector) 37 | 38 | @linux_only 39 | def test_linux_uses_correct_module(self): 40 | from mbed_tools.devices._internal.linux.device_detector import LinuxDeviceDetector 41 | 42 | assert isinstance(_get_detector_for_current_os(), LinuxDeviceDetector) 43 | 44 | @mock.patch("platform.system") 45 | def test_raises_when_os_is_unknown(self, platform_system): 46 | os_name = "SomethingNobodyUses" 47 | platform_system.return_value = os_name 48 | 49 | with pytest.raises(UnknownOSError): 50 | _get_detector_for_current_os() 51 | -------------------------------------------------------------------------------- /src/mbed_tools/build/build.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Configure and build a CMake project.""" 6 | import logging 7 | import pathlib 8 | import subprocess 9 | 10 | from typing import Optional 11 | 12 | from mbed_tools.build.exceptions import MbedBuildError 13 | 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | def build_project(build_dir: pathlib.Path, target: Optional[str] = None) -> None: 19 | """Build a project using CMake to invoke Ninja. 20 | 21 | Args: 22 | build_dir: Path to the CMake build tree. 23 | target: The CMake target to build (e.g 'install') 24 | """ 25 | _check_ninja_found() 26 | target_flag = ["--target", target] if target is not None else [] 27 | _cmake_wrapper("--build", str(build_dir), *target_flag) 28 | 29 | 30 | def generate_build_system(source_dir: pathlib.Path, build_dir: pathlib.Path, profile: str) -> None: 31 | """Configure a project using CMake. 32 | 33 | Args: 34 | source_dir: Path to the CMake source tree. 35 | build_dir: Path to the CMake build tree. 36 | profile: The Mbed build profile (develop, debug or release). 37 | """ 38 | _check_ninja_found() 39 | _cmake_wrapper("-S", str(source_dir), "-B", str(build_dir), "-GNinja", f"-DCMAKE_BUILD_TYPE={profile}") 40 | 41 | 42 | def _cmake_wrapper(*cmake_args: str) -> None: 43 | try: 44 | logger.debug("Running CMake with args: %s", cmake_args) 45 | subprocess.run(["cmake", *cmake_args], check=True) 46 | except FileNotFoundError: 47 | raise MbedBuildError("Could not find CMake. Please ensure CMake is installed and added to PATH.") 48 | except subprocess.CalledProcessError: 49 | raise MbedBuildError("CMake invocation failed!") 50 | 51 | 52 | def _check_ninja_found() -> None: 53 | try: 54 | subprocess.run(["ninja", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 55 | except FileNotFoundError: 56 | raise MbedBuildError( 57 | "Could not find the 'Ninja' build program. Please ensure 'Ninja' is installed and added to PATH." 58 | ) 59 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/disk_partition.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines a Disk partition.""" 6 | 7 | from typing import NamedTuple, cast 8 | 9 | from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor 10 | 11 | 12 | class DiskPartitionMsdnDefinition(NamedTuple): 13 | """Msdn definition of a disk partition. 14 | 15 | See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-diskpartition 16 | """ 17 | 18 | AdditionalAvailability: int 19 | Availability: int 20 | PowerManagementCapabilities: list 21 | IdentifyingDescriptions: list 22 | MaxQuiesceTime: int 23 | OtherIdentifyingInfo: int 24 | StatusInfo: int 25 | PowerOnHours: int 26 | TotalPowerOnHours: int 27 | Access: int 28 | BlockSize: int 29 | Bootable: bool 30 | BootPartition: bool 31 | Caption: str 32 | ConfigManagerErrorCode: int 33 | ConfigManagerUserConfig: bool 34 | CreationClassName: str 35 | Description: str 36 | DeviceID: str 37 | DiskIndex: int 38 | ErrorCleared: bool 39 | ErrorDescription: str 40 | ErrorMethodology: str 41 | HiddenSectors: int 42 | Index: int 43 | InstallDate: int 44 | LastErrorCode: int 45 | Name: str 46 | NumberOfBlocks: int 47 | PNPDeviceID: str 48 | PowerManagementSupported: bool 49 | PrimaryPartition: bool 50 | Purpose: str 51 | RewritePartition: bool 52 | Size: int 53 | StartingOffset: int 54 | Status: str 55 | SystemCreationClassName: str 56 | SystemName: str 57 | Type: str 58 | 59 | 60 | class DiskPartition(ComponentDescriptor): 61 | """Disk partition as defined in Windows API. 62 | 63 | See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-diskpartition 64 | """ 65 | 66 | def __init__(self) -> None: 67 | """Initialiser.""" 68 | super().__init__(DiskPartitionMsdnDefinition, win32_class_name="Win32_DiskPartition") 69 | 70 | @property 71 | def component_id(self) -> str: 72 | """Returns the device id field.""" 73 | return cast(str, self.get("DeviceID")) 74 | -------------------------------------------------------------------------------- /src/mbed_tools/build/flash.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Flash binary onto the connected device.""" 6 | 7 | import shutil 8 | import os 9 | import pathlib 10 | import platform 11 | 12 | from mbed_tools.build.exceptions import BinaryFileNotFoundError 13 | 14 | 15 | def _flash_dev(disk: pathlib.Path, image_path: pathlib.Path) -> None: 16 | """Flash device using copy method. 17 | 18 | Args: 19 | disk: Device mount point. 20 | image_path: Image file to be copied to device. 21 | """ 22 | shutil.copy(image_path, disk, follow_symlinks=False) 23 | if not platform.system() == "Windows": 24 | os.sync() 25 | 26 | 27 | def _build_binary_file_path(program_path: pathlib.Path, build_dir: pathlib.Path, hex_file: bool) -> pathlib.Path: 28 | """Build binary file name. 29 | 30 | Args: 31 | program_path: Path to the Mbed project. 32 | build_dir: Path to the CMake build folder. 33 | hex_file: Use hex file. 34 | 35 | Returns: 36 | Path to binary file. 37 | 38 | Raises: 39 | BinaryFileNotFoundError: binary file not found in the path specified 40 | """ 41 | fw_fbase = build_dir / program_path.name 42 | fw_file = fw_fbase.with_suffix(".hex" if hex_file else ".bin") 43 | if not fw_file.exists(): 44 | raise BinaryFileNotFoundError(f"Build program file (firmware) not found {fw_file}") 45 | return fw_file 46 | 47 | 48 | def flash_binary( 49 | mount_point: pathlib.Path, program_path: pathlib.Path, build_dir: pathlib.Path, mbed_target: str, hex_file: bool 50 | ) -> pathlib.Path: 51 | """Flash binary onto a device. 52 | 53 | Look through the connected devices and flash the binary if the connected and built target matches. 54 | 55 | Args: 56 | mount_point: Mount point of the target device. 57 | program_path: Path to the Mbed project. 58 | build_dir: Path to the CMake build folder. 59 | mbed_target: The name of the Mbed target to build for. 60 | hex_file: Use hex file. 61 | """ 62 | fw_file = _build_binary_file_path(program_path, build_dir, hex_file) 63 | _flash_dev(mount_point, fw_file) 64 | return fw_file 65 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/usb_hub.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines a USB hub.""" 6 | 7 | from typing import NamedTuple, cast 8 | 9 | from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor 10 | 11 | 12 | class UsbHubMsdnDefinition(NamedTuple): 13 | """Msdn definition of a Usb hub. 14 | 15 | See https://docs.microsoft.com/en-gb/previous-versions/windows/desktop/cimwin32a/win32-usbhub?redirectedfrom=MSDN 16 | See also https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-usbdevice 17 | """ 18 | 19 | Availability: int 20 | Caption: str 21 | ClassCode: int 22 | ConfigManagerUserConfig: bool 23 | CreationClassName: str 24 | CurrentAlternateSettings: list 25 | CurrentConfigValue: int 26 | Description: str 27 | ErrorCleared: bool 28 | ErrorDescription: str 29 | GangSwitched: bool 30 | InstallDate: int 31 | LastErrorCode: int 32 | NumberOfConfigs: int 33 | NumberOfPorts: int 34 | PNPDeviceID: str 35 | PowerManagementCapabilities: list 36 | PowerManagementSupported: bool 37 | ProtocolCode: int 38 | Status: str 39 | StatusInfo: int 40 | SubclassCode: int 41 | SystemCreationClassName: str 42 | SystemName: str 43 | USBVersion: int 44 | ConfigManagerErrorCode: int 45 | DeviceID: str 46 | Name: str 47 | 48 | 49 | class UsbHub(ComponentDescriptor): 50 | """USB Hub as defined in Windows API. 51 | 52 | See https://docs.microsoft.com/en-gb/previous-versions/windows/desktop/cimwin32a/win32-usbhub?redirectedfrom=MSDN 53 | Seems similar to https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-usbhub 54 | """ 55 | 56 | def __init__(self) -> None: 57 | """Initialiser.""" 58 | super().__init__(UsbHubMsdnDefinition, win32_class_name="Win32_USBHub") 59 | 60 | @property 61 | def component_id(self) -> str: 62 | """Returns the device id field.""" 63 | return cast(str, self.get("DeviceID")) 64 | 65 | @property 66 | def pnp_id(self) -> str: 67 | """Returns the plug and play id field.""" 68 | return cast(str, self.get("PNPDeviceID")) 69 | -------------------------------------------------------------------------------- /ci_scripts/prep-release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | INITIAL_HEAD=$(git rev-parse HEAD) 6 | printf "HEAD before auto generating docs and target_database = ${INITIAL_HEAD}\n" 7 | 8 | printf "Attempting to update the target database snapshot with the latest changes\n" 9 | python ci_scripts/sync_board_database.py -vvv 10 | git add src/mbed_tools/targets/_internal/data/board_database_snapshot.json 11 | git add news/ 12 | 13 | printf "Adding license headers\n" 14 | pre-commit run licenseheaders --all-files > /dev/null 2>&1 || true 15 | 16 | if ! NEW_VERSION=$(python ci_scripts/bump_version.py -vvv -n news/); then 17 | printf "No news files detected. Exiting.\n" 18 | exit 0 19 | fi 20 | 21 | # Update the index so we can check any recently staged changes 22 | git update-index -q --ignore-submodules --refresh 23 | 24 | if ! git diff-index --cached --quiet HEAD --ignore-submodules -- 25 | then 26 | printf "Found staged changes to commit after target database update: $(git diff-index --cached --name-status --ignore-submodules HEAD)\n" 27 | # Towncrier will fail if it detects a news file that has not been committed to 28 | # the repository. So we need to ensure the news file generated by 29 | # sync_board_database.py was committed before running towncrier. 30 | git commit -m "Prepare mbed-tools release ${NEW_VERSION}" 31 | fi 32 | 33 | # Generate changelog from news fragments 34 | towncrier --yes --version "${NEW_VERSION}" 35 | git add ./CHANGELOG.md 36 | 37 | LATEST_HEAD=$(git rev-parse HEAD) 38 | if [ "$LATEST_HEAD" = "$INITIAL_HEAD" ]; then 39 | printf "No updates were made during the release process, committing CHANGELOG\n" 40 | git commit -m "Prepare mbed-tools release ${NEW_VERSION}" 41 | else 42 | printf "A commit was made to update the docs and/or target database. Amending the commit to include CHANGELOG\n" 43 | git commit --amend -C HEAD 44 | fi 45 | 46 | printf "Tagging version ${NEW_VERSION}" 47 | git tag "${NEW_VERSION}" 48 | 49 | # Push the commit and tag to the remote. Relies on `GIT_TOKEN` environment 50 | # variable being set. 51 | printf "Pushing tag and master branch to remote\n" 52 | git push "https://${GIT_TOKEN}@github.com/ARMmbed/mbed-tools.git" master 53 | git push "https://${GIT_TOKEN}@github.com/ARMmbed/mbed-tools.git" "${NEW_VERSION}" 54 | -------------------------------------------------------------------------------- /tests/regression/test_configure.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import tempfile 7 | import pathlib 8 | import json 9 | 10 | from unittest import TestCase 11 | 12 | from click.testing import CliRunner 13 | 14 | from mbed_tools.cli.configure import configure 15 | 16 | target_json = json.dumps( 17 | { 18 | "TARGET": { 19 | "core": None, 20 | "trustzone": False, 21 | "default_toolchain": "ARM", 22 | "supported_toolchains": None, 23 | "extra_labels": [], 24 | "supported_form_factors": [], 25 | "components": [], 26 | "is_disk_virtual": False, 27 | "macros": [], 28 | "device_has": [], 29 | "features": [], 30 | "detect_code": [], 31 | "c_lib": "std", 32 | "bootloader_supported": False, 33 | "static_memory_defines": True, 34 | "printf_lib": "minimal-printf", 35 | "supported_c_libs": {"arm": ["std"], "gcc_arm": ["std", "small"], "iar": ["std"]}, 36 | "supported_application_profiles": ["full"], 37 | } 38 | } 39 | ) 40 | 41 | mbed_app_json = json.dumps( 42 | {"target_overrides": {"*": {"target.c_lib": "small", "target.printf_lib": "minimal-printf"}}} 43 | ) 44 | 45 | 46 | class TestConfigureRegression(TestCase): 47 | def test_generate_config_called_with_correct_arguments(self): 48 | with tempfile.TemporaryDirectory() as tmpDir: 49 | tmpDirPath = pathlib.Path(tmpDir) 50 | pathlib.Path(tmpDirPath / "mbed-os.lib").write_text("https://github.com/ARMmbed/mbed-os") 51 | pathlib.Path(tmpDirPath / "mbed_app.json").write_text(mbed_app_json) 52 | pathlib.Path(tmpDirPath / "mbed-os").mkdir() 53 | pathlib.Path(tmpDirPath / "mbed-os" / "targets").mkdir() 54 | pathlib.Path(tmpDirPath / "mbed-os" / "targets" / "targets.json").write_text(target_json) 55 | 56 | result = CliRunner().invoke( 57 | configure, ["-m", "Target", "-t", "gcc_arm", "-p", tmpDir], catch_exceptions=False 58 | ) 59 | self.assertIn("mbed_config.cmake has been generated and written to", result.output) 60 | -------------------------------------------------------------------------------- /src/mbed_tools/cli/sterm.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Command to launch a serial terminal to a connected Mbed device.""" 6 | from typing import Any, Optional 7 | 8 | import click 9 | 10 | from mbed_tools.cli.build import _get_target_id 11 | from mbed_tools.devices import find_connected_device, get_connected_devices 12 | from mbed_tools.devices.exceptions import MbedDevicesError 13 | from mbed_tools.sterm import terminal 14 | 15 | 16 | @click.command( 17 | help="Open a serial terminal to a connected Mbed Enabled device, or connect to a user-specified COM port." 18 | ) 19 | @click.option( 20 | "-p", 21 | "--port", 22 | type=str, 23 | help="Communication port. Default: auto-detect. Specifying this will also ignore the -m/--mbed-target option.", 24 | ) 25 | @click.option("-b", "--baudrate", type=int, default=9600, show_default=True, help="Communication baudrate.") 26 | @click.option( 27 | "-e", 28 | "--echo", 29 | default="on", 30 | show_default=True, 31 | type=click.Choice(["on", "off"], case_sensitive=False), 32 | help="Switch local echo on/off.", 33 | ) 34 | @click.option("-m", "--mbed-target", type=str, help="Mbed target to detect. Example: K64F, NUCLEO_F401RE, NRF51822...") 35 | def sterm(port: str, baudrate: int, echo: str, mbed_target: str) -> None: 36 | """Launches a serial terminal to a connected device.""" 37 | if port is None: 38 | port = _find_target_serial_port_or_default(mbed_target) 39 | 40 | terminal.run(port, baudrate, echo=True if echo == "on" else False) 41 | 42 | 43 | def _get_connected_mbed_devices() -> Any: 44 | connected_devices = get_connected_devices() 45 | if not connected_devices.identified_devices: 46 | raise MbedDevicesError("No Mbed enabled devices found.") 47 | 48 | return connected_devices.identified_devices 49 | 50 | 51 | def _find_target_serial_port_or_default(target: Optional[str]) -> Any: 52 | if target is None: 53 | # just return the first valid device found 54 | device, *_ = _get_connected_mbed_devices() 55 | else: 56 | target_name, target_id = _get_target_id(target) 57 | device = find_connected_device(target_name.upper(), target_id) 58 | return device.serial_port 59 | -------------------------------------------------------------------------------- /src/mbed_tools/project/_internal/progress.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Progress bar for git operations.""" 6 | import sys 7 | 8 | from typing import Optional, Any 9 | 10 | from git import RemoteProgress 11 | from tqdm import tqdm 12 | 13 | 14 | class ProgressBar(tqdm): 15 | """tqdm progress bar that can be used in a callback context.""" 16 | 17 | total: Any 18 | 19 | def update_progress(self, block_num: float = 1, block_size: float = 1, total_size: float = None) -> None: 20 | """Update the progress bar. 21 | 22 | Args: 23 | block_num: Number of the current block. 24 | block_size: Size of the current block. 25 | total_size: Total size of all expected blocks. 26 | """ 27 | if total_size is not None and self.total != total_size: 28 | self.total = total_size 29 | self.update(block_num * block_size - self.n) 30 | 31 | 32 | class ProgressReporter(RemoteProgress): 33 | """GitPython RemoteProgress subclass that displays a progress bar for git fetch and push operations.""" 34 | 35 | def __init__(self, *args: Any, name: str = "", **kwargs: Any) -> None: 36 | """Initialiser. 37 | 38 | Args: 39 | name: The name of the git repository to report progress on. 40 | """ 41 | self.name = name 42 | super().__init__(*args, **kwargs) 43 | 44 | def update(self, op_code: int, cur_count: float, max_count: Optional[float] = None, message: str = "") -> None: 45 | """Called whenever the progress changes. 46 | 47 | Args: 48 | op_code: Integer describing the stage of the current operation. 49 | cur_count: Current item count. 50 | max_count: Maximum number of items expected. 51 | message: Message string describing the number of bytes transferred in the WRITING operation. 52 | """ 53 | if self.BEGIN & op_code: 54 | self.bar = ProgressBar(total=max_count, file=sys.stderr, leave=False) 55 | 56 | self.bar.desc = f"{self.name} {self._cur_line}" 57 | self.bar.update_progress(block_num=cur_count, total_size=max_count) 58 | 59 | if self.END & op_code: 60 | self.bar.close() 61 | -------------------------------------------------------------------------------- /tests/targets/test_get_target.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | from unittest import TestCase, mock 6 | from mbed_tools.targets.get_target import get_target_by_board_type, get_target_by_name 7 | from mbed_tools.targets.exceptions import TargetError 8 | from mbed_tools.targets._internal.target_attributes import TargetAttributesError 9 | 10 | 11 | class TestGetTarget(TestCase): 12 | @mock.patch("mbed_tools.targets.get_target.target_attributes.get_target_attributes") 13 | def test_get_by_name(self, mock_target_attrs): 14 | target_name = "Target" 15 | targets_json_file_path = "targets.json" 16 | 17 | result = get_target_by_name(target_name, targets_json_file_path) 18 | 19 | self.assertEqual(result, mock_target_attrs.return_value) 20 | mock_target_attrs.assert_called_once_with(targets_json_file_path, target_name) 21 | 22 | @mock.patch("mbed_tools.targets.get_target.target_attributes.get_target_attributes") 23 | def test_get_by_name_raises_target_error_when_target_json_not_found(self, mock_target_attrs): 24 | target_name = "Target" 25 | targets_json_file_path = "not-targets.json" 26 | mock_target_attrs.side_effect = FileNotFoundError 27 | 28 | with self.assertRaises(TargetError): 29 | get_target_by_name(target_name, targets_json_file_path) 30 | 31 | @mock.patch("mbed_tools.targets.get_target.target_attributes.get_target_attributes") 32 | def test_get_by_name_raises_target_error_when_target_attr_collection_fails(self, mock_target_attrs): 33 | target_name = "Target" 34 | targets_json_file_path = "targets.json" 35 | mock_target_attrs.side_effect = TargetAttributesError 36 | 37 | with self.assertRaises(TargetError): 38 | get_target_by_name(target_name, targets_json_file_path) 39 | 40 | @mock.patch("mbed_tools.targets.get_target.get_target_by_name") 41 | def test_get_by_board_type(self, mock_get_target_by_name): 42 | board_type = "Board" 43 | path_to_mbed_program = "somewhere" 44 | 45 | result = get_target_by_board_type(board_type, path_to_mbed_program) 46 | 47 | self.assertEqual(result, mock_get_target_by_name.return_value) 48 | mock_get_target_by_name.assert_called_once_with(board_type, path_to_mbed_program) 49 | -------------------------------------------------------------------------------- /tests/devices/_internal/darwin/test_ioreg.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import plistlib 6 | from unittest import TestCase, mock 7 | 8 | from mbed_tools.devices._internal.darwin.ioreg import get_data, get_io_dialin_device 9 | 10 | 11 | @mock.patch("mbed_tools.devices._internal.darwin.ioreg.subprocess.check_output") 12 | class TestGetData(TestCase): 13 | def test_returns_data_from_ioreg_call(self, check_output): 14 | check_output.return_value = b""" 15 | 16 | foo 17 | 18 | """ 19 | 20 | self.assertEqual(get_data("some device"), ["foo"]) 21 | check_output.assert_called_once_with(["ioreg", "-a", "-r", "-n", "some device", "-l"]) 22 | 23 | def test_handles_corrupt_data_gracefully(self, check_output): 24 | check_output.return_value = b""" 25 | S\xc3\xbfn\x06P\xc2\x87TT%A\t\xc2\x87 26 | """ 27 | 28 | self.assertEqual(get_data("doesn't matter"), []) 29 | 30 | 31 | class TestGetIoDialinDevice(TestCase): 32 | @mock.patch("mbed_tools.devices._internal.darwin.ioreg.get_data") 33 | def test_identifies_nested_io_dialin_device_in_given_ioreg_data(self, get_data): 34 | plist = b""" 35 | 36 | 37 | IORegistryEntryChildren 38 | 39 | 40 | 41 | IORegistryEntryChildren 42 | 43 | 44 | IORegistryEntryChildren 45 | 46 | 47 | IODialinDevice 48 | /dev/tty.usbmodem1234 49 | 50 | 51 | 52 | 53 | 54 | 55 | """ 56 | data = plistlib.loads(plist) 57 | get_data.return_value = data 58 | 59 | self.assertEqual(get_io_dialin_device("some_device"), "/dev/tty.usbmodem1234") 60 | get_data.assert_called_once_with("some_device") 61 | -------------------------------------------------------------------------------- /tests/build/test_build.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import subprocess 6 | 7 | from unittest import mock 8 | 9 | import pytest 10 | 11 | from mbed_tools.build.build import build_project, generate_build_system 12 | from mbed_tools.build.exceptions import MbedBuildError 13 | 14 | 15 | @pytest.fixture 16 | def subprocess_run(): 17 | with mock.patch("mbed_tools.build.build.subprocess.run", autospec=True) as subproc: 18 | yield subproc 19 | 20 | 21 | class TestBuildProject: 22 | def test_invokes_cmake_with_correct_args(self, subprocess_run): 23 | build_project(build_dir="cmake_build", target="install") 24 | 25 | subprocess_run.assert_called_with(["cmake", "--build", "cmake_build", "--target", "install"], check=True) 26 | 27 | def test_invokes_cmake_with_correct_args_if_no_target_passed(self, subprocess_run): 28 | build_project(build_dir="cmake_build") 29 | 30 | subprocess_run.assert_called_with(["cmake", "--build", "cmake_build"], check=True) 31 | 32 | def test_raises_build_error_if_cmake_invocation_fails(self, subprocess_run): 33 | subprocess_run.side_effect = (None, subprocess.CalledProcessError(1, "")) 34 | 35 | with pytest.raises(MbedBuildError, match="CMake invocation failed"): 36 | build_project(build_dir="cmake_build") 37 | 38 | 39 | class TestConfigureProject: 40 | def test_invokes_cmake_with_correct_args(self, subprocess_run): 41 | source_dir = "source_dir" 42 | build_dir = "cmake_build" 43 | profile = "debug" 44 | 45 | generate_build_system(source_dir, build_dir, profile) 46 | 47 | subprocess_run.assert_called_with( 48 | ["cmake", "-S", source_dir, "-B", build_dir, "-GNinja", f"-DCMAKE_BUILD_TYPE={profile}"], check=True 49 | ) 50 | 51 | def test_raises_when_ninja_cannot_be_found(self, subprocess_run): 52 | subprocess_run.side_effect = FileNotFoundError 53 | 54 | with pytest.raises(MbedBuildError, match="Ninja"): 55 | generate_build_system("", "", "") 56 | 57 | def test_raises_when_cmake_cannot_be_found(self, subprocess_run): 58 | subprocess_run.side_effect = (None, FileNotFoundError) 59 | 60 | with pytest.raises(MbedBuildError, match="Could not find CMake"): 61 | generate_build_system("", "", "") 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Package definition for PyPI.""" 6 | import os 7 | 8 | from setuptools import setup, find_packages 9 | 10 | PROJECT_SLUG = "mbed-tools" 11 | SOURCE_DIR = "src/mbed_tools" 12 | 13 | repository_dir = os.path.dirname(__file__) 14 | 15 | # Use readme needed as long description in PyPI 16 | with open(os.path.join(repository_dir, "README.md"), encoding="utf8") as fh: 17 | long_description = fh.read() 18 | 19 | setup( 20 | author="Mbed team", 21 | author_email="support@mbed.com", 22 | classifiers=[ 23 | "Development Status :: 4 - Beta", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: Apache Software License", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3 :: Only", 28 | "Programming Language :: Python :: 3.6", 29 | "Programming Language :: Python :: 3.7", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Topic :: Software Development :: Build Tools", 33 | "Topic :: Software Development :: Embedded Systems", 34 | ], 35 | description="Command line interface for Mbed OS.", 36 | keywords="Arm Mbed OS MbedOS cli command line tools", 37 | include_package_data=True, 38 | install_requires=[ 39 | "python-dotenv", 40 | "Click>=7.1,<9", 41 | "GitPython", 42 | "tqdm", 43 | "tabulate", 44 | "dataclasses; python_version<'3.7'", 45 | "requests>=2.20", 46 | "pywin32; platform_system=='Windows'", 47 | "psutil; platform_system=='Linux'", 48 | "pyudev; platform_system=='Linux'", 49 | "typing-extensions", 50 | "Jinja2", 51 | "pyserial", 52 | ], 53 | license="Apache 2.0", 54 | long_description_content_type="text/markdown", 55 | long_description=long_description, 56 | name=PROJECT_SLUG, 57 | package_dir={"": "src"}, 58 | packages=find_packages(where="src"), 59 | python_requires=">=3.6,<4", 60 | url=f"https://github.com/ARMmbed/{PROJECT_SLUG}", 61 | entry_points={ 62 | "console_scripts": [ 63 | "mbedtools=mbed_tools.cli.main:cli", 64 | "mbed-tools=mbed_tools.cli.main:cli", 65 | "mbed_tools=mbed_tools.cli.main:cli", 66 | ] 67 | }, 68 | ) 69 | -------------------------------------------------------------------------------- /tests/build/_internal/test_cmake_file.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import pytest 6 | 7 | from mbed_tools.build._internal.cmake_file import render_mbed_config_cmake_template 8 | from mbed_tools.build._internal.config.config import Config 9 | from mbed_tools.build._internal.config.source import ConfigSetting, prepare 10 | 11 | 12 | TOOLCHAIN_NAME = "gcc" 13 | 14 | 15 | @pytest.fixture() 16 | def fake_target(): 17 | return { 18 | "labels": ["foo"], 19 | "extra_labels": ["morefoo"], 20 | "features": ["bar"], 21 | "components": ["baz"], 22 | "macros": ["macbaz"], 23 | "device_has": ["stuff"], 24 | "c_lib": ["c_lib"], 25 | "core": ["core"], 26 | "printf_lib": ["printf_lib"], 27 | "supported_form_factors": ["arduino"], 28 | "supported_c_libs": {TOOLCHAIN_NAME: ["ginormous"]}, 29 | "supported_application_profiles": ["full", "bare-metal"], 30 | } 31 | 32 | 33 | class TestRendersCMakeListsFile: 34 | def test_returns_rendered_content(self, fake_target): 35 | config = Config(prepare(fake_target)) 36 | result = render_mbed_config_cmake_template(config, TOOLCHAIN_NAME, "target_name") 37 | 38 | for label in fake_target["labels"] + fake_target["extra_labels"]: 39 | assert label in result 40 | 41 | for macro in fake_target["features"] + fake_target["components"] + [TOOLCHAIN_NAME]: 42 | assert macro in result 43 | 44 | for toolchain in fake_target["supported_c_libs"]: 45 | assert toolchain in result 46 | for supported_c_libs in toolchain: 47 | assert supported_c_libs in result 48 | 49 | for supported_application_profiles in fake_target["supported_application_profiles"]: 50 | assert supported_application_profiles in result 51 | 52 | def test_returns_quoted_content(self, fake_target): 53 | config = Config(prepare(fake_target)) 54 | 55 | # Add an option whose value contains quotes to the config. 56 | config["config"] = [ 57 | ConfigSetting( 58 | name="mqtt-host", namespace="iotc", help_text="", value='{"mqtt.2030.ltsapis.goog", IOTC_MQTT_PORT}', 59 | ) 60 | ] 61 | 62 | result = render_mbed_config_cmake_template(config, TOOLCHAIN_NAME, "target_name") 63 | assert '"MBED_CONF_IOTC_MQTT_HOST={\\"mqtt.2030.ltsapis.goog\\", IOTC_MQTT_PORT}"' in result 64 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/darwin/diskutil.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Interactions with `diskutil`.""" 6 | import plistlib 7 | import subprocess 8 | from typing import Dict, Iterable, List, Optional, cast 9 | from typing_extensions import TypedDict 10 | 11 | 12 | VolumeTree = Dict # mypy does not work with recursive types, which nested "Partitions" would require 13 | 14 | 15 | class Volume(TypedDict, total=False): 16 | """Representation of mounted volume.""" 17 | 18 | MountPoint: str # example: /Volumes/SomeName 19 | DeviceIdentifier: str # example: disk2 20 | 21 | 22 | def get_all_external_disks_data() -> List[VolumeTree]: 23 | """Returns parsed output of `diskutil` call, fetching only information of interest.""" 24 | output = subprocess.check_output(["diskutil", "list", "-plist", "external"], stderr=subprocess.DEVNULL) 25 | if output: 26 | data: Dict = plistlib.loads(output) 27 | return data.get("AllDisksAndPartitions", []) 28 | return [] 29 | 30 | 31 | def get_all_external_volumes_data() -> List[Volume]: 32 | """Returns all external volumes data. 33 | 34 | Reduces structure returned by `diskutil` call to one which will only contain data about Volumes. 35 | Useful for determining MountPoints and DeviceIdentifiers. 36 | """ 37 | data = get_all_external_disks_data() 38 | return _filter_volumes(data) 39 | 40 | 41 | def get_external_volume_data(device_identifier: str) -> Optional[Volume]: 42 | """Returns external volume data for a given identifier.""" 43 | data = get_all_external_volumes_data() 44 | for device in data: 45 | if device.get("DeviceIdentifier") == device_identifier: 46 | return device 47 | return None 48 | 49 | 50 | def get_mount_point(device_identifier: str) -> Optional[str]: 51 | """Returns mount point of a given device.""" 52 | device_data = get_external_volume_data(device_identifier) 53 | if device_data and "MountPoint" in device_data: 54 | return device_data["MountPoint"] 55 | return None 56 | 57 | 58 | def _filter_volumes(data: Iterable[VolumeTree]) -> List[Volume]: 59 | """Flattens the structure returned by `diskutil` call. 60 | 61 | Expected input will contain both partitioned an unpartitioned devices. 62 | Partitioned devices list mounted partitions under an arbitrary key, 63 | flattening the data helps finding actual end devices later on. 64 | """ 65 | devices = [] 66 | for device in data: 67 | if "Partitions" in device: 68 | devices.extend(_filter_volumes(device["Partitions"])) 69 | else: 70 | devices.append(cast(Volume, device)) 71 | return devices 72 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/darwin/system_profiler.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Interactions with `system_profiler`.""" 6 | import plistlib 7 | import re 8 | import subprocess 9 | from typing import Dict, Iterable, List, cast 10 | from typing_extensions import TypedDict 11 | 12 | USBDeviceTree = Dict # mypy does not work with recursive types, which "_items" would require 13 | 14 | 15 | class USBDeviceMedia(TypedDict, total=False): 16 | """Representation of usb device storage.""" 17 | 18 | bsd_name: str 19 | 20 | 21 | class USBDevice(TypedDict, total=False): 22 | """Representation of usb device.""" 23 | 24 | _name: str 25 | location_id: str 26 | vendor_id: str 27 | product_id: str 28 | serial_num: str 29 | Media: List[USBDeviceMedia] 30 | 31 | 32 | def get_all_usb_devices_data() -> List[USBDeviceTree]: 33 | """Returns parsed output of `system_profiler` call.""" 34 | output = subprocess.check_output(["system_profiler", "-xml", "SPUSBDataType"], stderr=subprocess.DEVNULL) 35 | if output: 36 | return cast(List[USBDeviceTree], plistlib.loads(output)) 37 | return [] 38 | 39 | 40 | def get_end_usb_devices_data() -> List[USBDevice]: 41 | """Returns only end devices from the output of `system_profiler` call.""" 42 | data = get_all_usb_devices_data() 43 | leaf_devices = _extract_leaf_devices(data) 44 | end_devices = _filter_end_devices(leaf_devices) 45 | return end_devices 46 | 47 | 48 | def _extract_leaf_devices(data: Iterable[USBDeviceTree]) -> List[USBDevice]: 49 | """Flattens the structure returned by `system_profiler` call. 50 | 51 | Expected input will contain a tree-like structures, this function will return their leaf nodes. 52 | """ 53 | end_devices = [] 54 | for device in data: 55 | if "_items" in device: 56 | child_devices = _extract_leaf_devices(device["_items"]) 57 | end_devices.extend(child_devices) 58 | else: 59 | end_devices.append(cast(USBDevice, device)) 60 | return end_devices 61 | 62 | 63 | def _filter_end_devices(data: Iterable[USBDevice]) -> List[USBDevice]: 64 | """Removes devices that don't look like end devices. 65 | 66 | An end device is a device that shouldn't have child devices. 67 | I.e.: a hub IS NOT an end device, a mouse IS an end device. 68 | """ 69 | return [device for device in data if not _is_hub(device) and not _is_bus(device)] 70 | 71 | 72 | def _is_hub(data: USBDevice) -> bool: 73 | return bool(re.match(r"USB\d.\d Hub", data.get("_name", ""))) 74 | 75 | 76 | def _is_bus(data: USBDevice) -> bool: 77 | return bool(re.match(r"USB\d\dBus", data.get("_name", ""))) 78 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/linux/device_detector.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines a device detector for Linux.""" 6 | import logging 7 | from pathlib import Path 8 | from typing import Tuple, List, Optional, cast 9 | 10 | import psutil 11 | import pyudev 12 | 13 | from mbed_tools.devices._internal.base_detector import DeviceDetector 14 | from mbed_tools.devices._internal.candidate_device import CandidateDevice, FilesystemMountpointError 15 | 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class LinuxDeviceDetector(DeviceDetector): 21 | """Linux specific implementation of device detection.""" 22 | 23 | def find_candidates(self) -> List[CandidateDevice]: 24 | """Return a list of CandidateDevices.""" 25 | context = pyudev.Context() 26 | candidates = [] 27 | for disk in context.list_devices(subsystem="block", ID_BUS="usb"): 28 | serial_number = disk.properties.get("ID_SERIAL_SHORT") 29 | try: 30 | candidates.append( 31 | CandidateDevice( 32 | mount_points=_find_fs_mounts_for_device(disk.properties.get("DEVNAME")), 33 | product_id=disk.properties.get("ID_MODEL_ID"), 34 | vendor_id=disk.properties.get("ID_VENDOR_ID"), 35 | serial_number=serial_number, 36 | serial_port=_find_serial_port_for_device(serial_number), 37 | ) 38 | ) 39 | except FilesystemMountpointError: 40 | logger.warning( 41 | f"A USB block device was detected at path {disk.properties.get('DEVNAME')}. However, the" 42 | " file system has failed to mount. Please disconnect and reconnect your device and try again." 43 | "If this problem persists, try running fsck.vfat on your block device, as the file system may be " 44 | "corrupted." 45 | ) 46 | continue 47 | return candidates 48 | 49 | 50 | def _find_serial_port_for_device(disk_serial_id: str) -> Optional[str]: 51 | """Try to find a serial port associated with the given device.""" 52 | for tty_dev in pyudev.Context().list_devices(subsystem="tty"): 53 | if tty_dev.properties.get("ID_SERIAL_SHORT") == disk_serial_id: 54 | return cast(str, tty_dev.properties.get("DEVNAME")) 55 | return None 56 | 57 | 58 | def _find_fs_mounts_for_device(device_file_path: str) -> Tuple[Path, ...]: 59 | """Find the file system mount point for a block device file path.""" 60 | return tuple(Path(part.mountpoint) for part in psutil.disk_partitions() if part.device == device_file_path) 61 | -------------------------------------------------------------------------------- /src/mbed_tools/cli/main.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Main cli entry point.""" 6 | import logging 7 | import sys 8 | 9 | from pkg_resources import get_distribution 10 | from typing import Union, Any 11 | 12 | import click 13 | 14 | from mbed_tools.lib.logging import set_log_level, MbedToolsHandler 15 | 16 | from mbed_tools.cli.configure import configure 17 | from mbed_tools.cli.list_connected_devices import list_connected_devices 18 | from mbed_tools.cli.project_management import new, import_, deploy 19 | from mbed_tools.cli.build import build 20 | from mbed_tools.cli.sterm import sterm 21 | 22 | CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) 23 | LOGGER = logging.getLogger(__name__) 24 | 25 | 26 | class GroupWithExceptionHandling(click.Group): 27 | """A click.Group which handles ToolsErrors and logging.""" 28 | 29 | def invoke(self, context: click.Context) -> None: 30 | """Invoke the command group. 31 | 32 | Args: 33 | context: The current click context. 34 | """ 35 | # Use the context manager to ensure tools exceptions (expected behaviour) are shown as messages to the user, 36 | # but all other exceptions (unexpected behaviour) are shown as errors. 37 | with MbedToolsHandler(LOGGER, context.params["traceback"]) as handler: 38 | super().invoke(context) 39 | 40 | sys.exit(handler.exit_code) 41 | 42 | 43 | def print_version(context: click.Context, param: Union[click.Option, click.Parameter], value: bool) -> Any: 44 | """Print the version of mbed-tools.""" 45 | if not value or context.resilient_parsing: 46 | return 47 | 48 | version_string = get_distribution("mbed-tools").version 49 | click.echo(version_string) 50 | context.exit() 51 | 52 | 53 | @click.group(cls=GroupWithExceptionHandling, context_settings=CONTEXT_SETTINGS) 54 | @click.option( 55 | "--version", 56 | is_flag=True, 57 | callback=print_version, 58 | expose_value=False, 59 | is_eager=True, 60 | help="Display versions of all Mbed Tools packages.", 61 | ) 62 | @click.option( 63 | "-v", 64 | "--verbose", 65 | default=0, 66 | count=True, 67 | help="Set the verbosity level, enter multiple times to increase verbosity.", 68 | ) 69 | @click.option("-t", "--traceback", is_flag=True, show_default=True, help="Show a traceback when an error is raised.") 70 | def cli(verbose: int, traceback: bool) -> None: 71 | """Command line tool for interacting with Mbed OS.""" 72 | set_log_level(verbose) 73 | 74 | 75 | cli.add_command(configure, "configure") 76 | cli.add_command(list_connected_devices, "detect") 77 | cli.add_command(new, "new") 78 | cli.add_command(deploy, "deploy") 79 | cli.add_command(import_, "import") 80 | cli.add_command(build, "compile") 81 | cli.add_command(sterm, "sterm") 82 | -------------------------------------------------------------------------------- /src/mbed_tools/build/config.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Parses the Mbed configuration system and generates a CMake config script.""" 6 | import pathlib 7 | 8 | from typing import Any, Tuple 9 | 10 | from mbed_tools.lib.json_helpers import decode_json_file 11 | from mbed_tools.project import MbedProgram 12 | from mbed_tools.targets import get_target_by_name 13 | from mbed_tools.build._internal.cmake_file import render_mbed_config_cmake_template 14 | from mbed_tools.build._internal.config.assemble_build_config import Config, assemble_config 15 | from mbed_tools.build._internal.write_files import write_file 16 | from mbed_tools.build.exceptions import MbedBuildError 17 | 18 | CMAKE_CONFIG_FILE = "mbed_config.cmake" 19 | MBEDIGNORE_FILE = ".mbedignore" 20 | 21 | 22 | def generate_config(target_name: str, toolchain: str, program: MbedProgram) -> Tuple[Config, pathlib.Path]: 23 | """Generate an Mbed config file after parsing the Mbed config system. 24 | 25 | Args: 26 | target_name: Name of the target to configure for. 27 | toolchain: Name of the toolchain to use. 28 | program: The MbedProgram to configure. 29 | 30 | Returns: 31 | Config object (UserDict). 32 | Path to the generated config file. 33 | """ 34 | targets_data = _load_raw_targets_data(program) 35 | target_build_attributes = get_target_by_name(target_name, targets_data) 36 | config = assemble_config( 37 | target_build_attributes, [program.root, program.mbed_os.root], program.files.app_config_file 38 | ) 39 | cmake_file_contents = render_mbed_config_cmake_template( 40 | target_name=target_name, config=config, toolchain_name=toolchain, 41 | ) 42 | cmake_config_file_path = program.files.cmake_build_dir / CMAKE_CONFIG_FILE 43 | write_file(cmake_config_file_path, cmake_file_contents) 44 | mbedignore_path = program.files.cmake_build_dir / MBEDIGNORE_FILE 45 | write_file(mbedignore_path, "*") 46 | return config, cmake_config_file_path 47 | 48 | 49 | def _load_raw_targets_data(program: MbedProgram) -> Any: 50 | targets_data = decode_json_file(program.mbed_os.targets_json_file) 51 | if program.files.custom_targets_json.exists(): 52 | custom_targets_data = decode_json_file(program.files.custom_targets_json) 53 | for custom_target in custom_targets_data: 54 | if custom_target in targets_data: 55 | raise MbedBuildError( 56 | f"Error found in {program.files.custom_targets_json}.\n" 57 | f"A target with the name '{custom_target}' already exists in targets.json. " 58 | "Please give your custom target a unique name so it can be identified." 59 | ) 60 | 61 | targets_data.update(custom_targets_data) 62 | 63 | return targets_data 64 | -------------------------------------------------------------------------------- /tests/project/test_mbed_project.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import pathlib 6 | 7 | import pytest 8 | 9 | from unittest import mock 10 | 11 | from mbed_tools.project import initialise_project, import_project, deploy_project, get_known_libs 12 | 13 | 14 | @pytest.fixture 15 | def mock_libs(): 16 | with mock.patch("mbed_tools.project.project.LibraryReferences") as libs: 17 | yield libs 18 | 19 | 20 | @pytest.fixture 21 | def mock_program(): 22 | with mock.patch("mbed_tools.project.project.MbedProgram") as prog: 23 | yield prog 24 | 25 | 26 | @pytest.fixture 27 | def mock_git(): 28 | with mock.patch("mbed_tools.project.project.git_utils") as gutils: 29 | yield gutils 30 | 31 | 32 | class TestInitialiseProject: 33 | def test_fetches_mbed_os_when_create_only_is_false(self, mock_libs, mock_program): 34 | path = pathlib.Path() 35 | initialise_project(path, create_only=False) 36 | 37 | mock_program.from_new.assert_called_once_with(path) 38 | mock_libs().fetch.assert_called_once() 39 | 40 | def test_skips_mbed_os_when_create_only_is_true(self, mock_libs, mock_program): 41 | path = pathlib.Path() 42 | initialise_project(path, create_only=True) 43 | 44 | mock_program.from_new.assert_called_once_with(path) 45 | mock_libs().fetch.assert_not_called() 46 | 47 | 48 | class TestImportProject: 49 | def test_clones_from_remote(self, mock_git): 50 | url = "https://git.com/gitorg/repo" 51 | import_project(url, recursive=False) 52 | 53 | mock_git.clone.assert_called_once_with(url, pathlib.Path(url.rsplit("/", maxsplit=1)[-1])) 54 | 55 | def test_resolves_libs_when_recursive_is_true(self, mock_git, mock_libs): 56 | url = "https://git.com/gitorg/repo" 57 | import_project(url, recursive=True) 58 | 59 | mock_git.clone.assert_called_once_with(url, pathlib.Path(url.rsplit("/", maxsplit=1)[-1])) 60 | mock_libs().fetch.assert_called_once() 61 | 62 | 63 | class TestDeployProject: 64 | def test_checks_out_libraries(self, mock_libs): 65 | path = pathlib.Path("somewhere") 66 | deploy_project(path, force=False) 67 | 68 | mock_libs().checkout.assert_called_once_with(force=False) 69 | 70 | def test_resolves_libs_if_unresolved_detected(self, mock_libs): 71 | mock_libs().iter_unresolved.return_value = [1] 72 | path = pathlib.Path("somewhere") 73 | deploy_project(path) 74 | 75 | mock_libs().fetch.assert_called_once() 76 | 77 | 78 | class TestPrintLibs: 79 | def test_list_libraries_gets_known_lib_list(self, mock_libs): 80 | path = pathlib.Path("somewhere") 81 | mock_libs().iter_resolved.return_value = ["", ""] 82 | 83 | libs = get_known_libs(path) 84 | 85 | assert libs == ["", ""] 86 | -------------------------------------------------------------------------------- /src/mbed_tools/targets/env.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Environment options for `mbed-targets`. 6 | 7 | All the env configuration options can be set either via environment variables or using a `.env` file 8 | containing the variable definitions as follows: 9 | 10 | ``` 11 | VARIABLE=value 12 | ``` 13 | 14 | Environment variables take precendence, meaning the values set in the file will be overriden 15 | by any values previously set in your environment. 16 | 17 | .. WARNING:: 18 | Do not upload `.env` files containing private tokens to version control! If you use this package 19 | as a dependency of your project, please ensure to include the `.env` in your `.gitignore`. 20 | """ 21 | import os 22 | 23 | import dotenv 24 | 25 | dotenv.load_dotenv(dotenv.find_dotenv(usecwd=True)) 26 | 27 | 28 | class Env: 29 | """Provides access to environment variables. 30 | 31 | Ensures variables are reloaded when environment changes during runtime. 32 | """ 33 | 34 | @property 35 | def MBED_API_AUTH_TOKEN(self) -> str: 36 | """Token to use when accessing online API. 37 | 38 | Mbed Targets uses the online mbed board database at os.mbed.com as its data source. 39 | A snapshot of the board database is shipped with the package, for faster lookup of known 40 | boards. Only public boards are stored in the database snapshot. If you are fetching data 41 | for a private board, mbed-targets will need to contact the online database. 42 | 43 | To fetch data about private boards from the online database, the user must have an account 44 | on os.mbed.com and be member of a vendor team that has permissions to see the private board. 45 | An authentication token for the team member must be provided in an environment variable named 46 | `MBED_API_AUTH_TOKEN`. 47 | """ 48 | return os.getenv("MBED_API_AUTH_TOKEN", "") 49 | 50 | @property 51 | def MBED_DATABASE_MODE(self) -> str: 52 | """Database mode to use when retrieving board data. 53 | 54 | Mbed Targets supports an online and offline mode, which controls where to look up the board database. 55 | 56 | The board lookup can be from either the online or offline database, depending 57 | on the value of an environment variable called `MBED_DATABASE_MODE`. 58 | 59 | The mode can be set to one of the following: 60 | 61 | - `AUTO`: the offline database is searched first, if the board isn't found the online database is searched. 62 | - `ONLINE`: the online database is always used. 63 | - `OFFLINE`: the offline database is always used. 64 | 65 | If `MBED_DATABASE_MODE` is not set, it defaults to `AUTO`. 66 | """ 67 | return os.getenv("MBED_DATABASE_MODE", "AUTO") 68 | 69 | 70 | env = Env() 71 | """Instance of `Env` class.""" 72 | -------------------------------------------------------------------------------- /tests/ci_scripts/test_bump_version.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import pathlib 6 | 7 | from unittest import mock 8 | 9 | import pytest 10 | 11 | from ci_scripts.bump_version import bump_version_from_news_files, news_ext_to_version_type, get_current_version, main 12 | 13 | 14 | @pytest.mark.parametrize( 15 | "files,expected_version", 16 | ( 17 | ((pathlib.Path("blah.feature"), pathlib.Path("blah.misc"), pathlib.Path("blah.bugfix")), "0.1.0"), 18 | ((pathlib.Path("blah.major"), pathlib.Path("blah.feature"), pathlib.Path("blah.bugfix")), "1.0.0"), 19 | ((pathlib.Path("blah.doc"), pathlib.Path("blah.misc"), pathlib.Path("blah.bugfix")), "0.0.1"), 20 | ), 21 | ) 22 | def test_bumps_version_according_to_news_file_extension(files, expected_version): 23 | ver = bump_version_from_news_files("0.0.0", files) 24 | 25 | assert ver == expected_version 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "files", 30 | ( 31 | tuple(), 32 | (pathlib.Path("blah.doc"), pathlib.Path("blah.removal"), pathlib.Path("blah.misc")), 33 | (pathlib.Path("blah.md"),), 34 | ), 35 | ) 36 | def test_raises_when_significant_news_file_not_found(files): 37 | with pytest.raises(ValueError): 38 | bump_version_from_news_files("0.0.0", files) 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "news_exts,expected", 43 | (((".feature", ".major", ".bugfix"), "major"), ((".feature", ".bugfix"), "minor"), ((".misc", ".bugfix"), "patch")), 44 | ) 45 | def test_converts_news_ext_to_version_type(news_exts, expected): 46 | version_type = news_ext_to_version_type(news_exts) 47 | 48 | assert version_type == expected 49 | 50 | 51 | @mock.patch("ci_scripts.bump_version.git.Repo") 52 | def test_get_version_returns_version_from_git(mock_repo): 53 | mock_version = "0.0.0" 54 | mock_repo().__enter__().git.describe.return_value = mock_version 55 | 56 | version_str = get_current_version() 57 | 58 | assert version_str == mock_version 59 | 60 | 61 | @mock.patch("ci_scripts.bump_version.set_log_level") 62 | @mock.patch("ci_scripts.bump_version.bump_version_from_news_files") 63 | @mock.patch("ci_scripts.bump_version.get_current_version") 64 | def test_main_execution_context(get_current_version, bump_version_from_news_files, set_log_level, tmp_path): 65 | class Args: 66 | verbosity = 0 67 | news_file_directory = pathlib.Path(tmp_path, "fake", "news") 68 | 69 | fake_version = "0.0.0" 70 | fake_args = Args() 71 | fake_args.news_file_directory.mkdir(parents=True) 72 | get_current_version.return_value = fake_version 73 | 74 | ret = main(fake_args) 75 | 76 | set_log_level.assert_called_once_with(fake_args.verbosity) 77 | bump_version_from_news_files.assert_called_once_with(fake_version, list(fake_args.news_file_directory.iterdir())) 78 | assert ret == 0 79 | -------------------------------------------------------------------------------- /src/mbed_tools/project/project.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines the public API of the package.""" 6 | import pathlib 7 | import logging 8 | 9 | from typing import List, Any 10 | 11 | from mbed_tools.project.mbed_program import MbedProgram, parse_url 12 | from mbed_tools.project._internal.libraries import LibraryReferences 13 | from mbed_tools.project._internal import git_utils 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | def import_project(url: str, dst_path: Any = None, recursive: bool = False) -> pathlib.Path: 19 | """Clones an Mbed project from a remote repository. 20 | 21 | Args: 22 | url: URL of the repository to clone. 23 | dst_path: Destination path for the repository. 24 | recursive: Recursively clone all project dependencies. 25 | 26 | Returns: 27 | The path the project was cloned to. 28 | """ 29 | git_data = parse_url(url) 30 | url = git_data["url"] 31 | if not dst_path: 32 | dst_path = pathlib.Path(git_data["dst_path"]) 33 | 34 | git_utils.clone(url, dst_path) 35 | if recursive: 36 | libs = LibraryReferences(root=dst_path, ignore_paths=["mbed-os"]) 37 | libs.fetch() 38 | 39 | return dst_path 40 | 41 | 42 | def initialise_project(path: pathlib.Path, create_only: bool) -> None: 43 | """Create a new Mbed project, optionally fetching and adding mbed-os. 44 | 45 | Args: 46 | path: Path to the project folder. Created if it doesn't exist. 47 | create_only: Flag which suppreses fetching mbed-os. If the value is `False`, fetch mbed-os from the remote. 48 | """ 49 | program = MbedProgram.from_new(path) 50 | if not create_only: 51 | libs = LibraryReferences(root=program.root, ignore_paths=["mbed-os"]) 52 | libs.fetch() 53 | 54 | 55 | def deploy_project(path: pathlib.Path, force: bool = False) -> None: 56 | """Deploy a specific revision of the current Mbed project. 57 | 58 | This function also resolves and syncs all library dependencies to the revision specified in the library reference 59 | files. 60 | 61 | Args: 62 | path: Path to the Mbed project. 63 | force: Force overwrite uncommitted changes. If False, the deploy will fail if there are uncommitted local 64 | changes. 65 | """ 66 | libs = LibraryReferences(path, ignore_paths=["mbed-os"]) 67 | libs.checkout(force=force) 68 | if list(libs.iter_unresolved()): 69 | logger.info("Unresolved libraries detected, downloading library source code.") 70 | libs.fetch() 71 | 72 | 73 | def get_known_libs(path: pathlib.Path) -> List: 74 | """List all resolved library dependencies. 75 | 76 | This function will not resolve dependencies. This will only generate a list of resolved dependencies. 77 | 78 | Args: 79 | path: Path to the Mbed project. 80 | 81 | Returns: 82 | A list of known dependencies. 83 | """ 84 | libs = LibraryReferences(path, ignore_paths=["mbed-os"]) 85 | return list(sorted(libs.iter_resolved())) 86 | -------------------------------------------------------------------------------- /src/mbed_tools/targets/boards.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Interface to the Board Database.""" 6 | import json 7 | 8 | from dataclasses import asdict 9 | from collections.abc import Set 10 | from typing import Iterator, Iterable, Any, Callable 11 | 12 | from mbed_tools.targets._internal import board_database 13 | 14 | from mbed_tools.targets.exceptions import UnknownBoard 15 | from mbed_tools.targets.board import Board 16 | 17 | 18 | class Boards(Set): 19 | """Interface to the Board Database. 20 | 21 | Boards is initialised with an Iterable[Board]. The classmethods 22 | can be used to construct Boards with data from either the online or offline database. 23 | """ 24 | 25 | @classmethod 26 | def from_offline_database(cls) -> "Boards": 27 | """Initialise with the offline board database. 28 | 29 | Raises: 30 | BoardDatabaseError: Could not retrieve data from the board database. 31 | """ 32 | return cls(Board.from_offline_board_entry(b) for b in board_database.get_offline_board_data()) 33 | 34 | @classmethod 35 | def from_online_database(cls) -> "Boards": 36 | """Initialise with the online board database. 37 | 38 | Raises: 39 | BoardDatabaseError: Could not retrieve data from the board database. 40 | """ 41 | return cls(Board.from_online_board_entry(b) for b in board_database.get_online_board_data()) 42 | 43 | def __init__(self, boards_data: Iterable["Board"]) -> None: 44 | """Initialise with a list of boards. 45 | 46 | Args: 47 | boards_data: iterable of board data from a board database source. 48 | """ 49 | self._boards_data = tuple(boards_data) 50 | 51 | def __iter__(self) -> Iterator["Board"]: 52 | """Yield an Board on each iteration.""" 53 | for board in self._boards_data: 54 | yield board 55 | 56 | def __len__(self) -> int: 57 | """Return the number of boards.""" 58 | return len(self._boards_data) 59 | 60 | def __contains__(self, board: object) -> Any: 61 | """Check if a board is in the collection of boards. 62 | 63 | Args: 64 | board: An instance of Board. 65 | """ 66 | if not isinstance(board, Board): 67 | return False 68 | 69 | return any(x == board for x in self) 70 | 71 | def get_board(self, matching: Callable) -> Board: 72 | """Returns first Board for which `matching` returns True. 73 | 74 | Args: 75 | matching: A function which will be called for each board in database 76 | 77 | Raises: 78 | UnknownBoard: the given product code was not found in the board database. 79 | """ 80 | try: 81 | return next(board for board in self if matching(board)) 82 | except StopIteration: 83 | raise UnknownBoard() 84 | 85 | def json_dump(self) -> str: 86 | """Return the contents of the board database as a json string.""" 87 | return json.dumps([asdict(b) for b in self], indent=4) 88 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/device_instance_id.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Utility in charge of finding the instance ID of a device.""" 6 | import win32con 7 | import win32api 8 | from mbed_tools.devices._internal.exceptions import SystemException 9 | import logging 10 | 11 | from typing import Optional, Any 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class RegKey(object): 17 | """Context manager in charge of opening and closing registry keys.""" 18 | 19 | def __init__(self, sub_registry_key: str) -> None: 20 | """Initialiser.""" 21 | access = win32con.KEY_READ | win32con.KEY_ENUMERATE_SUB_KEYS | win32con.KEY_QUERY_VALUE 22 | try: 23 | self._hkey = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, sub_registry_key, 0, access) 24 | except win32api.error as e: 25 | raise SystemException(f"Could not read key [{sub_registry_key}] in the registry: {e}") 26 | 27 | def __enter__(self) -> Any: 28 | """Actions on entry.""" 29 | return self._hkey 30 | 31 | def __exit__(self, type: Any, value: Any, traceback: Any) -> None: 32 | """Actions on exit.""" 33 | win32api.RegCloseKey(self._hkey) 34 | self._hkey.close() 35 | 36 | 37 | def get_children_instance_id(pnpid: str) -> Optional[str]: 38 | """Gets the USB children instance ID from the plug and play ID. 39 | 40 | See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/instance-ids. 41 | """ 42 | # Although the registry should not be accessed directly 43 | # (See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/hklm-system-currentcontrolset-enum-registry-tree), # noqa E501 44 | # and SetupDi functions/APIs should be used instead in a similar fashion to Miro utility 45 | # (See https://github.com/cool-RR/Miro/blob/7b9ecd9bc0878e463f5a5e26e8b00b675e3f98ac/tv/windows/plat/usbutils.py) 46 | # Most libraries seems to be reading the registry: 47 | # - Pyserial: https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports_windows.py 48 | # - Node serialport: https://github.com/serialport/node-serialport/blob/cd112ca5a3a3fe186e1ac6fa78eeeb5ea7396185/packages/bindings/src/serialport_win.cpp # noqa E501 49 | # - USB device forensics: https://github.com/woanware/usbdeviceforensics/blob/master/pyTskusbdeviceforensics.py 50 | # For more details about the registry key actually looked at, See: 51 | # - https://stackoverflow.com/questions/3331043/get-list-of-connected-usb-devices 52 | # - https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/usb-device-specific-registry-settings 53 | key_path = f"SYSTEM\\CurrentControlSet\\Enum\\{pnpid}" 54 | with RegKey(key_path) as hkey: 55 | try: 56 | value = win32api.RegQueryValueEx(hkey, "ParentIdPrefix") 57 | return str(value[0]) if value else None 58 | except win32api.error as e: 59 | logger.debug(f"Error occurred reading `ParentIdPrefix` field of key [{key_path}] in the registry: {e}") 60 | return None 61 | -------------------------------------------------------------------------------- /tests/devices/_internal/test_candidate_device.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import pathlib 6 | from unittest import TestCase 7 | from mbed_tools.devices._internal.candidate_device import CandidateDevice 8 | 9 | 10 | def build_candidate_data(**overrides): 11 | defaults = { 12 | "product_id": "0x1234", 13 | "vendor_id": "0x5678", 14 | "mount_points": (pathlib.Path("./foo"),), 15 | "serial_number": "qwer", 16 | "serial_port": "COM1", 17 | } 18 | return {**defaults, **overrides} 19 | 20 | 21 | class TestCandidateDevice(TestCase): 22 | def test_produces_a_valid_candidate(self): 23 | candidate_data = build_candidate_data() 24 | candidate = CandidateDevice(**candidate_data) 25 | 26 | self.assertEqual(candidate.product_id, candidate_data["product_id"]) 27 | self.assertEqual(candidate.vendor_id, candidate_data["vendor_id"]) 28 | self.assertEqual(candidate.mount_points, candidate_data["mount_points"]) 29 | self.assertEqual(candidate.serial_number, candidate_data["serial_number"]) 30 | self.assertEqual(candidate.serial_port, candidate_data["serial_port"]) 31 | 32 | def test_raises_when_product_id_is_empty(self): 33 | candidate_data = build_candidate_data(product_id="") 34 | with self.assertRaisesRegex(ValueError, "product_id"): 35 | CandidateDevice(**candidate_data) 36 | 37 | def test_raises_when_product_id_is_not_hex(self): 38 | candidate_data = build_candidate_data(product_id="TEST") 39 | with self.assertRaisesRegex(ValueError, "product_id"): 40 | CandidateDevice(**candidate_data) 41 | 42 | def test_prefixes_product_id_hex_value(self): 43 | candidate_data = build_candidate_data(product_id="ff01") 44 | candidate = CandidateDevice(**candidate_data) 45 | self.assertEqual(candidate.product_id, "0xff01") 46 | 47 | def test_raises_when_vendor_id_is_empty(self): 48 | candidate_data = build_candidate_data(vendor_id="") 49 | with self.assertRaisesRegex(ValueError, "vendor_id"): 50 | CandidateDevice(**candidate_data) 51 | 52 | def test_raises_when_vendor_id_is_not_hex(self): 53 | candidate_data = build_candidate_data(vendor_id="TEST") 54 | with self.assertRaisesRegex(ValueError, "vendor_id"): 55 | CandidateDevice(**candidate_data) 56 | 57 | def test_prefixes_vendor_id_hex_value(self): 58 | candidate_data = build_candidate_data(vendor_id="cbad") 59 | candidate = CandidateDevice(**candidate_data) 60 | self.assertEqual(candidate.vendor_id, "0xcbad") 61 | 62 | def test_raises_when_mount_points_are_empty(self): 63 | with self.assertRaisesRegex(ValueError, "mount_points"): 64 | CandidateDevice(product_id="1234", vendor_id="1234", mount_points=[], serial_number="1234") 65 | 66 | def test_raises_when_serial_number_is_empty(self): 67 | candidate_data = build_candidate_data(serial_number="") 68 | with self.assertRaisesRegex(ValueError, "serial_number"): 69 | CandidateDevice(**candidate_data) 70 | -------------------------------------------------------------------------------- /src/mbed_tools/build/_internal/templates/mbed_config.tmpl: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 Arm Mbed. All rights reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Automatically generated configuration file. 5 | # DO NOT EDIT. Content may be overwritten. 6 | 7 | include_guard(GLOBAL) 8 | 9 | set(MBED_TOOLCHAIN "{{toolchain_name}}" CACHE STRING "") 10 | set(MBED_TARGET "{{target_name}}" CACHE STRING "") 11 | set(MBED_CPU_CORE "{{core}}" CACHE STRING "") 12 | set(MBED_C_LIB "{{c_lib}}" CACHE STRING "") 13 | set(MBED_PRINTF_LIB "{{printf_lib}}" CACHE STRING "") 14 | set(MBED_OUTPUT_EXT "{{OUTPUT_EXT}}" CACHE STRING "") 15 | set(MBED_GREENTEA_TEST_RESET_TIMEOUT "{{forced_reset_timeout}}" CACHE STRING "") 16 | 17 | list(APPEND MBED_TARGET_SUPPORTED_C_LIBS {% for supported_c_lib in supported_c_libs %} 18 | {{supported_c_lib}} 19 | {%- endfor %} 20 | ) 21 | 22 | list(APPEND MBED_TARGET_SUPPORTED_APPLICATION_PROFILES {% for supported_application_profile in supported_application_profiles %} 23 | {{supported_application_profile}} 24 | {%- endfor %} 25 | ) 26 | 27 | list(APPEND MBED_TARGET_LABELS{% for label in labels %} 28 | {{label}} 29 | {%- endfor %} 30 | {% for extra_label in extra_labels %} 31 | {{extra_label}} 32 | {%- endfor %} 33 | {% for component in components %} 34 | {{component}} 35 | {%- endfor %} 36 | {% for feature in features %} 37 | {{feature}} 38 | {%- endfor %} 39 | ) 40 | 41 | # target 42 | set(MBED_TARGET_DEFINITIONS{% for component in components %} 43 | COMPONENT_{{component}}=1 44 | {%- endfor %} 45 | {% for feature in features %} 46 | FEATURE_{{feature}}=1 47 | {%- endfor %} 48 | {% for device in device_has %} 49 | DEVICE_{{device}}=1 50 | {%- endfor %} 51 | {% for label in labels %} 52 | TARGET_{{label}} 53 | {%- endfor %} 54 | {% for extra_label in extra_labels %} 55 | TARGET_{{extra_label}} 56 | {%- endfor %} 57 | {% for form_factor in supported_form_factors %} 58 | TARGET_FF_{{form_factor}} 59 | {%- endfor %} 60 | {% if mbed_rom_start is defined %} 61 | MBED_ROM_START={{ mbed_rom_start | to_hex }} 62 | {%- endif %} 63 | {% if mbed_rom_size is defined %} 64 | MBED_ROM_SIZE={{ mbed_rom_size | to_hex }} 65 | {%- endif %} 66 | {% if mbed_ram_start is defined %} 67 | MBED_RAM_START={{ mbed_ram_start | to_hex }} 68 | {%- endif %} 69 | {% if mbed_ram_size is defined %} 70 | MBED_RAM_SIZE={{ mbed_ram_size | to_hex }} 71 | {%- endif %} 72 | TARGET_LIKE_MBED 73 | __MBED__=1 74 | ) 75 | 76 | # config 77 | set(MBED_CONFIG_DEFINITIONS 78 | {% for setting in config %} 79 | {%- if setting.macro_name -%} 80 | {%- set setting_name = setting.macro_name -%} 81 | {%- else -%} 82 | {%- set setting_name = "MBED_CONF_{}_{}".format(setting.namespace.upper()|replace('-', '_'), setting.name.upper()|replace('-', '_')) -%} 83 | {%- endif -%} 84 | {%- if setting.value is sameas true or setting.value is sameas false -%} 85 | {% set value = setting.value|int %} 86 | {%- else -%} 87 | {% set value = setting.value|replace("\"", "\\\"") -%} 88 | {%- endif -%} 89 | {%- if setting.value is not none -%} 90 | "{{setting_name}}={{value}}" 91 | {% endif -%} 92 | {%- endfor -%} 93 | {% for macro in macros %} 94 | "{{macro|replace("\"", "\\\"")}}" 95 | {%- endfor %} 96 | ) 97 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/serial_port.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines a Serial Port. 6 | 7 | On Windows, Win32_SerialPort only represents physical serial Ports and hence, USB connections are not listed. 8 | https://superuser.com/questions/835848/how-to-view-serial-com-ports-but-not-through-device-manager 9 | https://stackoverflow.com/Questions/1388871/how-do-i-get-a-list-of-available-serial-ports-in-win32 10 | https://stackoverflow.com/questions/1205383/listing-serial-com-ports-on-windows 11 | https://serverfault.com/questions/398469/what-are-the-minimum-permissions-to-read-the-wmi-class-msserial-portname 12 | """ 13 | 14 | import re 15 | from typing import NamedTuple, cast 16 | 17 | from mbed_tools.devices._internal.windows.component_descriptor import ( 18 | ComponentDescriptor, 19 | UNKNOWN_VALUE, 20 | ) 21 | 22 | CAPTION_PATTERN = re.compile(r"^.* [(](.*)[)]$") 23 | 24 | 25 | def parse_caption(caption: str) -> str: 26 | """Parses the caption string and returns the Port Name.""" 27 | match = CAPTION_PATTERN.fullmatch(caption) 28 | return match.group(1) if match else UNKNOWN_VALUE 29 | 30 | 31 | class PnPEntityMsdnDefinition(NamedTuple): 32 | """Msdn definition of a PnPEntity. 33 | 34 | See https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-pnpentity 35 | """ 36 | 37 | Availability: int 38 | Caption: str 39 | ClassGuid: str 40 | CompatibleID: list 41 | ConfigManagerErrorCode: int 42 | ConfigManagerUserConfig: bool 43 | CreationClassName: str 44 | Description: str 45 | DeviceID: str 46 | ErrorCleared: bool 47 | ErrorDescription: str 48 | HardwareID: list 49 | InstallDate: int 50 | LastErrorCode: int 51 | Manufacturer: str 52 | Name: str 53 | PNPClass: str 54 | PNPDeviceID: str 55 | PowerManagementCapabilities: int 56 | PowerManagementSupported: bool 57 | Present: bool 58 | Service: str 59 | Status: str 60 | StatusInfo: int 61 | SystemCreationClassName: str 62 | SystemName: str 63 | 64 | 65 | class SerialPort(ComponentDescriptor): 66 | """Serial Port as defined in Windows API. 67 | 68 | As can be seen in Windows documentation, 69 | https://docs.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors#ports--com---lpt-ports--, 70 | ports are devices with ClassGuid = {4d36e978-e325-11ce-bfc1-08002be10318}. Hence the filter below. 71 | """ 72 | 73 | def __init__(self) -> None: 74 | """Initialiser.""" 75 | super().__init__( 76 | PnPEntityMsdnDefinition, 77 | win32_class_name="Win32_PnPEntity", 78 | win32_filter='ClassGuid="{4d36e978-e325-11ce-bfc1-08002be10318}"', 79 | ) 80 | 81 | @property 82 | def component_id(self) -> str: 83 | """Returns the device id field.""" 84 | return cast(str, self.get("DeviceID")) 85 | 86 | @property 87 | def port_name(self) -> str: 88 | """Gets the port name.""" 89 | return parse_caption(self.get("Caption")) 90 | 91 | @property 92 | def pnp_id(self) -> str: 93 | """Gets the Plug and play id.""" 94 | return cast(str, self.get("PNPDeviceID")) 95 | -------------------------------------------------------------------------------- /src/mbed_tools/lib/logging.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Helpers for logging errors according to severity of the exception.""" 6 | from typing import Type, Optional, cast 7 | from types import TracebackType 8 | import logging 9 | from mbed_tools.lib.exceptions import ToolsError 10 | 11 | LOGGING_FORMAT = "%(levelname)s: %(message)s" 12 | 13 | VERBOSITY_HELP = { 14 | logging.CRITICAL: "-v", 15 | logging.ERROR: "-v", 16 | logging.WARNING: "-vv", 17 | logging.INFO: "-vvv", 18 | logging.DEBUG: "--traceback", 19 | } 20 | 21 | 22 | def _exception_message(err: BaseException, log_level: int, traceback: bool) -> str: 23 | """Generate a user facing message with help on how to get more information from the logs.""" 24 | error_msg = str(err) 25 | if log_level != logging.DEBUG or not traceback: 26 | cli_option = VERBOSITY_HELP.get(log_level, "-v") 27 | error_msg += f"\n\nMore information may be available by using the command line option '{cli_option}'." 28 | return error_msg 29 | 30 | 31 | class MbedToolsHandler: 32 | """Context Manager to catch Mbed Tools exceptions and generate a helpful user facing message.""" 33 | 34 | def __init__(self, logger: logging.Logger, traceback: bool = False): 35 | """Keep track of the logger to use and whether or not a traceback should be generated.""" 36 | self._logger = logger 37 | self._traceback = traceback 38 | self.exit_code = 0 39 | 40 | def __enter__(self) -> "MbedToolsHandler": 41 | """Return the Context Manager.""" 42 | return self 43 | 44 | def __exit__( 45 | self, 46 | exc_type: Optional[Type[BaseException]], 47 | exc_value: Optional[BaseException], 48 | exc_traceback: Optional[TracebackType], 49 | ) -> bool: 50 | """Handle any raised exceptions, suppressing Tools errors and generating an error message instead.""" 51 | if exc_type and issubclass(exc_type, ToolsError): 52 | error_msg = _exception_message(cast(BaseException, exc_value), logging.root.level, self._traceback) 53 | self._logger.error(error_msg, exc_info=self._traceback) 54 | # Do not propagate exceptions derived from ToolsError 55 | self.exit_code = 1 56 | return True 57 | 58 | # Propagate all other exceptions 59 | return False 60 | 61 | 62 | def log_exception(logger: logging.Logger, exception: Exception, show_traceback: bool = False) -> None: 63 | """Logs an exception in both normal and verbose forms. 64 | 65 | Args: 66 | logger: logger 67 | exception: exception to log 68 | show_traceback: show the full traceback. 69 | """ 70 | logger.error(exception, exc_info=show_traceback) 71 | 72 | 73 | def set_log_level(verbose_count: int) -> None: 74 | """Sets the log level. 75 | 76 | Args: 77 | verbose_count: number of `-v` flags used 78 | """ 79 | if verbose_count > 2: 80 | log_level = logging.DEBUG 81 | elif verbose_count == 2: 82 | log_level = logging.INFO 83 | elif verbose_count == 1: 84 | log_level = logging.WARNING 85 | else: 86 | log_level = logging.ERROR 87 | logging.basicConfig(level=log_level, format=LOGGING_FORMAT) 88 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/system_data_loader.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Loads system data in parallel and all at once in order to improve performance.""" 6 | from concurrent.futures import ThreadPoolExecutor 7 | from typing import List, Tuple, Dict, Generator, Optional, cast 8 | 9 | from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptorWrapper, ComponentDescriptor 10 | from mbed_tools.devices._internal.windows.disk_drive import DiskDrive 11 | from mbed_tools.devices._internal.windows.disk_partition import DiskPartition 12 | from mbed_tools.devices._internal.windows.disk_partition_logical_disk_relationships import ( 13 | DiskPartitionLogicalDiskRelationship, 14 | ) 15 | from mbed_tools.devices._internal.windows.logical_disk import LogicalDisk 16 | from mbed_tools.devices._internal.windows.serial_port import SerialPort 17 | from mbed_tools.devices._internal.windows.usb_controller import UsbController 18 | from mbed_tools.devices._internal.windows.usb_hub import UsbHub 19 | 20 | # All Windows system data of interest in order to retrieve the information for DeviceCandidate. 21 | SYSTEM_DATA_TYPES = [ 22 | UsbHub, 23 | UsbController, 24 | DiskDrive, 25 | DiskPartition, 26 | LogicalDisk, 27 | DiskPartitionLogicalDiskRelationship, 28 | SerialPort, 29 | ] 30 | 31 | 32 | def load_all(cls: type) -> Tuple[type, List[ComponentDescriptor]]: 33 | """Loads all elements present in the system referring to a specific type.""" 34 | return (cls, [element for element in ComponentDescriptorWrapper(cls).element_generator()]) 35 | 36 | 37 | class SystemDataLoader: 38 | """Object in charge of loading all system data with regards to Usb, Disk or serial port. 39 | 40 | It loads all the data in parallel and all at once in order to improve performance. 41 | """ 42 | 43 | def __init__(self) -> None: 44 | """Initialiser.""" 45 | self._system_data: Optional[Dict[type, List[ComponentDescriptor]]] = None 46 | 47 | def _load(self) -> None: 48 | """Loads all system data in parallel.""" 49 | with ThreadPoolExecutor() as executor: 50 | results = executor.map(load_all, SYSTEM_DATA_TYPES) 51 | self._system_data = {k: v for (k, v) in results} 52 | 53 | @property 54 | def system_data(self) -> Dict[type, List[ComponentDescriptor]]: 55 | """Gets all system data.""" 56 | if not self._system_data: 57 | self._load() 58 | return cast(Dict[type, List[ComponentDescriptor]], self._system_data) 59 | 60 | def get_system_data(self, cls: type) -> List[ComponentDescriptor]: 61 | """Gets the system data for a particular type.""" 62 | return self.system_data.get(cls, list()) 63 | 64 | 65 | class ComponentsLoader: 66 | """Loads system components.""" 67 | 68 | def __init__(self, data_loader: SystemDataLoader, cls: type) -> None: 69 | """initialiser.""" 70 | self._cls = cls 71 | self._data_loader = data_loader 72 | 73 | def element_generator(self) -> Generator["ComponentDescriptor", None, None]: 74 | """Gets a generator over all elements currently registered in the system.""" 75 | for component in self._data_loader.get_system_data(self._cls): 76 | yield component 77 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/volume_set.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines a Volume Set. 6 | 7 | CIM_VolumeSet should be the data model to use but does not seem to actually return the data that we are looking for: 8 | https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-volumeset 9 | Therefore, a specific data model needs to be constructed using other Windows methods. 10 | """ 11 | 12 | from enum import Enum 13 | from typing import NamedTuple, List 14 | from mbed_tools.devices._internal.windows.component_descriptor import UNKNOWN_VALUE 15 | 16 | import logging 17 | import win32.win32api 18 | import win32.win32file 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class DriveType(Enum): 24 | """Drive type as defined in Win32 API. 25 | 26 | See https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea. 27 | """ 28 | 29 | DRIVE_UNKNOWN = 0 # The drive type cannot be determined. 30 | DRIVE_NO_ROOT_DIR = 1 # The root path is invalid; for example, there is no volume mounted at the specified path. 31 | DRIVE_REMOVABLE = ( 32 | 2 33 | # The drive has removable media; for example, a floppy drive, thumb drive, or flash card reader. 34 | ) 35 | DRIVE_FIXED = 3 # The drive has fixed media; for example, a hard disk drive or flash drive. 36 | DRIVE_REMOTE = 4 # The drive is a remote (network) drive. 37 | DRIVE_CDROM = 5 # The drive is a CD-ROM drive. 38 | DRIVE_RAMDISK = 6 # The drive is a RAM disk. 39 | 40 | 41 | class VolumeInformation(NamedTuple): 42 | """Volume information. 43 | 44 | See http://timgolden.me.uk/pywin32-docs/win32api__GetVolumeInformation_meth.html 45 | See also http://timgolden.me.uk/python/win32_how_do_i/find-drive-types.html 46 | """ 47 | 48 | Name: str 49 | SerialNumber: int 50 | MaxComponentLengthOfAFileName: int 51 | SysFlags: int 52 | FileSystem: str 53 | UniqueName: str # As defined by GetVolumeNameForVolumeMountPoint 54 | DriveType: DriveType # As defined by GetDriveType 55 | 56 | 57 | def _get_windows_volume_information(volume: str) -> List[str]: 58 | try: 59 | return list(win32.win32api.GetVolumeInformation(volume)) 60 | except Exception as e: 61 | logger.debug(f"Cannot retrieve information about volume {volume}. Reason: {e}") 62 | return [UNKNOWN_VALUE] * 5 63 | 64 | 65 | def _get_volume_name_for_mount_point(volume: str) -> str: 66 | try: 67 | return str(win32.win32file.GetVolumeNameForVolumeMountPoint(volume)) 68 | except Exception as e: 69 | logger.debug(f"Cannot retrieve the real name of volume {volume}. Reason: {e}") 70 | return UNKNOWN_VALUE 71 | 72 | 73 | def _get_drive_type(volume: str) -> DriveType: 74 | try: 75 | return DriveType(win32.win32file.GetDriveType(volume)) 76 | except Exception as e: 77 | logger.debug(f"Cannot retrieve the type of volume {volume}. Reason: {e}") 78 | return DriveType.DRIVE_UNKNOWN 79 | 80 | 81 | def get_volume_information(volume: str) -> VolumeInformation: 82 | """Gets the volume information.""" 83 | if not volume.endswith("\\"): 84 | volume = f"{volume}\\" 85 | values: list = _get_windows_volume_information(volume) + [ 86 | _get_volume_name_for_mount_point(volume), 87 | _get_drive_type(volume), # type: ignore 88 | ] 89 | return VolumeInformation(*values) 90 | -------------------------------------------------------------------------------- /tests/devices/_internal/windows/test_windows_component.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | from typing import NamedTuple, Any 6 | from unittest import TestCase 7 | 8 | from mbed_tools.devices._internal.windows.component_descriptor_utils import is_undefined_data_object, is_undefined_value 9 | from tests.devices._internal.windows.test_component_descriptor_utils import ( 10 | generate_undefined_values, 11 | generate_valid_values, 12 | ) 13 | from tests.devices.markers import windows_only 14 | 15 | 16 | class ComponentDefinition(NamedTuple): 17 | field1: str 18 | field2: str 19 | field3: str 20 | field4: Any 21 | field5: bool 22 | field6: int 23 | 24 | 25 | def get_test_class(): 26 | from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor 27 | 28 | class AComponentForTest(ComponentDescriptor): 29 | def __init__(self) -> None: 30 | """Initialiser.""" 31 | super().__init__(ComponentDefinition, win32_class_name="Win32_ComputerSystem") 32 | 33 | @property 34 | def component_id(self) -> str: 35 | """Returns the device id field.""" 36 | return self.get("field1") 37 | 38 | return AComponentForTest 39 | 40 | 41 | @windows_only 42 | class TestComponentDescriptor(TestCase): 43 | def test_init(self): 44 | self.assertIsNotNone(get_test_class()()) 45 | self.assertTrue(get_test_class()().__class__, "AComponentForTest") 46 | 47 | def test_parameters(self): 48 | self.assertListEqual([name for name in ComponentDefinition._fields], get_test_class()().field_names) 49 | 50 | def test_set_values(self): 51 | 52 | a_component = get_test_class()() 53 | self.assertTrue(is_undefined_data_object(a_component.to_tuple())) 54 | valid_values = {k: generate_valid_values() for k in a_component.field_names} 55 | a_component.set_data_values(valid_values) 56 | self.assertFalse(is_undefined_data_object(a_component.to_tuple())) 57 | self.assertTupleEqual(a_component.to_tuple(), tuple(valid_values.values())) 58 | 59 | def test_is_undefined(self): 60 | a_component = get_test_class()() 61 | self.assertTrue(a_component.is_undefined) 62 | self.assertTrue(is_undefined_value(a_component.component_id)) 63 | a_component_with_undefined_values = get_test_class()() 64 | undefined_values = {k: generate_undefined_values() for k in a_component.field_names} 65 | a_component_with_undefined_values.set_data_values(undefined_values) 66 | self.assertTrue(a_component_with_undefined_values.is_undefined) 67 | a_defined_component = get_test_class()() 68 | valid_values = {k: generate_valid_values() for k in a_component.field_names} 69 | a_defined_component.set_data_values(valid_values) 70 | self.assertIsNotNone(a_defined_component.component_id) 71 | self.assertFalse(a_defined_component.is_undefined) 72 | 73 | def test_iterator(self): 74 | from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptorWrapper 75 | 76 | generator = ComponentDescriptorWrapper(get_test_class()).element_generator() 77 | self.assertIsNotNone(generator) 78 | 79 | for element in generator: 80 | # The generator should be defined but none of the element should be defined as fields should not exist 81 | self.assertTrue(element.is_undefined) 82 | -------------------------------------------------------------------------------- /tests/build/test_flash.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | import pathlib 7 | import tempfile 8 | 9 | from unittest import TestCase, mock 10 | 11 | from mbed_tools.build.flash import flash_binary, _build_binary_file_path, _flash_dev 12 | from mbed_tools.build.exceptions import BinaryFileNotFoundError 13 | 14 | from tests.build.factories import DeviceFactory 15 | 16 | 17 | @mock.patch("mbed_tools.build.flash._build_binary_file_path") 18 | @mock.patch("mbed_tools.build.flash._flash_dev") 19 | class TestFlashBinary(TestCase): 20 | def test_check_flashing(self, _flash_dev, _build_binary_file_path): 21 | test_device = DeviceFactory() 22 | 23 | _flash_dev.return_value = True 24 | 25 | with tempfile.TemporaryDirectory() as tmpDir: 26 | base_dir = pathlib.Path(tmpDir) 27 | build_dir = base_dir / "cmake_build" 28 | build_dir.mkdir() 29 | bin_file = base_dir.name + ".bin" 30 | bin_file = build_dir / bin_file 31 | bin_file.touch() 32 | _build_binary_file_path.return_value = bin_file 33 | 34 | flash_binary(test_device.mount_points[0].resolve(), base_dir, build_dir, "TEST", False) 35 | 36 | _build_binary_file_path.assert_called_once_with(base_dir, build_dir, False) 37 | _flash_dev.assert_called_once_with(test_device.mount_points[0].resolve(), bin_file) 38 | 39 | 40 | class TestBuildBinFilePath(TestCase): 41 | def test_build_bin_file_path(self): 42 | with tempfile.TemporaryDirectory() as tmpDir: 43 | base_dir = pathlib.Path(tmpDir) 44 | build_dir = base_dir / "cmake_build" 45 | build_dir.mkdir() 46 | bin_file = base_dir.name + ".bin" 47 | bin_file = build_dir / bin_file 48 | bin_file.touch() 49 | 50 | self.assertEqual(_build_binary_file_path(base_dir, build_dir, False), bin_file) 51 | 52 | def test_build_hex_file_path(self): 53 | with tempfile.TemporaryDirectory() as tmpDir: 54 | base_dir = pathlib.Path(tmpDir) 55 | build_dir = base_dir / "cmake_build" 56 | build_dir.mkdir() 57 | bin_file = base_dir.name + ".hex" 58 | bin_file = build_dir / bin_file 59 | bin_file.touch() 60 | 61 | self.assertEqual(_build_binary_file_path(base_dir, build_dir, True), bin_file) 62 | 63 | def test_missing_binary_file(self): 64 | with tempfile.TemporaryDirectory() as tmpDir: 65 | base_dir = pathlib.Path(tmpDir) 66 | build_dir = base_dir / "cmake_build" 67 | build_dir.mkdir() 68 | 69 | with self.assertRaises(BinaryFileNotFoundError): 70 | _build_binary_file_path(base_dir, build_dir, False) 71 | 72 | 73 | @mock.patch("mbed_tools.build.flash.shutil.copy") 74 | class TestCopyToDevice(TestCase): 75 | def test_copy_to_device(self, copy): 76 | test_device = DeviceFactory() 77 | 78 | with tempfile.TemporaryDirectory() as tmpDir: 79 | base_dir = pathlib.Path(tmpDir) 80 | build_dir = base_dir / "cmake_build" 81 | build_dir.mkdir() 82 | bin_file = base_dir.name + ".bin" 83 | bin_file = build_dir / bin_file 84 | bin_file.touch() 85 | _flash_dev(test_device.mount_points[0].resolve(), bin_file) 86 | 87 | copy.assert_called_once_with(bin_file, test_device.mount_points[0].resolve(), follow_symlinks=False) 88 | -------------------------------------------------------------------------------- /src/mbed_tools/build/_internal/config/config.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Build configuration representation.""" 6 | import logging 7 | 8 | from collections import UserDict 9 | from typing import Any, Iterable, Hashable, List 10 | 11 | from mbed_tools.build._internal.config.source import Override, ConfigSetting 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class Config(UserDict): 17 | """Mapping of config settings. 18 | 19 | This object understands how to populate the different 'config sections' which all have different rules for how the 20 | settings are collected. 21 | Applies overrides, appends macros, and updates config settings. 22 | """ 23 | 24 | def __setitem__(self, key: Hashable, item: Any) -> None: 25 | """Set an item based on its key.""" 26 | if key == CONFIG_SECTION: 27 | self._update_config_section(item) 28 | elif key == OVERRIDES_SECTION: 29 | self._handle_overrides(item) 30 | elif key == MACROS_SECTION: 31 | self.data[MACROS_SECTION] = self.data.get(MACROS_SECTION, set()) | item 32 | elif key == REQUIRES_SECTION: 33 | self.data[REQUIRES_SECTION] = self.data.get(REQUIRES_SECTION, set()) | item 34 | else: 35 | super().__setitem__(key, item) 36 | 37 | def _handle_overrides(self, overrides: Iterable[Override]) -> None: 38 | for override in overrides: 39 | logger.debug("Applying override '%s.%s'='%s'", override.namespace, override.name, repr(override.value)) 40 | if override.name in self.data: 41 | _apply_override(self.data, override) 42 | continue 43 | 44 | setting = next( 45 | filter( 46 | lambda x: x.name == override.name and x.namespace == override.namespace, 47 | self.data.get(CONFIG_SECTION, []), 48 | ), 49 | None, 50 | ) 51 | if setting is None: 52 | logger.warning( 53 | f"You are attempting to override an undefined config parameter " 54 | f"`{override.namespace}.{override.name}`.\n" 55 | "It is an error to override an undefined configuration parameter. " 56 | "Please check your target_overrides are correct.\n" 57 | f"The parameter `{override.namespace}.{override.name}` will not be added to the configuration." 58 | ) 59 | else: 60 | setting.value = override.value 61 | 62 | def _update_config_section(self, config_settings: List[ConfigSetting]) -> None: 63 | for setting in config_settings: 64 | logger.debug("Adding config setting: '%s.%s'", setting.namespace, setting.name) 65 | if setting in self.data.get(CONFIG_SECTION, []): 66 | raise ValueError( 67 | f"Setting {setting.namespace}.{setting.name} already defined. You cannot duplicate config settings!" 68 | ) 69 | 70 | self.data[CONFIG_SECTION] = self.data.get(CONFIG_SECTION, []) + config_settings 71 | 72 | 73 | CONFIG_SECTION = "config" 74 | MACROS_SECTION = "macros" 75 | OVERRIDES_SECTION = "overrides" 76 | REQUIRES_SECTION = "requires" 77 | 78 | 79 | def _apply_override(data: dict, override: Override) -> None: 80 | if override.modifier == "add": 81 | data[override.name] |= override.value 82 | elif override.modifier == "remove": 83 | data[override.name] -= override.value 84 | else: 85 | data[override.name] = override.value 86 | -------------------------------------------------------------------------------- /azure-pipelines/analyse-and-test.yml: -------------------------------------------------------------------------------- 1 | # Azure Pipeline for Build and Release a Python package. 2 | # 3 | # This pipeline performs multiple actions to ensure the quality of the package: 4 | # - Performs static analysis and runs test on multiple Python versions and host platforms. 5 | # - Uploads test coverage to Code Climate 6 | 7 | # Trigger on a PR to master, beta or releases branches. 8 | pr: 9 | - master 10 | - beta 11 | - releases/* 12 | 13 | stages: 14 | - stage: AnalyseTest 15 | displayName: 'Analyse and Test' 16 | jobs: 17 | - job: Test 18 | strategy: 19 | maxParallel: 10 20 | matrix: 21 | Assert_news: 22 | python.version: '3.7' 23 | vmImageName: ubuntu-latest 24 | uploadCoverage: "false" 25 | tox.env: checknews 26 | 27 | Linting_Py_3_7: 28 | python.version: '3.7' 29 | vmImageName: ubuntu-latest 30 | uploadCoverage: "false" 31 | tox.env: linting 32 | 33 | Linting_Py_3_8: 34 | python.version: '3.8' 35 | vmImageName: ubuntu-latest 36 | uploadCoverage: "false" 37 | tox.env: linting 38 | 39 | Linting_Py_3_9: 40 | python.version: '3.9' 41 | vmImageName: ubuntu-latest 42 | uploadCoverage: "false" 43 | tox.env: linting 44 | 45 | Linux_Py_3_7: 46 | python.version: '3.7' 47 | vmImageName: ubuntu-latest 48 | uploadCoverage: "false" 49 | tox.env: py37 50 | 51 | Linux_Py_3_8: 52 | python.version: '3.8' 53 | vmImageName: ubuntu-latest 54 | uploadCoverage: "false" 55 | tox.env: py38 56 | 57 | Linux_Py_3_9: 58 | python.version: '3.9' 59 | vmImageName: ubuntu-latest 60 | uploadCoverage: "true" 61 | tox.env: py39 62 | 63 | Windows_Py_3_7: 64 | python.version: '3.7' 65 | vmImageName: windows-latest 66 | uploadCoverage: "false" 67 | tox.env: py37 68 | 69 | Windows_Py_3_8: 70 | python.version: '3.8' 71 | vmImageName: windows-latest 72 | uploadCoverage: "false" 73 | tox.env: py38 74 | 75 | Windows_Py_3_9: 76 | python.version: '3.9' 77 | vmImageName: windows-latest 78 | uploadCoverage: "true" 79 | tox.env: py39 80 | 81 | macOS_Py_3_7: 82 | python.version: '3.7' 83 | vmImageName: macOS-latest 84 | uploadCoverage: "false" 85 | tox.env: py37 86 | 87 | macOS_Py_3_8: 88 | python.version: '3.8' 89 | vmImageName: macOS-latest 90 | uploadCoverage: "false" 91 | tox.env: py38 92 | 93 | macOS_Py_3_9: 94 | python.version: '3.9' 95 | vmImageName: macOS-latest 96 | uploadCoverage: "true" 97 | tox.env: py39 98 | 99 | pool: 100 | vmImage: $(vmImageName) 101 | 102 | steps: 103 | - task: UsePythonVersion@0 104 | inputs: 105 | versionSpec: '$(python.version)' 106 | displayName: 'Use Python $(python.version)' 107 | 108 | - script: | 109 | python -m pip install --upgrade tox 110 | tox -e $(tox.env) 111 | displayName: run tox -e $(tox.env) 112 | 113 | - template: steps/publish-code-coverage-results.yml 114 | -------------------------------------------------------------------------------- /src/mbed_tools/cli/configure.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Command to generate the application CMake configuration script used by the build/compile system.""" 6 | import pathlib 7 | 8 | import click 9 | 10 | from mbed_tools.project import MbedProgram 11 | from mbed_tools.build import generate_config 12 | 13 | 14 | @click.command( 15 | help="Generate an Mbed OS config CMake file and write it to a .mbedbuild folder in the program directory." 16 | ) 17 | @click.option( 18 | "--custom-targets-json", type=click.Path(), default=None, help="Path to custom_targets.json.", 19 | ) 20 | @click.option( 21 | "-t", 22 | "--toolchain", 23 | type=click.Choice(["ARM", "GCC_ARM"], case_sensitive=False), 24 | required=True, 25 | help="The toolchain you are using to build your app.", 26 | ) 27 | @click.option("-m", "--mbed-target", required=True, help="A build target for an Mbed-enabled device, eg. K64F") 28 | @click.option("-b", "--profile", default="develop", help="The build type (release, develop or debug).") 29 | @click.option("-o", "--output-dir", type=click.Path(), default=None, help="Path to output directory.") 30 | @click.option( 31 | "-p", 32 | "--program-path", 33 | type=click.Path(), 34 | default=".", 35 | help="Path to local Mbed program. By default is the current working directory.", 36 | ) 37 | @click.option( 38 | "--mbed-os-path", type=click.Path(), default=None, help="Path to local Mbed OS directory.", 39 | ) 40 | @click.option( 41 | "--app-config", type=click.Path(), default=None, help="Path to application configuration file.", 42 | ) 43 | def configure( 44 | toolchain: str, 45 | mbed_target: str, 46 | profile: str, 47 | program_path: str, 48 | mbed_os_path: str, 49 | output_dir: str, 50 | custom_targets_json: str, 51 | app_config: str 52 | ) -> None: 53 | """Exports a mbed_config.cmake file to build directory in the program root. 54 | 55 | The parameters set in the CMake file will be dependent on the combination of 56 | toolchain and Mbed target provided and these can then control which parts of 57 | Mbed OS are included in the build. 58 | 59 | This command will create the .mbedbuild directory at the program root if it doesn't 60 | exist. 61 | 62 | Args: 63 | custom_targets_json: the path to custom_targets.json 64 | toolchain: the toolchain you are using (eg. GCC_ARM, ARM) 65 | mbed_target: the target you are building for (eg. K64F) 66 | profile: The Mbed build profile (debug, develop or release). 67 | program_path: the path to the local Mbed program 68 | mbed_os_path: the path to the local Mbed OS directory 69 | output_dir: the path to the output directory 70 | app_config: the path to the application configuration file 71 | """ 72 | cmake_build_subdir = pathlib.Path(mbed_target.upper(), profile.lower(), toolchain.upper()) 73 | if mbed_os_path is None: 74 | program = MbedProgram.from_existing(pathlib.Path(program_path), cmake_build_subdir) 75 | else: 76 | program = MbedProgram.from_existing(pathlib.Path(program_path), cmake_build_subdir, pathlib.Path(mbed_os_path)) 77 | if custom_targets_json is not None: 78 | program.files.custom_targets_json = pathlib.Path(custom_targets_json) 79 | if output_dir is not None: 80 | program.files.cmake_build_dir = pathlib.Path(output_dir) 81 | if app_config is not None: 82 | program.files.app_config_file = pathlib.Path(app_config) 83 | 84 | mbed_target = mbed_target.upper() 85 | _, output_path = generate_config(mbed_target, toolchain, program) 86 | click.echo(f"mbed_config.cmake has been generated and written to '{str(output_path.resolve())}'") 87 | -------------------------------------------------------------------------------- /tests/cli/test_sterm.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | from unittest import mock 6 | 7 | import pytest 8 | 9 | from click.testing import CliRunner 10 | 11 | from mbed_tools.cli.sterm import sterm 12 | from mbed_tools.devices.exceptions import MbedDevicesError 13 | 14 | 15 | @pytest.fixture 16 | def mock_terminal(): 17 | with mock.patch("mbed_tools.cli.sterm.terminal") as term: 18 | yield term 19 | 20 | 21 | @pytest.fixture 22 | def mock_get_devices(): 23 | with mock.patch("mbed_tools.cli.sterm.get_connected_devices") as get_devs: 24 | yield get_devs 25 | 26 | 27 | @pytest.fixture 28 | def mock_find_device(): 29 | with mock.patch("mbed_tools.cli.sterm.find_connected_device") as find_dev: 30 | yield find_dev 31 | 32 | 33 | def test_launches_terminal_on_given_serial_port(mock_terminal): 34 | port = "tty.1111" 35 | CliRunner().invoke(sterm, ["--port", port]) 36 | 37 | mock_terminal.run.assert_called_once_with(port, 9600, echo=True) 38 | 39 | 40 | def test_launches_terminal_with_given_baud_rate(mock_terminal): 41 | port = "tty.1111" 42 | baud = 115200 43 | CliRunner().invoke(sterm, ["--port", port, "--baudrate", baud]) 44 | 45 | mock_terminal.run.assert_called_once_with(port, baud, echo=True) 46 | 47 | 48 | def test_launches_terminal_with_echo_off_when_specified(mock_terminal): 49 | port = "tty.1111" 50 | CliRunner().invoke(sterm, ["--port", port, "--echo", "off"]) 51 | 52 | mock_terminal.run.assert_called_once_with(port, 9600, echo=False) 53 | 54 | 55 | def test_attempts_to_detect_device_if_no_port_given(mock_get_devices, mock_terminal): 56 | CliRunner().invoke(sterm, []) 57 | 58 | mock_get_devices.assert_called_once() 59 | 60 | 61 | def test_attempts_to_find_connected_target_if_target_given(mock_find_device, mock_terminal): 62 | expected_port = "tty.k64f" 63 | mock_find_device.return_value = mock.Mock(serial_port=expected_port, mbed_board=mock.Mock(board_type="K64F")) 64 | 65 | CliRunner().invoke(sterm, ["-m", "K64F"]) 66 | 67 | mock_terminal.run.assert_called_once_with(expected_port, 9600, echo=True) 68 | 69 | 70 | def test_returns_serial_port_for_first_device_detected_if_no_target_given(mock_get_devices, mock_terminal): 71 | expected_port = "tty.k64f" 72 | mock_get_devices.return_value = mock.Mock( 73 | identified_devices=[ 74 | mock.Mock(serial_port=expected_port, mbed_board=mock.Mock(board_type="K64F")), 75 | mock.Mock(serial_port="tty.disco", mbed_board=mock.Mock(board_type="DISCO")), 76 | ] 77 | ) 78 | 79 | CliRunner().invoke(sterm, []) 80 | 81 | mock_terminal.run.assert_called_once_with(expected_port, 9600, echo=True) 82 | 83 | 84 | def test_returns_serial_port_for_device_if_identifier_given(mock_find_device, mock_terminal): 85 | expected_port = "tty.k64f" 86 | mock_find_device.return_value = mock.Mock(serial_port=expected_port, mbed_board=mock.Mock(board_type="K64F")) 87 | 88 | CliRunner().invoke(sterm, ["-m", "K64F[1]"]) 89 | 90 | mock_terminal.run.assert_called_once_with(expected_port, 9600, echo=True) 91 | 92 | 93 | def test_raises_when_fails_to_find_default_device(mock_get_devices, mock_terminal): 94 | mock_get_devices.return_value = mock.Mock(identified_devices=[]) 95 | 96 | with pytest.raises(MbedDevicesError): 97 | CliRunner().invoke(sterm, [], catch_exceptions=False) 98 | 99 | 100 | def test_not_run_if_target_identifier_not_int(mock_get_devices, mock_terminal): 101 | target = "K64F[foo]" 102 | CliRunner().invoke(sterm, ["-m", target], catch_exceptions=False) 103 | mock_get_devices.assert_not_called() 104 | mock_terminal.assert_not_called() 105 | -------------------------------------------------------------------------------- /tests/cli/test_configure.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import pathlib 6 | 7 | from unittest import TestCase, mock 8 | 9 | from click.testing import CliRunner 10 | 11 | from mbed_tools.cli.configure import configure 12 | 13 | 14 | class TestConfigureCommand(TestCase): 15 | @mock.patch("mbed_tools.cli.configure.generate_config") 16 | @mock.patch("mbed_tools.cli.configure.MbedProgram") 17 | def test_generate_config_called_with_correct_arguments(self, program, generate_config): 18 | CliRunner().invoke(configure, ["-m", "k64f", "-t", "gcc_arm"]) 19 | 20 | generate_config.assert_called_once_with("K64F", "GCC_ARM", program.from_existing()) 21 | 22 | @mock.patch("mbed_tools.cli.configure.generate_config") 23 | @mock.patch("mbed_tools.cli.configure.MbedProgram") 24 | def test_generate_config_called_with_mbed_os_path(self, program, generate_config): 25 | CliRunner().invoke(configure, ["-m", "k64f", "-t", "gcc_arm", "--mbed-os-path", "./extern/mbed-os"]) 26 | 27 | generate_config.assert_called_once_with("K64F", "GCC_ARM", program.from_existing()) 28 | 29 | @mock.patch("mbed_tools.cli.configure.generate_config") 30 | @mock.patch("mbed_tools.cli.configure.MbedProgram") 31 | def test_custom_targets_location_used_when_passed(self, program, generate_config): 32 | program = program.from_existing() 33 | custom_targets_json_path = pathlib.Path("custom", "custom_targets.json") 34 | CliRunner().invoke( 35 | configure, ["-t", "gcc_arm", "-m", "k64f", "--custom-targets-json", custom_targets_json_path] 36 | ) 37 | 38 | generate_config.assert_called_once_with("K64F", "GCC_ARM", program) 39 | self.assertEqual(program.files.custom_targets_json, custom_targets_json_path) 40 | 41 | @mock.patch("mbed_tools.cli.configure.generate_config") 42 | @mock.patch("mbed_tools.cli.configure.MbedProgram") 43 | def test_custom_output_directory_used_when_passed(self, program, generate_config): 44 | program = program.from_existing() 45 | output_dir = pathlib.Path("build") 46 | CliRunner().invoke(configure, ["-t", "gcc_arm", "-m", "k64f", "-o", output_dir]) 47 | 48 | generate_config.assert_called_once_with("K64F", "GCC_ARM", program) 49 | self.assertEqual(program.files.cmake_build_dir, output_dir) 50 | 51 | @mock.patch("mbed_tools.cli.configure.generate_config") 52 | @mock.patch("mbed_tools.cli.configure.MbedProgram") 53 | def test_app_config_used_when_passed(self, program, generate_config): 54 | program = program.from_existing() 55 | app_config_path = pathlib.Path("alternative_config.json") 56 | CliRunner().invoke( 57 | configure, ["-t", "gcc_arm", "-m", "k64f", "--app-config", app_config_path] 58 | ) 59 | 60 | generate_config.assert_called_once_with("K64F", "GCC_ARM", program) 61 | self.assertEqual(program.files.app_config_file, app_config_path) 62 | 63 | @mock.patch("mbed_tools.cli.configure.generate_config") 64 | @mock.patch("mbed_tools.cli.configure.MbedProgram") 65 | def test_profile_used_when_passed(self, program, generate_config): 66 | test_program = program.from_existing() 67 | program.reset_mock() # clear call count from previous line 68 | 69 | toolchain = "gcc_arm" 70 | target = "k64f" 71 | profile = "release" 72 | 73 | CliRunner().invoke( 74 | configure, ["-t", toolchain, "-m", target, "--profile", profile] 75 | ) 76 | 77 | program.from_existing.assert_called_once_with( 78 | pathlib.Path("."), 79 | pathlib.Path(target.upper(), profile, toolchain.upper()) 80 | ) 81 | generate_config.assert_called_once_with("K64F", "GCC_ARM", test_program) 82 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/candidate_device.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Defines CandidateDevice model used for device detection.""" 6 | from dataclasses import dataclass 7 | from typing import Optional, Tuple, Any, Union, cast 8 | from pathlib import Path 9 | 10 | 11 | class CandidateDeviceError(ValueError): 12 | """Base exception raised by a CandidateDevice.""" 13 | 14 | 15 | class USBDescriptorError(CandidateDeviceError): 16 | """USB descriptor field was not found.""" 17 | 18 | 19 | class FilesystemMountpointError(CandidateDeviceError): 20 | """Filesystem mount point was not found.""" 21 | 22 | 23 | class DataField: 24 | """CandidateDevice data attribute descriptor.""" 25 | 26 | def __set_name__(self, owner: object, name: str) -> None: 27 | """Sets the descriptor name, this is called by magic in the owners.__new__ method.""" 28 | self.name = name 29 | 30 | def __get__(self, instance: object, owner: object = None) -> Any: 31 | """Get the attribute value from the instance.""" 32 | return instance.__dict__.setdefault(self.name, None) 33 | 34 | 35 | class USBDescriptorHex(DataField): 36 | """USB descriptor field which cannot be set to an empty value, or an invalid hex value.""" 37 | 38 | def __set__(self, instance: object, value: Any) -> None: 39 | """Prevent setting the descriptor to an empty or invalid hex value.""" 40 | try: 41 | instance.__dict__[self.name] = _format_hex(value) 42 | except ValueError: 43 | raise USBDescriptorError(f"{self.name} cannot be an empty and must be valid hex.") 44 | 45 | 46 | class USBDescriptorString(DataField): 47 | """USB descriptor field which cannot be set to an empty value.""" 48 | 49 | def __set__(self, instance: object, value: str) -> None: 50 | """Prevent setting the descriptor to a non-string or empty value.""" 51 | if not value or not isinstance(value, str): 52 | raise USBDescriptorError(f"{self.name} cannot be an empty field and must be a string.") 53 | 54 | instance.__dict__[self.name] = value 55 | 56 | 57 | class FilesystemMountpoints(DataField): 58 | """Data descriptor which must be set to a non-empty list or tuple.""" 59 | 60 | def __set__(self, instance: object, value: Union[tuple, list]) -> None: 61 | """Prevent setting the descriptor to a non-sequence or empty sequence value.""" 62 | if not value or not isinstance(value, (list, tuple)): 63 | raise FilesystemMountpointError(f"{self.name} must be set to a non-empty list or tuple.") 64 | 65 | instance.__dict__[self.name] = tuple(value) 66 | 67 | 68 | @dataclass(frozen=True, order=True) 69 | class CandidateDevice: 70 | """Valid candidate device connected to the host computer. 71 | 72 | We define a CandidateDevice as any USB mass storage device which mounts a filesystem. 73 | The device may or may not present a serial port. 74 | 75 | Attributes: 76 | product_id: USB device product ID. 77 | vendor_id: USB device vendor ID. 78 | serial_number: USB device serial number. 79 | mount_points: Filesystem mount points associated with the device. 80 | serial_port: Serial port associated with the device, this could be None. 81 | """ 82 | 83 | product_id: str = cast(str, USBDescriptorHex()) 84 | vendor_id: str = cast(str, USBDescriptorHex()) 85 | serial_number: str = cast(str, USBDescriptorString()) 86 | mount_points: Tuple[Path, ...] = cast(Tuple[Path], FilesystemMountpoints()) 87 | serial_port: Optional[str] = None 88 | 89 | 90 | def _format_hex(hex_value: str) -> str: 91 | """Return hex value with a prefix. 92 | 93 | Accepts hex_value in prefixed (0xff) and unprefixed (ff) formats. 94 | """ 95 | return hex(int(hex_value, 16)) 96 | -------------------------------------------------------------------------------- /src/mbed_tools/targets/board.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Representation of an Mbed-Enabled Development Board and related utilities.""" 6 | from dataclasses import dataclass 7 | from typing import Tuple 8 | 9 | 10 | @dataclass(frozen=True, order=True) 11 | class Board: 12 | """Representation of an Mbed-Enabled Development Board. 13 | 14 | Attributes: 15 | board_type: Type of board in format that allows cross-referencing with target definitions. 16 | board_name: Human readable name. 17 | product_code: Uniquely identifies a board for the online compiler. 18 | target_type: A confusing term that is not related to 'target' in other parts of the code. 19 | Identifies if a board in the online database is a `module` or a `platform` (modules are more 20 | complex and often have supplementary sensors, displays etc. A platform is a normal development board). 21 | slug: Used with target_type to identify a board's page on the mbed website. 22 | build_variant: Available build variants for the board. 23 | Can be used in conjunction with board_type for referencing targets. 24 | mbed_os_support: The versions of Mbed OS supported. 25 | mbed_enabled: Whether Mbed OS is supported or not. 26 | """ 27 | 28 | board_type: str 29 | board_name: str 30 | product_code: str 31 | target_type: str 32 | slug: str 33 | build_variant: Tuple[str, ...] 34 | mbed_os_support: Tuple[str, ...] 35 | mbed_enabled: Tuple[str, ...] 36 | 37 | @classmethod 38 | def from_online_board_entry(cls, board_entry: dict) -> "Board": 39 | """Create a new instance of Board from an online database entry. 40 | 41 | Args: 42 | board_entry: A single entity retrieved from the board database API. 43 | """ 44 | board_attrs = board_entry.get("attributes", {}) 45 | board_features = board_attrs.get("features", {}) 46 | 47 | return cls( 48 | # Online database has inconsistently cased board types. 49 | # Since this field is used to match against `targets.json`, we need to ensure consistency is maintained. 50 | board_type=board_attrs.get("board_type", "").upper(), 51 | board_name=board_attrs.get("name", ""), 52 | mbed_os_support=tuple(board_features.get("mbed_os_support", [])), 53 | mbed_enabled=tuple(board_features.get("mbed_enabled", [])), 54 | product_code=board_attrs.get("product_code", ""), 55 | target_type=board_attrs.get("target_type", ""), 56 | slug=board_attrs.get("slug", ""), 57 | # TODO: Remove this hard-coded example after demoing. 58 | # NOTE: Currently we hard-code the build variant for a single board type. 59 | # This is simply so we can demo the tools to PE. This must be removed and replaced with a proper mechanism 60 | # for determining the build variant for all platforms. We probably need to add this information to the 61 | # boards database. 62 | build_variant=tuple(), 63 | ) 64 | 65 | @classmethod 66 | def from_offline_board_entry(cls, board_entry: dict) -> "Board": 67 | """Construct an Board with data from the offline database snapshot.""" 68 | return cls( 69 | board_type=board_entry.get("board_type", ""), 70 | board_name=board_entry.get("board_name", ""), 71 | product_code=board_entry.get("product_code", ""), 72 | target_type=board_entry.get("target_type", ""), 73 | slug=board_entry.get("slug", ""), 74 | mbed_os_support=tuple(board_entry.get("mbed_os_support", [])), 75 | mbed_enabled=tuple(board_entry.get("mbed_enabled", [])), 76 | build_variant=tuple(board_entry.get("build_variant", [])), 77 | ) 78 | -------------------------------------------------------------------------------- /tests/devices/_internal/windows/test_disk_identifier.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | import unittest 6 | from tests.devices.markers import windows_only 7 | 8 | 9 | @windows_only 10 | class TestDiskUid(unittest.TestCase): 11 | def test_disk_uid_parsing(self): 12 | from mbed_tools.devices._internal.windows.disk_drive import Win32DiskIdParser, WindowsUID 13 | 14 | # Tests the parsing of real data. 15 | # ST Link 16 | pnpid1 = "USBSTOR\\DISK&VEN_MBED&PROD_MICROCONTROLLER&REV_1.0\\9&175DDF0B&0&066DFF555654725187095153&0" 17 | serial_number1 = "066DFF555654725187095153" 18 | self.assertEqual( 19 | Win32DiskIdParser().parse(pnpid=pnpid1, serial_number=serial_number1), 20 | WindowsUID( 21 | uid="066dff555654725187095153", 22 | raw_uid="9&175DDF0B&0&066DFF555654725187095153&0", 23 | serial_number="066DFF555654725187095153", 24 | ), 25 | ) 26 | pnpid2 = "USBSTOR\\DISK&VEN_MBED&PROD_MICROCONTROLLER&REV_1.0\\9&3849C7A8&0&0672FF574953867567051035&0" 27 | serial_number2 = "0672FF574953867567051035" 28 | self.assertEqual( 29 | Win32DiskIdParser().parse(pnpid=pnpid2, serial_number=serial_number2), 30 | WindowsUID( 31 | uid="0672ff574953867567051035", 32 | raw_uid="9&3849C7A8&0&0672FF574953867567051035&0", 33 | serial_number="0672FF574953867567051035", 34 | ), 35 | ) 36 | # System disk 37 | pnpid3 = "SCSI\\DISK&VEN_SAMSUNG&PROD_MZNLN512HMJP-000\\4&143821B1&0&000100" 38 | serial_number3 = "S2XANX0J211020" 39 | self.assertEqual( 40 | Win32DiskIdParser().parse(pnpid=pnpid3, serial_number=serial_number3), 41 | WindowsUID(uid="4&143821b1&0&000100", raw_uid="4&143821B1&0&000100", serial_number="S2XANX0J211020"), 42 | ) 43 | # Daplink 44 | pnpid4 = "USBSTOR\\DISK&VEN_MBED&PROD_VFS&REV_0.1\\0240000034544E45001A00018AA900292011000097969900&0" 45 | serial_number4 = "0240000034544E45001A00018AA900292011000097969900" 46 | self.assertEqual( 47 | Win32DiskIdParser().parse(pnpid=pnpid4, serial_number=serial_number4), 48 | WindowsUID( 49 | uid="0240000034544e45001a00018aa900292011000097969900", 50 | raw_uid="0240000034544E45001A00018AA900292011000097969900&0", 51 | serial_number="0240000034544E45001A00018AA900292011000097969900", 52 | ), 53 | ) 54 | # J Link 55 | pnpid5 = "USBSTOR\\DISK&VEN_SEGGER&PROD_MSD_VOLUME&REV_1.00\\9&DBDECF6&0&000440112138&0" 56 | serial_number5 = " 134657890" 57 | self.assertEqual( 58 | Win32DiskIdParser().parse(pnpid=pnpid5, serial_number=serial_number5), 59 | WindowsUID( 60 | uid="000440112138", 61 | raw_uid="9&DBDECF6&0&000440112138&0", 62 | serial_number=" 134657890", 63 | ), 64 | ) 65 | 66 | def test_uid_linking_between_usb_and_disk(self): 67 | from mbed_tools.devices._internal.windows.usb_device_identifier import UsbIdentifier, WindowsUID 68 | 69 | disk_uid = WindowsUID( 70 | uid="000440112138", 71 | raw_uid="9&DBDECF6&0&000440112138&0", 72 | serial_number=" 134657890", 73 | ) 74 | 75 | usb_uid = UsbIdentifier( 76 | UID=WindowsUID(uid="000440112138", raw_uid="000440112138", serial_number="8&2f125ec6&0"), 77 | VID="1366", 78 | PID="1015", 79 | REV=None, 80 | MI=None, 81 | ) 82 | self.assertEqual(disk_uid.presumed_serial_number, usb_uid.uid.presumed_serial_number) 83 | self.assertEqual(usb_uid.uid.presumed_serial_number, disk_uid.presumed_serial_number) 84 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/resolve_board.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Resolve targets for `CandidateDevice`. 6 | 7 | Resolving a target involves looking up an `MbedTarget` from the `mbed-targets` API, using data found in the "htm file" 8 | located on an "Mbed Enabled" device's USB MSD. 9 | 10 | For more information on the mbed-targets package visit https://github.com/ARMmbed/mbed-targets 11 | """ 12 | import logging 13 | 14 | from typing import Optional 15 | 16 | from mbed_tools.targets import ( 17 | Board, 18 | get_board_by_product_code, 19 | get_board_by_online_id, 20 | get_board_by_jlink_slug, 21 | ) 22 | from mbed_tools.targets.exceptions import UnknownBoard, MbedTargetsError 23 | 24 | from mbed_tools.devices._internal.exceptions import NoBoardForCandidate, ResolveBoardError 25 | from mbed_tools.devices._internal.file_parser import OnlineId 26 | 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | def resolve_board( 32 | product_code: Optional[str] = None, online_id: Optional[OnlineId] = None, serial_number: str = "" 33 | ) -> Board: 34 | """Resolves a board object from the platform database. 35 | 36 | We have multiple ways to identify boards from various metadata sources Mbed provides. This is because there are 37 | many supported Mbed device families, each with slightly different ways of identifying themselves as Mbed enabled. 38 | Because of this we need to try each input in turn, falling back to the next lookup method in the priority order if 39 | the previous one was unsuccessful. 40 | 41 | The rules are as follows: 42 | 43 | 1. Use the product code from the mbed.htm file or details.txt if available 44 | 2. Use online ID from the htm file or Board.html if available 45 | 3. Try to use the first 4 chars of the USB serial number as the product code 46 | """ 47 | if product_code: 48 | try: 49 | return get_board_by_product_code(product_code) 50 | except UnknownBoard: 51 | logger.error(f"Could not identify a board with the product code: '{product_code}'.") 52 | except MbedTargetsError as e: 53 | logger.error( 54 | f"There was an error looking up the product code `{product_code}` from the target database.\nError: {e}" 55 | ) 56 | raise ResolveBoardError() from e 57 | 58 | if online_id: 59 | slug = online_id.slug 60 | target_type = online_id.target_type 61 | try: 62 | if target_type == "jlink": 63 | return get_board_by_jlink_slug(slug=slug) 64 | else: 65 | return get_board_by_online_id(slug=slug, target_type=target_type) 66 | except UnknownBoard: 67 | logger.error(f"Could not identify a board with the slug: '{slug}' and target type: '{target_type}'.") 68 | except MbedTargetsError as e: 69 | logger.error( 70 | f"There was an error looking up the online ID `{online_id!r}` from the target database.\nError: {e}" 71 | ) 72 | raise ResolveBoardError() from e 73 | 74 | # Product code might be the first 4 characters of the serial number 75 | product_code = serial_number[:4] 76 | if product_code: 77 | try: 78 | return get_board_by_product_code(product_code) 79 | except UnknownBoard: 80 | # Most devices have a serial number so this may not be a problem 81 | logger.info( 82 | f"The device with the Serial Number: '{serial_number}' (Product Code: '{product_code}') " 83 | f"does not appear to be an Mbed development board." 84 | ) 85 | except MbedTargetsError as e: 86 | logger.error( 87 | f"There was an error looking up the product code `{product_code}` from the target database.\nError: {e}" 88 | ) 89 | raise ResolveBoardError() from e 90 | 91 | raise NoBoardForCandidate 92 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/_internal/windows/usb_hub_data_loader.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Loads System's USB hub.""" 6 | 7 | from typing import Dict, List, cast, Optional, Set, Generator 8 | 9 | from mbed_tools.devices._internal.windows.component_descriptor import ComponentDescriptor 10 | from mbed_tools.devices._internal.windows.device_instance_id import get_children_instance_id 11 | from mbed_tools.devices._internal.windows.system_data_loader import SystemDataLoader, ComponentsLoader 12 | from mbed_tools.devices._internal.windows.usb_controller import UsbController 13 | from mbed_tools.devices._internal.windows.usb_device_identifier import parse_device_id, UsbIdentifier 14 | from mbed_tools.devices._internal.windows.usb_hub import UsbHub 15 | 16 | 17 | class SystemUsbDeviceInformation: 18 | """Usb Hub cache for this system. 19 | 20 | On Windows, each interface e.g. Composite, Mass storage, Port is defined as 21 | a separate independent UsbHub although they are related to the same device. 22 | This cache tries to reduce the list of UsbHubs to only genuinely different devices. 23 | """ 24 | 25 | def __init__(self, data_loader: SystemDataLoader) -> None: 26 | """Initialiser.""" 27 | self._cache: Optional[Dict[UsbIdentifier, List[UsbHub]]] = None 28 | self._ids_cache: Optional[Set[UsbIdentifier]] = None 29 | self._data_loader = data_loader 30 | 31 | def _list_usb_controller_ids(self) -> List[UsbIdentifier]: 32 | return cast( 33 | List[UsbIdentifier], 34 | [ 35 | parse_device_id(cast(UsbController, usbc).component_id) 36 | for usbc in ComponentsLoader(self._data_loader, UsbController).element_generator() 37 | ], 38 | ) 39 | 40 | def _iterate_over_hubs(self) -> Generator[ComponentDescriptor, None, None]: 41 | return ComponentsLoader(self._data_loader, UsbHub).element_generator() 42 | 43 | def _populate_id_cache_with_non_serialnumbers(self) -> None: 44 | if not self._cache or not self._ids_cache: 45 | return 46 | for usb_id in self._cache: 47 | if usb_id not in self._ids_cache: 48 | self._ids_cache.add(usb_id) 49 | 50 | def _determine_potential_serial_number(self, usb_device: UsbHub) -> Optional[str]: 51 | return get_children_instance_id(usb_device.pnp_id) 52 | 53 | def _load(self) -> None: 54 | """Populates the cache.""" 55 | self._cache = cast(Dict[UsbIdentifier, List[UsbHub]], dict()) 56 | self._ids_cache = cast(Set[UsbIdentifier], set()) 57 | controllers = self._list_usb_controller_ids() 58 | for usb_device in self._iterate_over_hubs(): 59 | usb_id = parse_device_id( 60 | usb_device.component_id, serial_number=self._determine_potential_serial_number(cast(UsbHub, usb_device)) 61 | ) 62 | if usb_id in controllers: 63 | continue 64 | entry = self._cache.get(usb_id, list()) 65 | entry.append(cast(UsbHub, usb_device)) 66 | self._cache[usb_id] = entry 67 | 68 | if usb_id.contains_genuine_serial_number(): 69 | self._ids_cache.add(usb_id) 70 | self._populate_id_cache_with_non_serialnumbers() 71 | 72 | @property 73 | def usb_devices(self) -> Dict[UsbIdentifier, List[UsbHub]]: 74 | """Usb devices present in the system.""" 75 | if not self._cache: 76 | self._load() 77 | return cast(Dict[UsbIdentifier, List[UsbHub]], self._cache) 78 | 79 | def get_usb_devices(self, uid: UsbIdentifier) -> List[UsbHub]: 80 | """Gets all USB devices related to an identifier.""" 81 | return self.usb_devices.get(uid, list()) 82 | 83 | def usb_device_ids(self) -> List[UsbIdentifier]: 84 | """Gets system usb device IDs.""" 85 | if not self._ids_cache: 86 | self._load() 87 | return cast(List[UsbIdentifier], self._ids_cache) 88 | -------------------------------------------------------------------------------- /src/mbed_tools/cli/project_management.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """Project management commands: new, import_, deploy and libs.""" 6 | import os 7 | import pathlib 8 | 9 | from typing import Any, List 10 | 11 | import click 12 | import tabulate 13 | 14 | from mbed_tools.project import initialise_project, import_project, get_known_libs, deploy_project 15 | from mbed_tools.project._internal import git_utils 16 | 17 | 18 | @click.command() 19 | @click.option("--create-only", "-c", is_flag=True, show_default=True, help="Create a program without fetching mbed-os.") 20 | @click.argument("path", type=click.Path(resolve_path=True)) 21 | def new(path: str, create_only: bool) -> None: 22 | """Creates a new Mbed project at the specified path. Downloads mbed-os and adds it to the project. 23 | 24 | PATH: Path to the destination directory for the project. Will be created if it does not exist. 25 | """ 26 | click.echo(f"Creating a new Mbed program at path '{path}'.") 27 | if not create_only: 28 | click.echo("Downloading mbed-os and adding it to the project.") 29 | 30 | initialise_project(pathlib.Path(path), create_only) 31 | 32 | 33 | @click.command() 34 | @click.argument("url") 35 | @click.argument("path", type=click.Path(), default="") 36 | @click.option( 37 | "--skip-resolve-libs", 38 | "-s", 39 | is_flag=True, 40 | show_default=True, 41 | help="Skip resolving program library dependencies after cloning.", 42 | ) 43 | def import_(url: str, path: Any, skip_resolve_libs: bool) -> None: 44 | """Clone an Mbed project and library dependencies. 45 | 46 | URL: The git url of the remote project to clone. 47 | 48 | PATH: Destination path for the clone. If not given the destination path is set to the project name in the cwd. 49 | """ 50 | click.echo(f"Cloning Mbed program '{url}'") 51 | if not skip_resolve_libs: 52 | click.echo("Resolving program library dependencies.") 53 | 54 | if path: 55 | click.echo(f"Destination path is '{path}'") 56 | path = pathlib.Path(path) 57 | 58 | dst_path = import_project(url, path, not skip_resolve_libs) 59 | if not skip_resolve_libs: 60 | libs = get_known_libs(dst_path) 61 | _print_dependency_table(libs) 62 | 63 | 64 | @click.command() 65 | @click.argument("path", type=click.Path(), default=os.getcwd()) 66 | @click.option( 67 | "--force", 68 | "-f", 69 | is_flag=True, 70 | show_default=True, 71 | help="Forces checkout of all library repositories at specified commit in the .lib file, overwrites local changes.", 72 | ) 73 | def deploy(path: str, force: bool) -> None: 74 | """Checks out Mbed program library dependencies at the revision specified in the ".lib" files. 75 | 76 | Ensures all dependencies are resolved and the versions are synchronised to the version specified in the library 77 | reference. 78 | 79 | PATH: Path to the Mbed project [default: CWD] 80 | """ 81 | click.echo("Checking out all libraries to revisions specified in .lib files. Resolving any unresolved libraries.") 82 | root_path = pathlib.Path(path) 83 | deploy_project(root_path, force) 84 | libs = get_known_libs(root_path) 85 | _print_dependency_table(libs) 86 | 87 | 88 | def _print_dependency_table(libs: List) -> None: 89 | click.echo("The following library dependencies were fetched: \n") 90 | table = [] 91 | for lib in libs: 92 | table.append( 93 | [ 94 | lib.reference_file.stem, 95 | lib.get_git_reference().repo_url, 96 | lib.source_code_path, 97 | git_utils.get_default_branch(git_utils.get_repo(lib.source_code_path)) 98 | if not lib.get_git_reference().ref 99 | else lib.get_git_reference().ref, 100 | ] 101 | ) 102 | 103 | headers = ("Library Name", "Repository URL", "Path", "Git Reference") 104 | click.echo(tabulate.tabulate(table, headers=headers)) 105 | -------------------------------------------------------------------------------- /src/mbed_tools/devices/devices.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | """API for listing devices.""" 6 | 7 | from operator import attrgetter 8 | from typing import List, Optional 9 | 10 | from mbed_tools.devices._internal.detect_candidate_devices import detect_candidate_devices 11 | 12 | from mbed_tools.devices.device import ConnectedDevices, Device 13 | from mbed_tools.devices.exceptions import DeviceLookupFailed, NoDevicesFound 14 | 15 | 16 | def get_connected_devices() -> ConnectedDevices: 17 | """Returns Mbed Devices connected to host computer. 18 | 19 | Connected devices which have been identified as Mbed Boards and also connected devices which are potentially 20 | Mbed Boards (but not could not be identified in the database) are returned. 21 | """ 22 | connected_devices = ConnectedDevices() 23 | 24 | for candidate_device in detect_candidate_devices(): 25 | device = Device.from_candidate(candidate_device) 26 | connected_devices.add_device(device) 27 | 28 | return connected_devices 29 | 30 | 31 | def find_connected_device(target_name: str, identifier: Optional[int] = None) -> Device: 32 | """Find a connected device matching the given target_name, if there is only one. 33 | 34 | Args: 35 | target_name: The Mbed target name of the device. 36 | identifier: Where multiple of the same Mbed device are connected, the associated [id]. 37 | 38 | Raise: 39 | DeviceLookupFailed: Could not find device matching target_name. 40 | 41 | Returns: 42 | The first Device found matching target_name. 43 | """ 44 | devices = find_all_connected_devices(target_name) 45 | if identifier is None and len(devices) == 1: 46 | return devices[0] 47 | elif identifier is not None and len(devices) > identifier: 48 | return devices[identifier] 49 | 50 | detected_targets = "\n".join( 51 | f"target: {dev.mbed_board.board_type}[{i}]," f" port: {dev.serial_port}, mount point(s): {dev.mount_points}" 52 | for i, dev in enumerate(devices) 53 | ) 54 | if identifier is None: 55 | msg = ( 56 | f"`Multiple matching, please select a connected target with [n] identifier.\n" 57 | f"The following {target_name}s were detected:\n{detected_targets}" 58 | ) 59 | else: 60 | msg = ( 61 | f"`{target_name}[{identifier}]` is not a valid connected target.\n" 62 | f"The following {target_name}s were detected:\n{detected_targets}" 63 | ) 64 | raise DeviceLookupFailed(msg) 65 | 66 | 67 | def find_all_connected_devices(target_name: str) -> List[Device]: 68 | """Find all connected devices matching the given target_name. 69 | 70 | Args: 71 | target_name: The Mbed target name of the device. 72 | 73 | Raises: 74 | NoDevicesFound: Could not find any connected devices. 75 | DeviceLookupFailed: Could not find a connected device matching target_name. 76 | 77 | Returns: 78 | List of Devices matching target_name. 79 | """ 80 | connected = get_connected_devices() 81 | if not connected.identified_devices: 82 | raise NoDevicesFound("No Mbed enabled devices found.") 83 | 84 | matching_devices = sorted( 85 | [device for device in connected.identified_devices if device.mbed_board.board_type == target_name.upper()], 86 | key=attrgetter("serial_number"), 87 | ) 88 | if matching_devices: 89 | return matching_devices 90 | 91 | detected_targets = "\n".join( 92 | f"target: {dev.mbed_board.board_type}, port: {dev.serial_port}, mount point(s): {dev.mount_points}" 93 | for dev in connected.identified_devices 94 | ) 95 | msg = ( 96 | f"Target '{target_name}' was not detected.\n" 97 | "Check the device is connected by USB, and that the name is entered correctly.\n" 98 | f"The following devices were detected:\n{detected_targets}" 99 | ) 100 | raise DeviceLookupFailed(msg) 101 | --------------------------------------------------------------------------------