diff --git a/.gitignore b/.gitignore index 4f1071c..121ace6 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,7 @@ ENV/ # editors .vscode +.idea # macOS-specific .DS_Store diff --git a/CHANGELOG b/CHANGELOG index cbe6378..afcf3d2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -79,3 +79,23 @@ stm32pio changelog: - Changed: util.py functions now raising the exceptions instead of forcing the exit - Changed: test '.ioc' file is updated to the latest STM32CubeMX version (5.1.0 at the moment) - Changed: documentation improvements + + ver. 0.8 (09.19): + - New: setup.py can now install executable script to run 'stm32pio' from any location + - New: stm32pio logo/schematic + - New: add PyCharm to .gitignore + - New: add clear TODOs for the next release (some sort of a roadmap) + - New: single __version__ reference + - New: add some new tests (test_build_should_raise, test_file_not_found) + - Fixed: options '--start-editor' and '--with-build' can now be used both for 'new' and 'generate' commands + - Fixed: import scheme is now as it should be + - Changed: migrate from 'os.path' to 'pathlib' as much as possible for paths management (as a more high-level module) + - Changed: 'start editor' feature is now starting an arbitrary editor (in the same way as you do it from the terminal) + - Changed: take outside 'platformio' command (to 'settings') + - Changed: screenshots were actualized for recent CubeMX versions + - Changed: logging output in standard (non-verbose) mode is simpler + - Changed: move tests in new location + - Changed: revised and improved tests + - Changed: tests are now run by 'python -m unittest' and cannot be run as standalone + - Changed: actualized .ioc file and clean-up the code according to the latest STM32CubeMX version (5.3.0 at the moment) + - Changed: revised and improved util module diff --git a/MANIFEST.in b/MANIFEST.in index 7c22991..d1bb712 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,5 +4,5 @@ include MANIFEST.in include CHANGELOG include TODO.md include .gitignore -include stm32pio-test/stm32pio-test.ioc +recursive-include stm32pio/tests * include screenshots/*.png diff --git a/README.md b/README.md index 5463b73..8c87e9d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ # stm32pio -Small cross-platform Python app that can create and update [PlatformIO](https://platformio.org) projects from [STM32CubeMX](http://www.st.com/en/development-tools/stm32cubemx.html) `.ioc` files. +Small cross-platform Python app that can create and update [PlatformIO](https://platformio.org) projects from [STM32CubeMX](https://www.st.com/en/development-tools/stm32cubemx.html) `.ioc` files. + +![Logo](/screenshots/logo.png) ## Features - - Start the new project in a single directory using only `.ioc` file - - Update existing project after adding/changing hardware options from CubeMX + - Start the new project in a single directory using only an `.ioc` file + - Update existing project after changing hardware options from CubeMX - Clean-up the project (WARNING: it deletes ALL content of 'path' except the `.ioc` file!) - *[optional]* Automatically run your favorite editor in the end - *[optional]* Make an initial build of the project ## Restrictions - - The tool doesn't check for different parameters compatibility, e.g. CPU frequency, memory sizes and so on. It simply ease your workflow with these 2 programs (PlatformIO and STM32CubeMX) a little bit. - - CubeMX middlewares doesn't support yet because it's hard to be prepared for every possible configuration. You need to manually adjust them to build appropriately. For example, FreeRTOS can be added via PlatformIO' `lib` feature or be directly compiled in its own directory using `lib_extra_dirs` option: + - The tool doesn't check for different parameters compatibility, e.g. CPU frequency, memory sizes and so on. It simply eases your workflow with these 2 programs (PlatformIO and STM32CubeMX) a little bit. + - CubeMX middlewares don't support yet because it's hard to be prepared for every possible configuration. You need to manually adjust them to build appropriately. For example, FreeRTOS can be added via PlatformIO' `lib` feature or be directly compiled in its own directory using `lib_extra_dirs` option: ```ini lib_extra_dirs = Middlewares/Third_Party/FreeRTOS ``` @@ -30,7 +32,7 @@ Small cross-platform Python app that can create and update [PlatformIO](https:// ## Usage -Basically, you need to follow such pattern: +Basically, you need to follow such a pattern: 1. Create CubeMX project, set-up hardware configuration 2. Run stm32pio that automatically invoke CubeMX to generate the code, create PlatformIO project, patch an '.ini' file and so on 3. Work on the project in your editor, compile/upload/debug it @@ -38,47 +40,60 @@ Basically, you need to follow such pattern: Refer to Example section on more detailed steps. -stm32pio will create an accessory file 'cubemx-script' in your project directory that contains commands passed to CubeMX. You can safely delete it (it will be created again on the next run) or edit corresponding to your goals. +stm32pio will create an accessory file `cubemx-script`'` in your project directory that contains commands passed to CubeMX. You can safely delete it (it will be created again on the next run) or edit corresponding to your goals. Check `settings.py` to make sure that all user-specific parameters are valid. Run -```bash +```shell script $ python3 stm32pio.py --help ``` to see help. +## Installation +Starting from v0.8 it is possible to install the utility to be able to run stm32pio from anywhere. Use +```shell script +stm32pio-repo/ $ pip3 install . +``` +command to launch the setup process. To uninstall run +```shell script +$ pip3 uninstall stm32pio +``` + + ## Example 1. Run CubeMX, choose MCU/board, do all necessary stuff -2. Open `Project -> Settings` menu, specify Project Name, choose Other Toolchains (GPDSC). In Code Generator tab check "Copy only the necessary library files" and "Generate periphery initialization as a pair of '.c/.h' files per peripheral" options +2. Select `Project Manager -> Project` tab, specify "Project Name", choose "Other Toolchains (GPDSC)". In `Code Generator` tab check "Copy only the necessary library files" and "Generate periphery initialization as a pair of '.c/.h' files per peripheral" options ![Code Generator tab](/screenshots/tab_CodeGenerator.png) -3. Back in the first tab (Project) copy the "Toolchain Folder Location" string. Click OK, close CubeMX +3. Back in the first tab (Project) copy the "Toolchain Folder Location" string (you maybe not be able to copy it in modern CubeMX versions so use terminal or file manager to do this). Save the project, close CubeMX ![Project tab](/screenshots/tab_Project.png) -4. Use copied string as a `-d` argument for stm32pio. So it is assumed that the name of the project folder matches the name of `.ioc` file. (`-d` argument can be omitted if your current working directory is already a project directory) +4. Use a copied string as a `-d` argument for stm32pio. So it is assumed that the name of the project folder matches the name of `.ioc` file. (`-d` argument can be omitted if your current working directory is already a project directory) 5. Run `platformio boards` (`pio boards`) or go to [boards](https://docs.platformio.org/en/latest/boards) to list all supported devices. Pick one and use its ID as a `-b` argument (for example, `nucleo_f031k6`) -6. All done. You can now run -```bash -$ python3 stm32pio.py new -d /path/to/cubemx/project -b nucleo_f031k6 --start-editor=vscode --with-build -``` -to complete generation, start the Visual Studio Code editor with opened folder and compile the project (as example, not required). Make sure you have all tools in PATH (`java` (or set in `settings.py`), editor, Python) - +6. All done! You can now run + ```shell script + $ python3 stm32pio.py new -d path/to/cubemx/project/ -b nucleo_f031k6 --start-editor=vscode --with-build + ``` + to complete generation, start the Visual Studio Code editor with opened folder and compile the project (as an example, not required). Make sure you have all tools in PATH (`java` (or set its path in `settings.py`), `python`, editor). You can use shorter form if you are already located in the project directory (also using shebang alias): + ```shell script + path/to/cubemx/project/ $ stm32pio.py new -b nucleo_f031k6 + ``` 7. If you will be in need to update hardware configuration in the future, make all necessary stuff in CubeMX and run `generate` command in a similar way: -```bash -$ python3 stm32pio.py generate -d /path/to/cubemx/project -``` -8. To clean-up the folder and keep only `.ioc` file run `clean` command + ```shell script + $ python3 stm32pio.py generate -d /path/to/cubemx/project + ``` +8. To clean-up the folder and keep only the `.ioc` file run `clean` command ## Testing -Since ver. 0.45 there are some unit-tests in file `tests.py` (based on the unittest module). Run -```bash -$ python3 tests.py -v +Since ver. 0.45 there are some unit-tests in file `stm32pio/tests/test.py` (based on the unittest module). Run +```shell script +stm32pio-repo/ $ python3 -m unittest discover -v ``` -to test the app. It uses STM32F0 framework to generate and build a code from the `./stm32pio-test/stm32pio-test.ioc` file. - - -## Notes - - CI is hard to implement for all target OSes during the requirement to have all tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to CubeMX for downloading +or +```shell script +stm32pio-repo/ $ python3 -m stm32pio.tests.test -v +``` +to test the app. It uses STM32F0 framework to generate and build a code from the `stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc` file. It's fine to fail an editor test as you not necessarily should have all the editors on your machine. CI is hard to implement for all target OSes during the requirement to have all tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to CubeMX for downloading diff --git a/TODO.md b/TODO.md index b5c6d44..32f3df7 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,7 @@ # TODOs - - [ ] Middleware support (FreeRTOS, etc.) - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... + - [ ] Do we need some sort of GUI? For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output + - [ ] remade as Class (constructor init(project_path)) + - [ ] test CLI (i.e. run stm32pio as subprocess) + - [ ] upload to PyPI diff --git a/screenshots/logo.png b/screenshots/logo.png new file mode 100644 index 0000000..5bd4dde Binary files /dev/null and b/screenshots/logo.png differ diff --git a/screenshots/tab_CodeGenerator.png b/screenshots/tab_CodeGenerator.png index 741122f..a655b26 100644 Binary files a/screenshots/tab_CodeGenerator.png and b/screenshots/tab_CodeGenerator.png differ diff --git a/screenshots/tab_Project.png b/screenshots/tab_Project.png index ba0c368..3abb7e8 100644 Binary files a/screenshots/tab_Project.png and b/screenshots/tab_Project.png differ diff --git a/setup.py b/setup.py index 404160e..40fc197 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,13 @@ import setuptools -with open("README.md", "r") as fh: +from stm32pio.stm32pio import __version__ + +with open("README.md", 'r') as fh: long_description = fh.read() setuptools.setup( name="stm32pio", - version="0.74", + version=__version__, author="ussserrr", author_email="andrei4.2008@gmail.com", description="Small cross-platform Python app that can create and update PlatformIO projects from STM32CubeMX .ioc " @@ -19,5 +21,10 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], - include_package_data=True + include_package_data=True, + entry_points={ + 'console_scripts': [ + 'stm32pio = stm32pio.stm32pio:main' + ] + } ) diff --git a/stm32pio/__init__.py b/stm32pio/__init__.py index b901244..e69de29 100644 --- a/stm32pio/__init__.py +++ b/stm32pio/__init__.py @@ -1,2 +0,0 @@ -name = "stm32pio" -__version__ = "0.74" diff --git a/stm32pio/settings.py b/stm32pio/settings.py index fa39e45..6384baa 100644 --- a/stm32pio/settings.py +++ b/stm32pio/settings.py @@ -1,25 +1,24 @@ -import logging -import pathlib import platform +import pathlib my_os = platform.system() -home_dir = str(pathlib.Path.home()) - -logger = logging.getLogger('') - # (default is OK) How do you start Java from command line? (edit if Java not in PATH) java_cmd = 'java' +# (default is OK) How do you start PlatformIO from command line? (edit if not in PATH, check +# https://docs.platformio.org/en/latest/installation.html#install-shell-commands) +platformio_cmd = 'platformio' + # (default is OK) We trying to guess STM32CubeMX location. You can just avoid this and hard-code it. -# Note that STM32CubeMX will be called as 'java -jar CUBEMX' +# Note that STM32CubeMX will be invoked as 'java -jar CUBEMX' # macOS default: 'Applications' folder if my_os == 'Darwin': cubemx_path = "/Applications/STMicroelectronics/STM32CubeMX.app/Contents/Resources/STM32CubeMX" # Linux (Ubuntu) default: elif my_os == 'Linux': - cubemx_path = "/usr/local/STMicroelectronics/STM32Cube/STM32CubeMX/STM32CubeMX" + cubemx_path = pathlib.Path.home().joinpath("STM32CubeMX/STM32CubeMX") # Windows default: elif my_os == 'Windows': cubemx_path = "C:/Program Files/STMicroelectronics/STM32Cube/STM32CubeMX/STM32CubeMX.exe" @@ -28,11 +27,11 @@ cubemx_script_filename = 'cubemx-script' # (default is OK) see CubeMX user manual PDF to see other useful options -cubemx_script_text = "config load {cubemx_ioc_full_filename}\n" \ - "generate code {project_path}\n" \ - "exit\n" +cubemx_script_content = "config load {cubemx_ioc_full_filename}\n" \ + "generate code {project_path}\n" \ + "exit\n" # (default is OK) -platformio_ini_patch_text = "\n[platformio]\n" \ - "include_dir = Inc\n" \ - "src_dir = Src\n" +platformio_ini_patch_content = "\n[platformio]\n" \ + "include_dir = Inc\n" \ + "src_dir = Src\n" diff --git a/stm32pio/stm32pio.py b/stm32pio/stm32pio.py index 5742e86..e63f706 100755 --- a/stm32pio/stm32pio.py +++ b/stm32pio/stm32pio.py @@ -1,57 +1,54 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- +__version__ = "0.8" -if __name__ == '__main__': - - import argparse - import logging - import sys - import os +import argparse +import logging +import sys +import pathlib - import __init__ +def main(): parser = argparse.ArgumentParser(description="Automation of creating and updating STM32CubeMX-PlatformIO projects. " "Requirements: Python 3.6+, STM32CubeMX, Java, PlatformIO CLI. Edit " - "settings.py to set project path to the STM32CubeMX (if default " - "doesn't work)") - # global arguments (there is also automatically added '-h, --help' option) - parser.add_argument('--version', action='version', version=f"%(prog)s v{__init__.__version__}") - parser.add_argument('-v', '--verbose', help="enable verbose output (default: INFO)", - action='count', required=False) + "settings.py to set path to the STM32CubeMX (if default doesn't work)") + # Global arguments (there is also an automatically added '-h, --help' option) + parser.add_argument('--version', action='version', version=f"%(prog)s v{__version__}") + parser.add_argument('-v', '--verbose', help="enable verbose output (default: INFO)", action='count', required=False) subparsers = parser.add_subparsers(dest='subcommand', title='subcommands', description="valid subcommands", help="modes of operation") parser_new = subparsers.add_parser('new', help="generate CubeMX code, create PlatformIO project [and start the editor]") - parser_new.add_argument('-d', '--directory', dest='project_path', - help="path to the project (current directory, if not given)", default=os.getcwd()) - parser_new.add_argument('-b', '--board', dest='board', help="PlatformIO name of the board", required=True) - parser_new.add_argument('--start-editor', dest='editor', help="use specified editor to open PlatformIO project", - choices=['atom', 'vscode', 'sublime'], required=False) - parser_new.add_argument('--with-build', action='store_true', help="initiate a build after project generation", - required=False) - parser_generate = subparsers.add_parser('generate', help="generate CubeMX code") - parser_generate.add_argument('-d', '--directory', dest='project_path', - help="path to the project (current directory, if not given)", default=os.getcwd()) - parser_clean = subparsers.add_parser('clean', help="clean-up the project (WARNING: it deletes ALL content of " "'path' except the .ioc file)") - parser_clean.add_argument('-d', '--directory', dest='project_path', - help="path to the project (current directory, if not given)", default=os.getcwd()) + + # Common subparsers options + for p in [parser_new, parser_generate, parser_clean]: + p.add_argument('-d', '--directory', dest='project_path', help="path to the project (current directory, if not " + "given)", default=pathlib.Path.cwd()) + for p in [parser_new, parser_generate]: + p.add_argument('--start-editor', dest='editor', help="use specified editor to open PlatformIO project (e.g. " + "subl, code, atom)", required=False) + p.add_argument('--with-build', action='store_true', help="build a project after generation", required=False) + + parser_new.add_argument('-b', '--board', dest='board', help="PlatformIO name of the board", required=True) args = parser.parse_args() # Logger instance goes through the whole program. - # Currently only 2 levels of verbosity through the '-v' option are counted - logging.basicConfig(format="%(levelname)-8s %(funcName)-16s %(message)s") - logger = logging.getLogger('') + # Currently only 2 levels of verbosity through the '-v' option are counted (INFO and DEBUG) + logger = logging.getLogger() if args.verbose: + logging.basicConfig(format="%(levelname)-8s %(funcName)-16s %(message)s") logger.setLevel(logging.DEBUG) logger.debug("debug logging enabled") else: + logging.basicConfig(format="%(levelname)-8s %(message)s") logger.setLevel(logging.INFO) @@ -62,27 +59,29 @@ # Main routine else: - import util + import stm32pio.util try: - if args.subcommand == 'new': - util.generate_code(args.project_path) - util.pio_init(args.project_path, args.board) - util.patch_platformio_ini(args.project_path) + if args.subcommand == 'new' or args.subcommand == 'generate': + stm32pio.util.generate_code(args.project_path) + if args.subcommand == 'new': + stm32pio.util.pio_init(args.project_path, args.board) + stm32pio.util.patch_platformio_ini(args.project_path) - if args.editor: - util.start_editor(args.project_path, args.editor) if args.with_build: - util.pio_build(args.project_path) - - elif args.subcommand == 'generate': - util.generate_code(args.project_path) + stm32pio.util.pio_build(args.project_path) + if args.editor: + stm32pio.util.start_editor(args.project_path, args.editor) elif args.subcommand == 'clean': - util.clean(args.project_path) + stm32pio.util.clean(args.project_path) except Exception as e: print(e.__repr__()) logger.info("exiting...") + + +if __name__ == '__main__': + main() diff --git a/stm32pio/tests.py b/stm32pio/tests.py deleted file mode 100755 index c1dfb1f..0000000 --- a/stm32pio/tests.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 - - -import os -import subprocess -import time -import unittest - -import settings -import util - - - -project_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'stm32pio-test') -board = 'nucleo_f031k6' - - - -def clean_run(test): - def wrapper(self): - util.clean(project_path) - return test(self) - return wrapper - - - -class Test(unittest.TestCase): - - @clean_run - def test_generate_code(self): - """ - Check whether files and folders have been created - """ - - util.generate_code(project_path) - self.assertEqual([os.path.isfile(os.path.join(project_path, settings.cubemx_script_filename)), - os.path.isfile(os.path.join(project_path, 'Src/main.c')), - os.path.isfile(os.path.join(project_path, 'Inc/main.h'))], - [True, True, True], - msg=f"{settings.cubemx_script_filename}, /Inc/main.h, /Src/main.c haven't been created") - - - @clean_run - def test_pio_init(self): - """ - Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization - """ - - util.pio_init(project_path, board) - self.assertTrue(os.path.isfile(os.path.join(project_path, 'platformio.ini')), msg="platformio.ini is not here") - - - @clean_run - def test_patch_platformio_ini(self): - """ - Compare contents of the patched string and the desired patch - """ - - with open(os.path.join(project_path, 'platformio.ini'), mode='w') as platformio_ini: - platformio_ini.write("*** TEST PLATFORMIO.INI FILE ***") - - util.patch_platformio_ini(project_path) - - with open(os.path.join(project_path, 'platformio.ini'), mode='rb') as platformio_ini: - # '2' in seek() means that we count from the end of the file. This feature works only in binary file mode - # In Windows additional '\r' is appended to every '\n' (newline differences) so we need to count them - # for the correct calculation - if settings.my_os == 'Windows': - platformio_ini.seek(-(len(settings.platformio_ini_patch_text) + - settings.platformio_ini_patch_text.count('\n')), 2) - platformio_ini_patched_str = platformio_ini.read(len(settings.platformio_ini_patch_text) + - settings.platformio_ini_patch_text.count('\n')) - platformio_ini_patched_str = platformio_ini_patched_str.replace(b'\r', b'').decode('utf-8') - else: - platformio_ini.seek(-len(settings.platformio_ini_patch_text), 2) - platformio_ini_patched_str = platformio_ini.read( - len(settings.platformio_ini_patch_text)).decode('utf-8') - - self.assertEqual(platformio_ini_patched_str, settings.platformio_ini_patch_text, - msg="'platformio.ini' patching error") - - - @clean_run - def test_build(self): - """ - Initialize a new project and try to build it - """ - - util.generate_code(project_path) - util.pio_init(project_path, board) - util.patch_platformio_ini(project_path) - - result = subprocess.run(['platformio', 'run'], - cwd=project_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Or, for Python 3.7 and above: - # result = subprocess.run(['platformio', 'run'], cwd=project_path, capture_output=True) - - self.assertEqual(result.returncode, 0, msg="build failed") - - - def test_run_editor(self): - """ - Call editors - """ - - util.start_editor(project_path, 'atom') - util.start_editor(project_path, 'vscode') - util.start_editor(project_path, 'sublime') - time.sleep(1) # wait a little bit for apps to start - - if settings.my_os == 'Windows': - result = subprocess.run(['wmic', 'process', 'get', 'description'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') - self.assertIn('atom.exe', result.stdout) - self.assertIn('Code.exe', result.stdout) - self.assertIn('sublime_text.exe', result.stdout) - else: - result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') - # Or, for Python 3.7 and above: - # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') - if settings.my_os == 'Darwin': - self.assertIn('Atom', result.stdout) - self.assertIn('Visual Studio Code', result.stdout) - self.assertIn('Sublime', result.stdout) - if settings.my_os == 'Linux': - self.assertIn('atom', result.stdout) - self.assertIn('code', result.stdout) - self.assertIn('sublime', result.stdout) - - - - @clean_run - def test_regenerate_code(self): - """ - Simulate new project creation, its changing and CubeMX regeneration (for example, after adding new hardware - and some new files) - """ - - # Generate a new project ... - util.generate_code(project_path) - util.pio_init(project_path, board) - util.patch_platformio_ini(project_path) - - # ... change it: - # - add some sample string inside CubeMX' /* BEGIN - END */ block - with open(os.path.join(project_path, 'Src', 'main.c'), mode='r+') as main_c: - main_c_content = main_c.read() - pos = main_c_content.index("while (1)") - main_c_new_content = main_c_content[:pos] + "*** TEST STRING 1 ***\n" + main_c_content[pos:] - main_c.seek(0) - main_c.truncate() - main_c.write(main_c_new_content) - # - add new file inside the project - with open(os.path.join(project_path, 'Inc', 'my_header.h'), mode='w') as my_header_h: - my_header_h.write("*** TEST STRING 2 ***\n") - - # Regenerate CubeMX project - util.generate_code(project_path) - - # Check if added information is preserved - with open(os.path.join(project_path, 'Src', 'main.c'), mode='r') as main_c: - self.assertIn("*** TEST STRING 1 ***", main_c.read()) - with open(os.path.join(project_path, 'Inc', 'my_header.h'), mode='r') as my_header_h: - self.assertIn("*** TEST STRING 2 ***", my_header_h.read()) - - - -if __name__ == '__main__': - unittest.main(exit=False) - util.clean(project_path) diff --git a/stm32pio/tests/__init__.py b/stm32pio/tests/__init__.py new file mode 100644 index 0000000..a4c1230 --- /dev/null +++ b/stm32pio/tests/__init__.py @@ -0,0 +1,12 @@ +""" +Some unit-tests for stm32pio. Uses sample project to generate and build it. It's OK to get errors on `test_run_editor` +one because you don't necessarily should have all of the editors. Run as + + python3 -m unittest discover -v + +or + + python3 -m stm32pio.tests.test -v + +(from repo's root) +""" diff --git a/stm32pio-test/stm32pio-test.ioc b/stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc similarity index 90% rename from stm32pio-test/stm32pio-test.ioc rename to stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc index 8771f4b..73411a1 100644 --- a/stm32pio-test/stm32pio-test.ioc +++ b/stm32pio/tests/stm32pio-test-project/stm32pio-test-project.ioc @@ -1,101 +1,101 @@ -#MicroXplorer Configuration settings - do not modify -File.Version=6 -KeepUserPlacement=true -Mcu.Family=STM32F0 -Mcu.IP0=NVIC -Mcu.IP1=RCC -Mcu.IP2=SYS -Mcu.IP3=USART1 -Mcu.IPNb=4 -Mcu.Name=STM32F031K6Tx -Mcu.Package=LQFP32 -Mcu.Pin0=PF0-OSC_IN -Mcu.Pin1=PA2 -Mcu.Pin2=PA13 -Mcu.Pin3=PA14 -Mcu.Pin4=PA15 -Mcu.Pin5=VP_SYS_VS_Systick -Mcu.PinsNb=6 -Mcu.ThirdPartyNb=0 -Mcu.UserConstants= -Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.1.0 -MxDb.Version=DB.5.0.10 -NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true -PA13.GPIOParameters=GPIO_Label -PA13.GPIO_Label=SWDIO -PA13.Locked=true -PA13.Mode=Serial_Wire -PA13.Signal=SYS_SWDIO -PA14.GPIOParameters=GPIO_Label -PA14.GPIO_Label=SWCLK -PA14.Locked=true -PA14.Mode=Serial_Wire -PA14.Signal=SYS_SWCLK -PA15.GPIOParameters=GPIO_Label -PA15.GPIO_Label=VCP_RX -PA15.Locked=true -PA15.Mode=Asynchronous -PA15.Signal=USART1_RX -PA2.GPIOParameters=GPIO_Label -PA2.GPIO_Label=VCP_TX -PA2.Locked=true -PA2.Mode=Asynchronous -PA2.Signal=USART1_TX -PCC.Checker=false -PCC.Line=STM32F0x1 -PCC.MCU=STM32F031K6Tx -PCC.PartNumber=STM32F031K6Tx -PCC.Seq0=0 -PCC.Series=STM32F0 -PCC.Temperature=25 -PCC.Vdd=3.6 -PF0-OSC_IN.Locked=true -PF0-OSC_IN.Mode=HSE-External-Clock-Source -PF0-OSC_IN.Signal=RCC_OSC_IN -PinOutPanel.RotationAngle=0 -ProjectManager.AskForMigrate=true -ProjectManager.BackupPrevious=false -ProjectManager.CompilerOptimize=6 -ProjectManager.ComputerToolchain=false -ProjectManager.CoupleFile=true -ProjectManager.CustomerFirmwarePackage= -ProjectManager.DefaultFWLocation=true -ProjectManager.DeletePrevious=true -ProjectManager.DeviceId=STM32F031K6Tx -ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.9.0 -ProjectManager.FreePins=false -ProjectManager.HalAssertFull=false -ProjectManager.HeapSize=0x200 -ProjectManager.KeepUserCode=true -ProjectManager.LastFirmware=true -ProjectManager.LibraryCopy=1 -ProjectManager.MainLocation=Src -ProjectManager.NoMain=false -ProjectManager.PreviousToolchain= -ProjectManager.ProjectBuild=false -ProjectManager.ProjectFileName=stm32pio-test.ioc -ProjectManager.ProjectName=stm32pio-test -ProjectManager.StackSize=0x400 -ProjectManager.TargetToolchain=Other Toolchains (GPDSC) -ProjectManager.ToolChainLocation= -ProjectManager.UnderRoot=false -ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true -RCC.CECFreq_Value=32786.88524590164 -RCC.FamilyName=M -RCC.HSICECFreq_Value=32786.88524590164 -RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value -RCC.PLLCLKFreq_Value=8000000 -RCC.PLLMCOFreq_Value=8000000 -RCC.TimSysFreq_Value=8000000 -RCC.VCOOutput2Freq_Value=4000000 -USART1.IPParameters=VirtualMode-Asynchronous -USART1.VirtualMode-Asynchronous=VM_ASYNC -VP_SYS_VS_Systick.Mode=SysTick -VP_SYS_VS_Systick.Signal=SYS_VS_Systick -board=NUCLEO-F031K6 -boardIOC=true +#MicroXplorer Configuration settings - do not modify +File.Version=6 +KeepUserPlacement=true +Mcu.Family=STM32F0 +Mcu.IP0=NVIC +Mcu.IP1=RCC +Mcu.IP2=SYS +Mcu.IP3=USART1 +Mcu.IPNb=4 +Mcu.Name=STM32F031K6Tx +Mcu.Package=LQFP32 +Mcu.Pin0=PF0-OSC_IN +Mcu.Pin1=PA2 +Mcu.Pin2=PA13 +Mcu.Pin3=PA14 +Mcu.Pin4=PA15 +Mcu.Pin5=VP_SYS_VS_Systick +Mcu.PinsNb=6 +Mcu.ThirdPartyNb=0 +Mcu.UserConstants= +Mcu.UserName=STM32F031K6Tx +MxCube.Version=5.3.0 +MxDb.Version=DB.5.0.30 +NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true +PA13.GPIOParameters=GPIO_Label +PA13.GPIO_Label=SWDIO +PA13.Locked=true +PA13.Mode=Serial_Wire +PA13.Signal=SYS_SWDIO +PA14.GPIOParameters=GPIO_Label +PA14.GPIO_Label=SWCLK +PA14.Locked=true +PA14.Mode=Serial_Wire +PA14.Signal=SYS_SWCLK +PA15.GPIOParameters=GPIO_Label +PA15.GPIO_Label=VCP_RX +PA15.Locked=true +PA15.Mode=Asynchronous +PA15.Signal=USART1_RX +PA2.GPIOParameters=GPIO_Label +PA2.GPIO_Label=VCP_TX +PA2.Locked=true +PA2.Mode=Asynchronous +PA2.Signal=USART1_TX +PCC.Checker=false +PCC.Line=STM32F0x1 +PCC.MCU=STM32F031K6Tx +PCC.PartNumber=STM32F031K6Tx +PCC.Seq0=0 +PCC.Series=STM32F0 +PCC.Temperature=25 +PCC.Vdd=3.6 +PF0-OSC_IN.Locked=true +PF0-OSC_IN.Mode=HSE-External-Clock-Source +PF0-OSC_IN.Signal=RCC_OSC_IN +PinOutPanel.RotationAngle=0 +ProjectManager.AskForMigrate=true +ProjectManager.BackupPrevious=false +ProjectManager.CompilerOptimize=6 +ProjectManager.ComputerToolchain=false +ProjectManager.CoupleFile=true +ProjectManager.CustomerFirmwarePackage= +ProjectManager.DefaultFWLocation=true +ProjectManager.DeletePrevious=true +ProjectManager.DeviceId=STM32F031K6Tx +ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.10.1 +ProjectManager.FreePins=false +ProjectManager.HalAssertFull=false +ProjectManager.HeapSize=0x200 +ProjectManager.KeepUserCode=true +ProjectManager.LastFirmware=true +ProjectManager.LibraryCopy=1 +ProjectManager.MainLocation=Src +ProjectManager.NoMain=false +ProjectManager.PreviousToolchain= +ProjectManager.ProjectBuild=false +ProjectManager.ProjectFileName=stm32pio-test-project.ioc +ProjectManager.ProjectName=stm32pio-test-project +ProjectManager.StackSize=0x400 +ProjectManager.TargetToolchain=Other Toolchains (GPDSC) +ProjectManager.ToolChainLocation= +ProjectManager.UnderRoot=true +ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true +RCC.CECFreq_Value=32786.88524590164 +RCC.FamilyName=M +RCC.HSICECFreq_Value=32786.88524590164 +RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value +RCC.PLLCLKFreq_Value=8000000 +RCC.PLLMCOFreq_Value=8000000 +RCC.TimSysFreq_Value=8000000 +RCC.VCOOutput2Freq_Value=4000000 +USART1.IPParameters=VirtualMode-Asynchronous +USART1.VirtualMode-Asynchronous=VM_ASYNC +VP_SYS_VS_Systick.Mode=SysTick +VP_SYS_VS_Systick.Signal=SYS_VS_Systick +board=NUCLEO-F031K6 +boardIOC=true diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py new file mode 100755 index 0000000..65ea76f --- /dev/null +++ b/stm32pio/tests/test.py @@ -0,0 +1,181 @@ +import pathlib +import subprocess +import time +import unittest + +import stm32pio.settings +import stm32pio.util + + +project_path = pathlib.Path('stm32pio/tests/stm32pio-test-project').resolve() +board = 'nucleo_f031k6' + + + +def clean_run(test): + """ + The decorator that clean the project directory before the test (or any other function). Its functionality can also + be done using setUp method of unittest. Also, we assume that stm32pio.util.clean() does not contain any errors + itself :) + """ + def wrapper(self): + stm32pio.util.clean(project_path) + return test(self) + return wrapper + + + +class Test(unittest.TestCase): + + @clean_run + def test_generate_code(self): + """ + Check whether files and folders have been created + """ + stm32pio.util.generate_code(project_path) + # Assuming that the presence of these files indicates a success + files_should_be_present = [stm32pio.settings.cubemx_script_filename, 'Src/main.c', 'Inc/main.h'] + self.assertEqual([project_path.joinpath(file).is_file() for file in files_should_be_present], + [True] * len(files_should_be_present), + msg=f"At least one of {files_should_be_present} files haven't been created") + + + @clean_run + def test_pio_init(self): + """ + Consider that existence of 'platformio.ini' file is displaying successful PlatformIO project initialization + """ + stm32pio.util.pio_init(project_path, board) + self.assertTrue(project_path.joinpath('platformio.ini').is_file(), msg="platformio.ini is not there") + + + @clean_run + def test_patch_platformio_ini(self): + """ + Compare contents of the patched string and the desired patch + """ + test_content = "*** TEST PLATFORMIO.INI FILE ***" + project_path.joinpath('platformio.ini').write_text(test_content) + + stm32pio.util.patch_platformio_ini(project_path) + + after_patch_content = project_path.joinpath('platformio.ini').read_text() + + # Initial content wasn't corrupted + self.assertEqual(after_patch_content[:len(test_content)], test_content, + msg="Initial content of platformio.ini is corrupted") + # Patch content is as expected + self.assertEqual(after_patch_content[len(test_content):], stm32pio.settings.platformio_ini_patch_content, + msg="patch content is not as expected") + + + @clean_run + def test_build_should_raise(self): + """ + Build an empty project so PlatformIO should return non-zero code and we should throw the exception + """ + stm32pio.util.pio_init(project_path, board) + with self.assertRaisesRegex(Exception, "PlatformIO build error", + msg="Build error exception hadn't been raised"): + stm32pio.util.pio_build(project_path) + + + @clean_run + def test_build(self): + """ + Initialize a new project and try to build it + """ + stm32pio.util.generate_code(project_path) + stm32pio.util.pio_init(project_path, board) + stm32pio.util.patch_platformio_ini(project_path) + + result = stm32pio.util.pio_build(project_path) + + self.assertEqual(result, 0, msg="Build failed") + + + def test_run_editor(self): + """ + Call the editors + """ + stm32pio.util.start_editor(project_path, 'atom') + stm32pio.util.start_editor(project_path, 'code') + stm32pio.util.start_editor(project_path, 'subl') + time.sleep(1) # wait a little bit for apps to start + + if stm32pio.settings.my_os == 'Windows': + result = subprocess.run(['wmic', 'process', 'get', 'description'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + self.assertIn('atom.exe', result.stdout) + self.assertIn('Code.exe', result.stdout) + self.assertIn('sublime_text.exe', result.stdout) + else: + result = subprocess.run(['ps', '-A'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + # Or, for Python 3.7 and above: + # result = subprocess.run(['ps', '-A'], capture_output=True, encoding='utf-8') + if stm32pio.settings.my_os == 'Darwin': + self.assertIn('Atom', result.stdout) + self.assertIn('Visual Studio Code', result.stdout) + self.assertIn('Sublime', result.stdout) + if stm32pio.settings.my_os == 'Linux': + self.assertIn('atom', result.stdout) + self.assertIn('code', result.stdout) + self.assertIn('sublime', result.stdout) + + + @clean_run + def test_regenerate_code(self): + """ + Simulate new project creation, its changing and CubeMX code re-generation (for example, after adding new + hardware features and some new files) + """ + + # Generate a new project ... + stm32pio.util.generate_code(project_path) + stm32pio.util.pio_init(project_path, board) + stm32pio.util.patch_platformio_ini(project_path) + + # ... change it: + test_file_1 = project_path.joinpath('Src', 'main.c') + test_content_1 = "*** TEST STRING 1 ***\n" + test_file_2 = project_path.joinpath('Inc', 'my_header.h') + test_content_2 = "*** TEST STRING 2 ***\n" + # - add some sample string inside CubeMX' /* BEGIN - END */ block + main_c_content = test_file_1.read_text() + pos = main_c_content.index("while (1)") + main_c_new_content = main_c_content[:pos] + test_content_1 + main_c_content[pos:] + test_file_1.write_text(main_c_new_content) + # - add new file inside the project + test_file_2.write_text(test_content_2) + + # Re-generate CubeMX project + stm32pio.util.generate_code(project_path) + + # Check if added information is preserved + main_c_after_regenerate_content = test_file_1.read_text() + my_header_h_after_regenerate_content = test_file_2.read_text() + self.assertIn(test_content_1, main_c_after_regenerate_content, + msg=f"{test_file_1} does not preserve user content after regeneration") + self.assertIn(test_content_2, my_header_h_after_regenerate_content, + msg=f"{test_file_2} does not preserve user content after regeneration") + + + def test_file_not_found(self): + """ + Pass non-existing path and expect the error + """ + not_existing_path = project_path.joinpath('does_not_exist') + with self.assertRaises(FileNotFoundError, msg="FileNotFoundError was not raised"): + stm32pio.util._get_project_path(not_existing_path) + + +def tearDownModule(): + """ + Clean up after yourself + """ + stm32pio.util.clean(project_path) + + + +if __name__ == '__main__': + unittest.main() diff --git a/stm32pio/util.py b/stm32pio/util.py index f6f873a..b89dc21 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -1,18 +1,23 @@ import logging -import os +import pathlib import shutil import subprocess -import settings +import stm32pio.settings -logger = logging.getLogger('') +logger = logging.getLogger() def _get_project_path(dirty_path): - # Handle '/path/to/proj' and '/path/to/proj/', 'dot' (current directory) cases - correct_path = os.path.abspath(os.path.normpath(dirty_path)) - if not os.path.exists(correct_path): + """ + Handle '/path/to/proj' and '/path/to/proj/', . (current directory) and other cases + + Args: + dirty_path: some filesystem path + """ + correct_path = pathlib.Path(dirty_path).resolve() + if not correct_path.exists(): logger.error("incorrect project path") raise FileNotFoundError(correct_path) else: @@ -20,7 +25,6 @@ def _get_project_path(dirty_path): -# TODO: simplify the code by dividing big routines on several smaller ones def generate_code(dirty_path): """ Call STM32CubeMX app as a 'java -jar' file with the automatically prearranged 'cubemx-script' file @@ -31,57 +35,43 @@ def generate_code(dirty_path): project_path = _get_project_path(dirty_path) - # Assuming the name of the '.ioc' file is the same as the project folder, we extract it from the given string - project_name = os.path.basename(project_path) + project_name = project_path.name logger.debug(f"searching for {project_name}.ioc file...") - cubemx_ioc_full_filename = os.path.join(project_path, f'{project_name}.ioc') - if os.path.exists(cubemx_ioc_full_filename): + cubemx_ioc_full_filename = project_path.joinpath(f'{project_name}.ioc') + if cubemx_ioc_full_filename.exists(): logger.debug(f"{project_name}.ioc file was found") else: logger.error(f"there is no {project_name}.ioc file") raise FileNotFoundError(cubemx_ioc_full_filename) - - # There should be correct 'cubemx-script' file, otherwise STM32CubeMX will fail - logger.debug(f"searching for '{settings.cubemx_script_filename}' file...") - cubemx_script_full_filename = os.path.join(project_path, settings.cubemx_script_filename) - if not os.path.isfile(cubemx_script_full_filename): - logger.debug(settings.cubemx_script_filename + " file wasn't found, creating one...") - cubemx_script_text = settings.cubemx_script_text.format(project_path=project_path, - cubemx_ioc_full_filename=cubemx_ioc_full_filename) - # TODO: wrap into try-except to catch writing errors - with open(cubemx_script_full_filename, 'w') as cubemx_script_file: - cubemx_script_file.write(cubemx_script_text) - logger.debug(f"'{settings.cubemx_script_filename}' file has been successfully created") + # Find/create 'cubemx-script' file + logger.debug(f"searching for '{stm32pio.settings.cubemx_script_filename}' file...") + cubemx_script_full_filename = project_path.joinpath(stm32pio.settings.cubemx_script_filename) + if not cubemx_script_full_filename.is_file(): + logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file wasn't found, creating one...") + cubemx_script_content = stm32pio.settings.cubemx_script_content.format( + project_path=project_path, cubemx_ioc_full_filename=cubemx_ioc_full_filename) + cubemx_script_full_filename.write_text(cubemx_script_content) + logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file has been successfully created") else: - logger.debug(f"'{settings.cubemx_script_filename}' file is already there") - + logger.debug(f"'{stm32pio.settings.cubemx_script_filename}' file is already there") logger.info("starting to generate a code from the CubeMX .ioc file...") + command_arr = [stm32pio.settings.java_cmd, '-jar', stm32pio.settings.cubemx_path, '-q', + str(cubemx_script_full_filename)] if logger.level <= logging.DEBUG: - # TODO: take out all commands to the extrenal file (possibly JSON or settings.py) for easy maintaining - result = subprocess.run([settings.java_cmd, '-jar', settings.cubemx_path, '-q', cubemx_script_full_filename]) + result = subprocess.run(command_arr) else: - result = subprocess.run([settings.java_cmd, '-jar', settings.cubemx_path, '-q', cubemx_script_full_filename], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = subprocess.run(command_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Or, for Python 3.7 and above: - # result = subprocess.run([settings.java_cmd, '-jar', settings.cubemx_path, '-q', cubemx_script_full_filename], - # capture_output=True) - if result.returncode != 0: + # result = subprocess.run(command_arr, capture_output=True) + if result.returncode == 0: + logger.info("successful code generation") + else: logger.error(f"code generation error (CubeMX return code is {result.returncode}).\n" "Try to enable a verbose output or generate a code from the CubeMX itself.") raise Exception("code generation error") - else: - logger.info("successful code generation") - - - # Clean Windows-only temp files - if settings.my_os == 'Windows': - if os.path.exists(os.path.join(project_path, 'MXTmpFiles')): - logger.debug("del MXTmpFiles/") - shutil.rmtree(os.path.join(project_path, 'MXTmpFiles'), ignore_errors=True) - def pio_init(dirty_path, board): @@ -95,40 +85,31 @@ def pio_init(dirty_path, board): project_path = _get_project_path(dirty_path) - # Check board name - logger.debug("searching for PlatformIO' board...") - result = subprocess.run(['platformio', 'boards'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + logger.debug("searching for PlatformIO board...") + result = subprocess.run([stm32pio.settings.platformio_cmd, 'boards'], encoding='utf-8', + stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Or, for Python 3.7 and above: - # result = subprocess.run(['platformio', 'boards'], capture_output=True, encoding='utf-8') - if result.returncode != 0: - logger.error("failed to start PlatformIO") - raise Exception("failed to start PlatformIO") - else: + # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) + if result.returncode == 0: if board not in result.stdout.split(): logger.error("wrong STM32 board. Run 'platformio boards' for possible names") raise Exception("wrong STM32 board") - + else: + logger.error("failed to start PlatformIO") + raise Exception("failed to start PlatformIO") logger.info("starting PlatformIO project initialization...") - # 02.04.18: both versions work but second one is much more slower - # 06.11.18: all versions have the same speed - if logger.level <= logging.DEBUG: - # result = subprocess.run(f"platformio init -d {project_path} -b {board} -O framework=stm32cube", shell=True) - result = subprocess.run(['platformio', 'init', '-d', project_path, '-b', board, '-O', 'framework=stm32cube']) + command_arr = [stm32pio.settings.platformio_cmd, 'init', '-d', str(project_path), '-b', board, + '-O', 'framework=stm32cube'] + if logger.level > logging.DEBUG: + command_arr.append('--silent') + result = subprocess.run(command_arr) + if result.returncode == 0: + logger.info("successful PlatformIO project initialization") else: - # result = subprocess.run(f"platformio init -d {project_path} -b {board} -O framework=stm32cube", shell=True, - # stdout=subprocess.PIPE, stderr=subprocess.PIPE) - result = subprocess.run(['platformio', 'init', '-d', project_path, '-b', board, '-O', 'framework=stm32cube', - '--silent']) - # Or, for Python 3.7 and above: - # result = subprocess.run(['platformio', 'init', '-d', project_path, '-b', board, '-O', 'framework=stm32cube'], - # capture_output=True) - if result.returncode != 0: logger.error("PlatformIO project initialization error") raise Exception("PlatformIO error") - else: - logger.info("successful PlatformIO project initialization") @@ -142,24 +123,23 @@ def patch_platformio_ini(dirty_path): project_path = _get_project_path(dirty_path) - logger.debug("patching 'platformio.ini' file...") - if os.path.isfile(os.path.join(project_path, 'platformio.ini')): - with open(os.path.join(project_path, 'platformio.ini'), mode='a') as platformio_ini_file: - platformio_ini_file.write(settings.platformio_ini_patch_text) + platformio_ini_file = project_path.joinpath('platformio.ini') + if platformio_ini_file.is_file(): + with platformio_ini_file.open(mode='a') as f: + f.write(stm32pio.settings.platformio_ini_patch_content) logger.info("'platformio.ini' patched") else: logger.warning("'platformio.ini' file not found") + shutil.rmtree(str(project_path.joinpath('include')), ignore_errors=True) + if not project_path.joinpath('SRC').is_dir(): # case sensitive file system + shutil.rmtree(str(project_path.joinpath('src')), ignore_errors=True) - shutil.rmtree(os.path.join(project_path, 'include'), ignore_errors=True) - if not os.path.exists(os.path.join(project_path, 'SRC')): # case sensitive file system - shutil.rmtree(os.path.join(project_path, 'src'), ignore_errors=True) - -def start_editor(dirty_path, editor): +def start_editor(dirty_path, editor_command): """ Start 'editor' with project at 'project_path' opened @@ -173,23 +153,9 @@ def start_editor(dirty_path, editor): logger.info("starting an editor...") try: - if settings.my_os == 'Windows': - if editor == 'atom': - subprocess.run(['atom', project_path], check=True, shell=True) - elif editor == 'vscode': - subprocess.run(['code', project_path], check=True, shell=True) - elif editor == 'sublime': - subprocess.run(['subl', project_path], check=True, shell=True) - else: - if editor == 'atom': - subprocess.run(['atom', project_path], check=True) - elif editor == 'vscode': - subprocess.run(['code', project_path], check=True) - elif editor == 'sublime': - subprocess.run(['subl', project_path], check=True) - + subprocess.run([editor_command, str(project_path)], check=True) except subprocess.CalledProcessError as e: - logger.error(f"Failed to start the editor {editor}: {e.stdout}") + logger.error(f"Failed to start the editor {editor_command}: {e.stderr}") @@ -199,29 +165,29 @@ def pio_build(dirty_path): Args: dirty_path: path to the project + Returns: + 0 if success, raise an exception otherwise """ project_path = _get_project_path(dirty_path) - logger.info("starting PlatformIO build...") - if logger.level <= logging.DEBUG: - result = subprocess.run(['platformio', 'run', '-d', project_path]) + command_arr = [stm32pio.settings.platformio_cmd, 'run', '-d', str(project_path)] + if logger.level > logging.DEBUG: + command_arr.append('--silent') + result = subprocess.run(command_arr) + if result.returncode == 0: + logger.info("successful PlatformIO build") + return result.returncode else: - result = subprocess.run(['platformio', 'run', '-d', project_path, '--silent']) - # Or, for Python 3.7 and above: - # result = subprocess.run(['platformio', 'run', '-d', project_path, '--silent'], capture_output=True) - if result.returncode != 0: logger.error("PlatformIO build error") - raise Exception("PlatformIO error") - else: - logger.info("successful PlatformIO build") + raise Exception("PlatformIO build error") def clean(dirty_path): """ - Clean-up the project folder and preserve only a '.ioc' file + Clean-up the project folder and preserve only an '.ioc' file Args: dirty_path: path to the project @@ -229,19 +195,13 @@ def clean(dirty_path): project_path = _get_project_path(dirty_path) - - # Get folder content - folder_content = os.listdir(project_path) - # Keep the '.ioc' file - if (os.path.basename(project_path) + '.ioc') in folder_content: - folder_content.remove(os.path.basename(project_path) + '.ioc') - - for item in folder_content: - if os.path.isdir(os.path.join(project_path, item)): - shutil.rmtree(os.path.join(project_path, item), ignore_errors=True) - logger.debug(f"del {item}/") - elif os.path.isfile(os.path.join(project_path, item)): - os.remove(os.path.join(project_path, item)) - logger.debug(f"del {item}") + for child in project_path.iterdir(): + if child.name != f"{project_path.name}.ioc": + if child.is_dir(): + shutil.rmtree(str(child), ignore_errors=True) + logger.debug(f"del {child}/") + elif child.is_file(): + child.unlink() + logger.debug(f"del {child}") logger.info("project has been cleaned")