├── .isort.cfg ├── .gitignore ├── tox.ini ├── .pylintrc ├── Makefile ├── pioinstaller ├── pack │ ├── __init__.py │ ├── template.py │ └── packer.py ├── __init__.py ├── exception.py ├── home.py ├── __main__.py ├── util.py ├── penv.py ├── python.py └── core.py ├── .github ├── stale.yml └── workflows │ └── main.yml ├── README.rst ├── tests ├── conftest.py ├── test_packer.py ├── test_python.py ├── test_core.py └── test_penv.py ├── setup.py └── LICENSE /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=88 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # App specific 7 | *.egg-info/ 8 | .tox/ 9 | dist/ 10 | build/ 11 | 12 | # Misc 13 | .idea 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Copyright 2019-present PlatformIO Labs 2 | 3 | [testenv] 4 | usedevelop = True 5 | deps = 6 | black 7 | isort 8 | pylint 9 | pytest 10 | commands = 11 | {envpython} --version 12 | 13 | [testenv:lint] 14 | commands = 15 | {envpython} --version 16 | pioinstaller --version 17 | pylint --rcfile=./.pylintrc ./pioinstaller -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | jobs=1 3 | 4 | [REPORTS] 5 | output-format=colorized 6 | 7 | [MESSAGES CONTROL] 8 | disable= 9 | missing-docstring, 10 | ungrouped-imports, 11 | invalid-name, 12 | duplicate-code, 13 | fixme, 14 | arguments-differ, 15 | useless-object-inheritance, 16 | super-with-arguments, 17 | raise-missing-from, 18 | consider-using-f-string, 19 | unspecified-encoding 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | pylint --rcfile=./.pylintrc ./pioinstaller 3 | 4 | isort: 5 | isort ./tests 6 | isort ./pioinstaller 7 | 8 | format: 9 | black ./pioinstaller 10 | black ./tests 11 | 12 | test: 13 | py.test --verbose --capture=no --exitfirst tests 14 | 15 | pack: 16 | pioinstaller pack 17 | 18 | before-commit: isort format lint 19 | 20 | clean: 21 | find . -name \*.pyc -delete 22 | find . -name __pycache__ -delete 23 | rm -rf .cache 24 | 25 | publish: 26 | python setup.py sdist upload -------------------------------------------------------------------------------- /pioinstaller/pack/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - bug 8 | - known issue 9 | - feature 10 | - enhancement 11 | # Label to use when marking an issue as stale 12 | staleLabel: stale 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. Please provide more details or it will be closed if no 17 | further activity occurs. Thank you for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PlatformIO Core Installer 2 | ========================= 3 | 4 | 5 | .. image:: https://github.com/platformio/platformio-core-installer/workflows/CI/badge.svg 6 | :target: https://docs.platformio.org/en/latest/core/installation.html 7 | :alt: CI Build 8 | .. image:: https://img.shields.io/badge/license-Apache%202.0-blue.svg 9 | :target: https://pypi.python.org/pypi/platformio/ 10 | :alt: License 11 | 12 | 13 | A standalone installer for `PlatformIO Core `_. 14 | 15 | Usage 16 | ----- 17 | 18 | * `Installation Guide `_ 19 | * `Integration with custom applications (extensions, plugins) `_. 20 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from pioinstaller.pack import packer 18 | 19 | 20 | @pytest.fixture(scope="session") 21 | def pio_installer_script(tmpdir_factory): 22 | tmpdir = tmpdir_factory.mktemp("pioinstaller") 23 | return packer.pack(str(tmpdir)) 24 | -------------------------------------------------------------------------------- /tests/test_packer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import subprocess 16 | 17 | from pioinstaller import __version__ 18 | 19 | 20 | def test_pioinstaller_packer(pio_installer_script): 21 | output = subprocess.check_output(["python", pio_installer_script, "--version"]) 22 | assert ("version %s" % __version__) in output.decode() 23 | -------------------------------------------------------------------------------- /tests/test_python.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import subprocess 17 | 18 | import pytest 19 | 20 | 21 | def test_check_default_python(pio_installer_script): 22 | assert ( 23 | subprocess.check_call(["python", pio_installer_script, "check", "python"]) == 0 24 | ) 25 | 26 | 27 | def test_check_conda_python(pio_installer_script): 28 | if not os.getenv("MINICONDA"): 29 | return 30 | with pytest.raises(subprocess.CalledProcessError) as excinfo: 31 | subprocess.check_call( 32 | [os.getenv("MINICONDA"), pio_installer_script, "check", "python"] 33 | ) 34 | -------------------------------------------------------------------------------- /pioinstaller/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging.config 16 | 17 | VERSION = (1, 2, 2) 18 | __version__ = ".".join([str(s) for s in VERSION]) 19 | 20 | __title__ = "platformio-installer" 21 | __description__ = "An installer for PlatformIO Core" 22 | 23 | __url__ = "https://platformio.org" 24 | 25 | __author__ = "PlatformIO Labs" 26 | __email__ = "contact@piolabs.com" 27 | 28 | 29 | __license__ = "Apache-2.0" 30 | __copyright__ = "Copyright 2014-present PlatformIO" 31 | 32 | 33 | logging.basicConfig(format="%(levelname)s: %(message)s") 34 | logging.config.dictConfig( 35 | {"version": 1, "loggers": {"pioinstaller": {"level": "INFO"}}} 36 | ) 37 | -------------------------------------------------------------------------------- /pioinstaller/exception.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class PIOInstallerException(Exception): 17 | MESSAGE = None 18 | 19 | def __str__(self): # pragma: no cover 20 | if self.MESSAGE: 21 | # pylint: disable=not-an-iterable 22 | return self.MESSAGE.format(*self.args) 23 | 24 | return super(PIOInstallerException, self).__str__() 25 | 26 | 27 | class IncompatiblePythonError(PIOInstallerException): 28 | MESSAGE = "{0}" 29 | 30 | 31 | class PythonVenvModuleNotFound(PIOInstallerException): 32 | MESSAGE = "Could not find Python `venv` module" 33 | 34 | 35 | class InvalidPlatformIOCore(PIOInstallerException): 36 | MESSAGE = "{0}" 37 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macos-latest] 11 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Check Python 20 | run: | 21 | python -V 22 | which python 23 | echo $PATH 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -U tox pytest 28 | pip install -e . 29 | - name: Python Lint 30 | run: | 31 | tox -e lint 32 | - name: Integration Tests 33 | run: | 34 | make test 35 | - name: Pack Installer Script 36 | run: | 37 | make pack 38 | - name: Install PlatformIO Core 39 | run: | 40 | python3 get-platformio.py 41 | if [ "$RUNNER_OS" == "Windows" ]; then 42 | ~/.platformio/penv/Scripts/pio.exe system info 43 | else 44 | ~/.platformio/penv/bin/pio system info 45 | fi 46 | shell: bash 47 | -------------------------------------------------------------------------------- /pioinstaller/home.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import multiprocessing 17 | 18 | import requests 19 | 20 | HTTP_HOST = "127.0.0.1" 21 | HTTP_PORT_BEGIN = 8008 22 | HTTP_PORT_END = 8050 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | def _shutdown(): 28 | for port in range(HTTP_PORT_BEGIN, HTTP_PORT_END): 29 | try: 30 | requests.get( 31 | "http://%s:%d?__shutdown__=1" % (HTTP_HOST, port), timeout=(0.5, 2) 32 | ) 33 | log.debug("The server %s:%d is stopped", HTTP_HOST, port) 34 | except: # pylint:disable=bare-except 35 | pass 36 | 37 | 38 | def shutdown_pio_home_servers(): 39 | proc = multiprocessing.Process(target=_shutdown) 40 | proc.start() 41 | proc.join(10) 42 | proc.terminate() 43 | return True 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from pioinstaller import ( 17 | __author__, 18 | __description__, 19 | __email__, 20 | __license__, 21 | __title__, 22 | __url__, 23 | __version__, 24 | ) 25 | from setuptools import find_packages, setup 26 | 27 | setup( 28 | name=__title__, 29 | version=__version__, 30 | description=__description__, 31 | long_description=open("README.rst").read(), 32 | author=__author__, 33 | author_email=__email__, 34 | url=__url__, 35 | license=__license__, 36 | install_requires=[ 37 | # Core 38 | "click==8.0.4", # >8.0.4 does not support Python 3.6 39 | "urllib3<2", # issue 4614: urllib3 v2.0 only supports OpenSSL 1.1.1+ 40 | "requests==2.31.0", 41 | "colorama==0.4.6", 42 | "semantic-version==2.8.5", # >2.8.5 does not support Python 3.6 43 | "certifi==2023.11.17", 44 | # Misc 45 | "wheel==0.42.0", 46 | ], 47 | packages=find_packages(), 48 | entry_points={ 49 | "console_scripts": [ 50 | "pioinstaller = pioinstaller.__main__:main", 51 | ] 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /pioinstaller/pack/template.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # pylint:disable=bad-option-value,import-outside-toplevel 16 | 17 | import os 18 | import shutil 19 | import sys 20 | import tempfile 21 | from base64 import b64decode 22 | 23 | DEPENDENCIES = b""" 24 | {zipfile_content} 25 | """ 26 | 27 | 28 | def create_temp_dir(): 29 | try: 30 | parent_dir = os.getenv( 31 | "PLATFORMIO_INSTALLER_TMPDIR", os.path.dirname(os.path.realpath(__file__)) 32 | ) 33 | tmp_dir = tempfile.mkdtemp(dir=parent_dir, prefix=".piocore-installer-") 34 | testscript_path = os.path.join(tmp_dir, "test.py") 35 | with open(testscript_path, "w") as fp: 36 | fp.write("print(1)") 37 | assert os.path.isfile(testscript_path) 38 | os.remove(testscript_path) 39 | return tmp_dir 40 | except (AssertionError, NameError): 41 | pass 42 | return tempfile.mkdtemp() 43 | 44 | 45 | def bootstrap(): 46 | import pioinstaller.__main__ 47 | 48 | pioinstaller.__main__.main() 49 | 50 | 51 | def main(): 52 | runtime_tmp_dir = create_temp_dir() 53 | os.environ["TMPDIR"] = runtime_tmp_dir 54 | tmp_dir = tempfile.mkdtemp(dir=runtime_tmp_dir) 55 | try: 56 | pioinstaller_zip = os.path.join(tmp_dir, "pioinstaller.zip") 57 | with open(pioinstaller_zip, "wb") as fp: 58 | fp.write(b64decode(DEPENDENCIES)) 59 | 60 | sys.path.insert(0, pioinstaller_zip) 61 | 62 | bootstrap() 63 | finally: 64 | for d in (runtime_tmp_dir, tmp_dir): 65 | if d and os.path.isdir(d): 66 | shutil.rmtree(d, ignore_errors=True) 67 | 68 | 69 | if __name__ == "__main__": 70 | main() 71 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import os 17 | import subprocess 18 | 19 | from pioinstaller import __version__, core, penv, util 20 | 21 | 22 | def test_install_pio_core(pio_installer_script, tmpdir, monkeypatch): 23 | monkeypatch.setattr(util, "get_installer_script", lambda: pio_installer_script) 24 | 25 | core_dir = tmpdir.mkdir(".pio") 26 | penv_dir = str(core_dir.mkdir("penv")) 27 | os.environ["PLATFORMIO_CORE_DIR"] = str(core_dir) 28 | 29 | assert core.install_platformio_core(shutdown_piohome=False) 30 | 31 | python_exe = os.path.join( 32 | penv.get_penv_bin_dir(penv_dir), "python.exe" if util.IS_WINDOWS else "python" 33 | ) 34 | assert subprocess.check_call([python_exe, "-m", "platformio", "--version"]) == 0 35 | 36 | core_state_path = os.path.join(str(core_dir), "core-state.json") 37 | assert ( 38 | subprocess.check_call( 39 | [ 40 | "python", 41 | pio_installer_script, 42 | "check", 43 | "core", 44 | "--dump-state=%s" % core_state_path, 45 | ], 46 | stderr=subprocess.STDOUT, 47 | ) 48 | == 0 49 | ) 50 | with open(core_state_path) as fp: 51 | json_info = json.load(fp) 52 | assert json_info.get("core_dir") == str(core_dir) 53 | assert json_info.get("penv_dir") == penv_dir 54 | assert json_info.get("installer_version") == __version__ 55 | assert json_info.get("system") == util.get_systype() 56 | 57 | assert os.path.isfile( 58 | os.path.join( 59 | penv.get_penv_bin_dir(penv_dir), 60 | "platformio.exe" if util.IS_WINDOWS else "platformio", 61 | ) 62 | ) 63 | -------------------------------------------------------------------------------- /pioinstaller/pack/packer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import base64 16 | import io 17 | import os 18 | import re 19 | import shutil 20 | import subprocess 21 | import tempfile 22 | import zipfile 23 | 24 | from pioinstaller import util 25 | 26 | 27 | def create_wheels(package_dir, dest_dir): 28 | subprocess.call(["pip", "wheel", "--wheel-dir", dest_dir, "."], cwd=package_dir) 29 | 30 | 31 | def pack(target): 32 | assert isinstance(target, str) 33 | 34 | if os.path.isdir(target): 35 | target = os.path.join(target, "get-platformio.py") 36 | if not os.path.isdir(os.path.dirname(target)): 37 | os.makedirs(os.path.dirname(target)) 38 | 39 | tmp_dir = tempfile.mkdtemp() 40 | create_wheels(os.path.dirname(util.get_source_dir()), tmp_dir) 41 | 42 | new_data = io.BytesIO() 43 | for whl in os.listdir(tmp_dir): 44 | with zipfile.ZipFile(os.path.join(tmp_dir, whl)) as existing_zip: 45 | with zipfile.ZipFile(new_data, mode="a") as new_zip: 46 | for zinfo in existing_zip.infolist(): 47 | if re.search(r"\.dist-info/", zinfo.filename): 48 | continue 49 | new_zip.writestr(zinfo, existing_zip.read(zinfo)) 50 | zipdata = base64.b64encode(new_data.getvalue()).decode("utf8") 51 | with open(target, "w") as fp: 52 | with open(os.path.join(util.get_source_dir(), "pack", "template.py")) as fptlp: 53 | fp.write(fptlp.read().format(zipfile_content=zipdata)) 54 | 55 | # Ensure the permissions on the newly created file 56 | oldmode = os.stat(target).st_mode & 0o7777 57 | newmode = (oldmode | 0o555) & 0o7777 58 | os.chmod(target, newmode) 59 | 60 | # Clearing up 61 | shutil.rmtree(tmp_dir) 62 | 63 | return target 64 | -------------------------------------------------------------------------------- /tests/test_penv.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import os 17 | import subprocess 18 | 19 | from pioinstaller import __version__, penv, python, util 20 | 21 | 22 | def test_penv_with_default_python(pio_installer_script, tmpdir, monkeypatch): 23 | monkeypatch.setattr(util, "get_installer_script", lambda: pio_installer_script) 24 | 25 | penv_dir = str(tmpdir.mkdir("penv")) 26 | 27 | assert penv.create_core_penv(penv_dir=penv_dir) 28 | 29 | python_exe = os.path.join( 30 | penv.get_penv_bin_dir(penv_dir), "python.exe" if util.IS_WINDOWS else "python" 31 | ) 32 | assert ( 33 | subprocess.check_call([python_exe, pio_installer_script, "check", "python"]) 34 | == 0 35 | ) 36 | with open(os.path.join(penv_dir, "state.json")) as fp: 37 | json_info = json.load(fp) 38 | assert json_info.get("installer_version") == __version__ 39 | 40 | 41 | def test_penv_with_downloadable_venv(pio_installer_script, tmpdir, monkeypatch): 42 | monkeypatch.setattr(util, "get_installer_script", lambda: pio_installer_script) 43 | 44 | penv_dir = str(tmpdir.mkdir("penv")) 45 | 46 | python_exes = python.find_compatible_pythons() 47 | if not python_exes: 48 | raise Exception("Python executable not found.") 49 | python_exe = python_exes[0] 50 | 51 | assert penv.create_with_remote_venv(python_exe=python_exe, penv_dir=penv_dir) 52 | 53 | python_exe = os.path.join( 54 | penv.get_penv_bin_dir(penv_dir), "python.exe" if util.IS_WINDOWS else "python" 55 | ) 56 | assert ( 57 | subprocess.check_call([python_exe, pio_installer_script, "check", "python"]) 58 | == 0 59 | ) 60 | 61 | 62 | def test_penv_with_portable_python(pio_installer_script, tmpdir, monkeypatch): 63 | if not util.IS_WINDOWS: 64 | return 65 | monkeypatch.setattr(util, "get_installer_script", lambda: pio_installer_script) 66 | 67 | penv_dir = str(tmpdir.mkdir("penv")) 68 | 69 | python_exe = python.fetch_portable_python(os.path.dirname(penv_dir)) 70 | assert penv.create_virtualenv(python_exe=python_exe, penv_dir=penv_dir) 71 | 72 | python_exe = os.path.join( 73 | penv.get_penv_bin_dir(penv_dir), "python.exe" if util.IS_WINDOWS else "python" 74 | ) 75 | assert ( 76 | subprocess.check_call([python_exe, pio_installer_script, "check", "python"]) 77 | == 0 78 | ) 79 | -------------------------------------------------------------------------------- /pioinstaller/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import os 17 | import platform 18 | import sys 19 | 20 | import click 21 | 22 | from pioinstaller import __title__, __version__, core, exception, util 23 | from pioinstaller.pack import packer 24 | from pioinstaller.python import check as python_check 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | 29 | @click.group(name="main", invoke_without_command=True) 30 | @click.version_option(__version__, prog_name=__title__) 31 | @click.option("--verbose", is_flag=True, default=False, help="Verbose output") 32 | @click.option("--shutdown-piohome/--no-shutdown-piohome", is_flag=True, default=True) 33 | @click.option("--dev", is_flag=True, default=False) 34 | @click.option( 35 | "--ignore-python", 36 | multiple=True, 37 | help="A path to Python to be ignored (multiple options and unix wildcards are allowed)", 38 | ) 39 | @click.option( 40 | "--pypi-index-url", 41 | help="Custom base URL of the Python Package Index (default `https://pypi.org/simple`)", 42 | ) 43 | @click.pass_context 44 | def cli( 45 | ctx, verbose, shutdown_piohome, dev, ignore_python, pypi_index_url 46 | ): # pylint:disable=too-many-arguments 47 | if verbose: 48 | logging.getLogger("pioinstaller").setLevel(logging.DEBUG) 49 | if pypi_index_url: 50 | os.environ["PIP_INDEX_URL"] = pypi_index_url 51 | ctx.obj["dev"] = dev 52 | if ctx.invoked_subcommand: 53 | return 54 | 55 | click.echo("Installer version: %s" % __version__) 56 | click.echo("Platform: %s" % platform.platform(terse=True)) 57 | click.echo("Python version: %s" % sys.version) 58 | click.echo("Python path: %s" % sys.executable) 59 | 60 | try: 61 | core.install_platformio_core(shutdown_piohome, dev, ignore_python) 62 | except exception.PIOInstallerException as exc: 63 | raise click.ClickException(str(exc)) 64 | 65 | 66 | @cli.command() 67 | @click.argument( 68 | "target", 69 | default=os.getcwd, 70 | required=False, 71 | type=click.Path( 72 | exists=False, file_okay=True, dir_okay=True, writable=True, resolve_path=True 73 | ), 74 | ) 75 | def pack(target): 76 | return packer.pack(target) 77 | 78 | 79 | @cli.group() 80 | def check(): 81 | pass 82 | 83 | 84 | @check.command() 85 | def python(): 86 | try: 87 | python_check() 88 | click.secho( 89 | "The Python %s (%s) interpreter is compatible." 90 | % (platform.python_version(), util.get_pythonexe_path()), 91 | fg="green", 92 | ) 93 | except (exception.IncompatiblePythonError, exception.PythonVenvModuleNotFound) as e: 94 | raise click.ClickException( 95 | "The Python %s (%s) interpreter is not compatible.\nReason: %s" 96 | % (platform.python_version(), util.get_pythonexe_path(), str(e)) 97 | ) 98 | 99 | 100 | @check.command("core") 101 | @click.option("--auto-upgrade/--no-auto-upgrade", is_flag=True, default=True) 102 | @click.option("--global", is_flag=True, default=False) 103 | @click.option("--version-spec", default=None) 104 | @click.option( 105 | "--dump-state", 106 | type=click.Path( 107 | exists=False, file_okay=True, dir_okay=True, writable=True, resolve_path=True 108 | ), 109 | ) 110 | @click.pass_context 111 | def core_check(ctx, **kwargs): 112 | try: 113 | state = core.check( 114 | develop=ctx.obj.get("dev", False), 115 | global_=kwargs.get("global"), 116 | auto_upgrade=kwargs.get("auto_upgrade"), 117 | version_spec=kwargs.get("version_spec"), 118 | ) 119 | if kwargs.get("dump_state"): 120 | core.dump_state(target=str(kwargs.get("dump_state")), state=state) 121 | click.secho( 122 | "Found compatible PlatformIO Core %s -> %s" 123 | % (state.get("core_version"), state.get("platformio_exe")), 124 | fg="green", 125 | ) 126 | except exception.InvalidPlatformIOCore as e: 127 | raise click.ClickException( 128 | "Compatible PlatformIO Core not found.\nReason: %s" % str(e) 129 | ) 130 | 131 | 132 | def main(): 133 | return cli(obj={}) # pylint: disable=no-value-for-parameter, unexpected-keyword-arg 134 | 135 | 136 | if __name__ == "__main__": 137 | sys.exit(main()) 138 | -------------------------------------------------------------------------------- /pioinstaller/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import io 16 | import logging 17 | import os 18 | import platform 19 | import re 20 | import shutil 21 | import stat 22 | import subprocess 23 | import sys 24 | import tarfile 25 | 26 | import requests 27 | 28 | IS_WINDOWS = sys.platform.lower().startswith("win") 29 | IS_MACOS = sys.platform.lower() == "darwin" 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | 34 | def get_source_dir(): 35 | curpath = os.path.realpath(__file__) 36 | if not os.path.isfile(curpath): 37 | for p in sys.path: 38 | if os.path.isfile(os.path.join(p, __file__)): 39 | curpath = os.path.join(p, __file__) 40 | break 41 | return os.path.dirname(curpath) 42 | 43 | 44 | def get_pythonexe_path(): 45 | return os.environ.get("PYTHONEXEPATH", os.path.normpath(sys.executable)) 46 | 47 | 48 | def expanduser(path): 49 | """ 50 | Be compatible with Python 3.8, on Windows skip HOME and check for USERPROFILE 51 | """ 52 | if not IS_WINDOWS or not path.startswith("~") or "USERPROFILE" not in os.environ: 53 | return os.path.expanduser(path) 54 | return os.environ["USERPROFILE"] + path[1:] 55 | 56 | 57 | def has_non_ascii_char(text): 58 | for c in text: 59 | if ord(c) >= 128: 60 | return True 61 | return False 62 | 63 | 64 | def rmtree(path): 65 | def _onerror(func, path, __): 66 | st_mode = os.stat(path).st_mode 67 | if st_mode & stat.S_IREAD: 68 | os.chmod(path, st_mode | stat.S_IWRITE) 69 | func(path) 70 | 71 | return shutil.rmtree(path, onerror=_onerror) 72 | 73 | 74 | def find_file(name, path): 75 | for root, _, files in os.walk(path): 76 | if name in files: 77 | return os.path.join(root, name) 78 | return None 79 | 80 | 81 | def safe_create_dir(path, raise_exception=False): 82 | try: 83 | os.makedirs(path) 84 | return path 85 | except Exception as e: # pylint: disable=broad-except 86 | if raise_exception: 87 | raise e 88 | return None 89 | 90 | 91 | def download_file(url, dst, cache=True): 92 | if cache: 93 | content_length = requests.head(url, timeout=10).headers.get("Content-Length") 94 | if os.path.isfile(dst) and content_length == os.path.getsize(dst): 95 | log.debug("Getting from cache: %s", dst) 96 | return dst 97 | 98 | resp = requests.get(url, stream=True, timeout=10) 99 | itercontent = resp.iter_content(chunk_size=io.DEFAULT_BUFFER_SIZE) 100 | safe_create_dir(os.path.dirname(dst)) 101 | with open(dst, "wb") as fp: 102 | for chunk in itercontent: 103 | fp.write(chunk) 104 | return dst 105 | 106 | 107 | def unpack_archive(src, dst): 108 | assert src.endswith("tar.gz") 109 | with tarfile.open(src, mode="r:gz") as fp: 110 | fp.extractall(dst) 111 | return dst 112 | 113 | 114 | def get_installer_script(): 115 | return os.path.abspath(sys.argv[0]) 116 | 117 | 118 | def get_systype(): 119 | type_ = platform.system().lower() 120 | arch = platform.machine().lower() 121 | if type_ == "windows": 122 | arch = "amd64" if platform.architecture()[0] == "64bit" else "x86" 123 | return "%s_%s" % (type_, arch) if arch else type_ 124 | 125 | 126 | def safe_remove_dir(path, raise_exception=False): 127 | try: 128 | return rmtree(path) 129 | except Exception as e: # pylint: disable=broad-except 130 | if raise_exception: 131 | raise e 132 | return None 133 | 134 | 135 | def pepver_to_semver(pepver): 136 | return re.sub(r"(\.\d+)\.?(dev|a|b|rc|post)", r"\1-\2.", pepver, 1) 137 | 138 | 139 | def where_is_program(program, envpath=None): 140 | env = os.environ 141 | if envpath: 142 | env["PATH"] = envpath 143 | 144 | # try OS's built-in commands 145 | try: 146 | result = ( 147 | subprocess.check_output( 148 | ["where" if IS_WINDOWS else "which", program], env=env 149 | ) 150 | .decode() 151 | .strip() 152 | ) 153 | if os.path.isfile(result): 154 | return result 155 | except (subprocess.CalledProcessError, OSError): 156 | pass 157 | 158 | # look up in $PATH 159 | for bin_dir in env.get("PATH", "").split(os.pathsep): 160 | if os.path.isfile(os.path.join(bin_dir, program)): 161 | return os.path.join(bin_dir, program) 162 | if os.path.isfile(os.path.join(bin_dir, "%s.exe" % program)): 163 | return os.path.join(bin_dir, "%s.exe" % program) 164 | 165 | return program 166 | -------------------------------------------------------------------------------- /pioinstaller/penv.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import logging 17 | import os 18 | import platform 19 | import subprocess 20 | import time 21 | 22 | import click 23 | 24 | from pioinstaller import __version__, core, exception, python, util 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | 29 | VIRTUALENV_URL = "https://bootstrap.pypa.io/virtualenv/virtualenv.pyz" 30 | PIP_URL = "https://bootstrap.pypa.io/get-pip.py" 31 | 32 | 33 | def get_penv_dir(path=None): 34 | if os.getenv("PLATFORMIO_PENV_DIR"): 35 | return os.getenv("PLATFORMIO_PENV_DIR") 36 | 37 | core_dir = path or core.get_core_dir() 38 | return os.path.join(core_dir, "penv") 39 | 40 | 41 | def get_penv_bin_dir(path=None): 42 | penv_dir = path or get_penv_dir() 43 | return os.path.join(penv_dir, "Scripts" if util.IS_WINDOWS else "bin") 44 | 45 | 46 | def create_core_penv(penv_dir=None, ignore_pythons=None): 47 | penv_dir = penv_dir or get_penv_dir() 48 | 49 | click.echo("Creating a virtual environment at %s" % penv_dir) 50 | 51 | result_dir = None 52 | for python_exe in python.find_compatible_pythons(ignore_pythons): 53 | result_dir = create_virtualenv(python_exe, penv_dir) 54 | if result_dir: 55 | break 56 | 57 | if not result_dir and not python.is_portable(): 58 | python_exe = python.fetch_portable_python(os.path.dirname(penv_dir)) 59 | if python_exe: 60 | result_dir = create_virtualenv(python_exe, penv_dir) 61 | 62 | if not result_dir: 63 | raise exception.PIOInstallerException( 64 | "Could not create PIO Core Virtual Environment. Please report to " 65 | "https://github.com/platformio/platformio-core-installer/issues" 66 | ) 67 | 68 | python_exe = os.path.join( 69 | get_penv_bin_dir(penv_dir), "python.exe" if util.IS_WINDOWS else "python" 70 | ) 71 | init_state(python_exe, penv_dir) 72 | update_pip(python_exe, penv_dir) 73 | click.echo("Virtual environment has been successfully created!") 74 | return result_dir 75 | 76 | 77 | def create_virtualenv(python_exe, penv_dir): 78 | log.debug("Using %s Python for virtual environment.", python_exe) 79 | try: 80 | return create_with_local_venv(python_exe, penv_dir) 81 | except Exception as e: # pylint:disable=broad-except 82 | log.debug( 83 | "Could not create virtualenv with local packages" 84 | " Trying download virtualenv script and using it. Error: %s", 85 | str(e), 86 | ) 87 | try: 88 | return create_with_remote_venv(python_exe, penv_dir) 89 | except Exception as exc: # pylint:disable=broad-except 90 | log.debug( 91 | "Could not create virtualenv with downloaded script. Error: %s", 92 | str(exc), 93 | ) 94 | return None 95 | 96 | 97 | def create_with_local_venv(python_exe, penv_dir): 98 | venv_cmd_options = [ 99 | [python_exe, "-m", "venv", penv_dir], 100 | [python_exe, "-m", "virtualenv", "-p", python_exe, penv_dir], 101 | ["virtualenv", "-p", python_exe, penv_dir], 102 | [python_exe, "-m", "virtualenv", penv_dir], 103 | ["virtualenv", penv_dir], 104 | ] 105 | last_error = None 106 | for command in venv_cmd_options: 107 | util.safe_remove_dir(penv_dir) 108 | log.debug("Creating virtual environment: %s", " ".join(command)) 109 | try: 110 | subprocess.run(command, check=True) 111 | return penv_dir 112 | except Exception as e: # pylint:disable=broad-except 113 | last_error = e 114 | raise last_error # pylint:disable=raising-bad-type 115 | 116 | 117 | def create_with_remote_venv(python_exe, penv_dir): 118 | util.safe_remove_dir(penv_dir) 119 | 120 | log.debug("Downloading virtualenv package archive") 121 | venv_script_path = util.download_file( 122 | VIRTUALENV_URL, 123 | os.path.join( 124 | os.path.dirname(penv_dir), ".cache", "tmp", os.path.basename(VIRTUALENV_URL) 125 | ), 126 | ) 127 | if not venv_script_path: 128 | raise exception.PIOInstallerException("Could not find virtualenv script") 129 | command = [python_exe, venv_script_path, penv_dir] 130 | log.debug("Creating virtual environment: %s", " ".join(command)) 131 | subprocess.run(command, check=True) 132 | return penv_dir 133 | 134 | 135 | def init_state(python_exe, penv_dir): 136 | version_code = ( 137 | "import sys; version=sys.version_info; " 138 | "print('%d.%d.%d'%(version[0],version[1],version[2]))" 139 | ) 140 | python_version = ( 141 | subprocess.check_output( 142 | [python_exe, "-c", version_code], stderr=subprocess.PIPE 143 | ) 144 | .decode() 145 | .strip() 146 | ) 147 | state = { 148 | "created_on": int(round(time.time())), 149 | "python": { 150 | "path": python_exe, 151 | "version": python_version, 152 | }, 153 | "installer_version": __version__, 154 | "platform": { 155 | "platform": platform.platform(), 156 | "release": platform.release(), 157 | }, 158 | } 159 | return save_state(state, penv_dir) 160 | 161 | 162 | def load_state(penv_dir=None): 163 | penv_dir = penv_dir or get_penv_dir() 164 | state_path = os.path.join(penv_dir, "state.json") 165 | if not os.path.isfile(state_path): 166 | raise exception.PIOInstallerException( 167 | "Could not found state.json file in `%s`" % state_path 168 | ) 169 | with open(state_path) as fp: 170 | return json.load(fp) 171 | 172 | 173 | def save_state(state, penv_dir=None): 174 | penv_dir = penv_dir or get_penv_dir() 175 | state_path = os.path.join(penv_dir, "state.json") 176 | with open(state_path, "w") as fp: 177 | json.dump(state, fp) 178 | return state_path 179 | 180 | 181 | def update_pip(python_exe, penv_dir): 182 | click.echo("Updating Python package manager (PIP) in the virtual environment") 183 | try: 184 | log.debug("Creating pip.conf file in %s", penv_dir) 185 | with open(os.path.join(penv_dir, "pip.conf"), "w") as fp: 186 | fp.write("\n".join(["[global]", "user=no"])) 187 | 188 | try: 189 | log.debug("Updating PIP ...") 190 | subprocess.run( 191 | [python_exe, "-m", "pip", "install", "-U", "pip"], check=True 192 | ) 193 | except subprocess.CalledProcessError as e: 194 | log.debug( 195 | "Could not update PIP. Error: %s", 196 | str(e), 197 | ) 198 | log.debug("Downloading 'get-pip.py' installer...") 199 | get_pip_path = os.path.join( 200 | os.path.dirname(penv_dir), ".cache", "tmp", os.path.basename(PIP_URL) 201 | ) 202 | util.download_file(PIP_URL, get_pip_path) 203 | log.debug("Installing PIP ...") 204 | subprocess.run([python_exe, get_pip_path], check=True) 205 | 206 | click.echo("PIP has been successfully updated!") 207 | return True 208 | except Exception as e: # pylint:disable=broad-except 209 | log.debug( 210 | "Could not install PIP. Error: %s", 211 | str(e), 212 | ) 213 | return False 214 | -------------------------------------------------------------------------------- /pioinstaller/python.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import glob 16 | import json 17 | import logging 18 | import os 19 | import platform 20 | import subprocess 21 | import sys 22 | import tempfile 23 | 24 | import click 25 | import requests 26 | import semantic_version 27 | 28 | from pioinstaller import exception, util 29 | 30 | log = logging.getLogger(__name__) 31 | 32 | 33 | def is_conda(): 34 | return any( 35 | [ 36 | os.path.exists(os.path.join(sys.prefix, "conda-meta")), 37 | # (os.getenv("CONDA_PREFIX") or os.getenv("CONDA_DEFAULT_ENV")), 38 | "anaconda" in sys.executable.lower(), 39 | "miniconda" in sys.executable.lower(), 40 | "continuum analytics" in sys.version.lower(), 41 | "conda" in sys.version.lower(), 42 | ] 43 | ) 44 | 45 | 46 | def is_portable(): 47 | try: 48 | __import__("winpython") 49 | 50 | return True 51 | except: # pylint:disable=bare-except 52 | pass 53 | print(os.path.normpath(sys.executable)) 54 | python_dir = os.path.dirname(sys.executable) 55 | if not util.IS_WINDOWS: 56 | # skip "bin" folder 57 | python_dir = os.path.dirname(python_dir) 58 | manifest_path = os.path.join(python_dir, "package.json") 59 | if not os.path.isfile(manifest_path): 60 | return False 61 | try: 62 | with open(manifest_path) as fp: 63 | return json.load(fp).get("name") == "python-portable" 64 | except ValueError: 65 | pass 66 | return False 67 | 68 | 69 | def fetch_portable_python(dst): 70 | url = get_portable_python_url() 71 | if not url: 72 | log.debug("Could not find portable Python for %s", util.get_systype()) 73 | return None 74 | try: 75 | log.debug("Downloading portable python...") 76 | 77 | archive_path = util.download_file( 78 | url, os.path.join(os.path.join(dst, ".cache", "tmp"), os.path.basename(url)) 79 | ) 80 | 81 | python_dir = os.path.join(dst, "python3") 82 | util.safe_remove_dir(python_dir) 83 | util.safe_create_dir(python_dir, raise_exception=True) 84 | 85 | log.debug("Unpacking portable python...") 86 | util.unpack_archive(archive_path, python_dir) 87 | if util.IS_WINDOWS: 88 | return os.path.join(python_dir, "python.exe") 89 | return os.path.join(python_dir, "bin", "python3") 90 | except: # pylint:disable=bare-except 91 | log.debug("Could not download portable python") 92 | return None 93 | 94 | 95 | def get_portable_python_url(): 96 | systype = util.get_systype() 97 | result = requests.get( 98 | "https://api.registry.platformio.org/v3/packages/" 99 | "platformio/tool/python-portable", 100 | timeout=10, 101 | ).json() 102 | versions = [ 103 | version 104 | for version in result["versions"] 105 | if is_version_system_compatible(version, systype) 106 | ] 107 | best_version = {} 108 | for version in versions: 109 | if not best_version or semantic_version.Version( 110 | version["name"] 111 | ) > semantic_version.Version(best_version["name"]): 112 | best_version = version 113 | for item in best_version.get("files", []): 114 | if systype in item["system"]: 115 | return item["download_url"] 116 | return None 117 | 118 | 119 | def is_version_system_compatible(version, systype): 120 | return any(systype in item["system"] for item in version["files"]) 121 | 122 | 123 | def check(): 124 | # platform check 125 | if sys.platform == "cygwin": 126 | raise exception.IncompatiblePythonError("Unsupported Cygwin platform") 127 | 128 | # version check 129 | if sys.version_info < (3, 6): 130 | raise exception.IncompatiblePythonError( 131 | "Unsupported Python version: %s. " 132 | "Minimum supported Python version is 3.6 or above." 133 | % platform.python_version(), 134 | ) 135 | 136 | # conda check 137 | if is_conda(): 138 | raise exception.IncompatiblePythonError("Conda is not supported") 139 | 140 | try: 141 | __import__("ensurepip") 142 | __import__("venv") 143 | # __import__("distutils.command") 144 | except ImportError: 145 | raise exception.PythonVenvModuleNotFound() 146 | 147 | # portable Python 3 for macOS is not compatible with macOS < 10.13 148 | # https://github.com/platformio/platformio-core-installer/issues/70 149 | if util.IS_MACOS: 150 | with tempfile.NamedTemporaryFile() as tmpfile: 151 | os.utime(tmpfile.name) 152 | 153 | if not util.IS_WINDOWS: 154 | return True 155 | 156 | # windows check 157 | if any(s in util.get_pythonexe_path().lower() for s in ("msys", "mingw", "emacs")): 158 | raise exception.IncompatiblePythonError( 159 | "Unsupported environments: msys, mingw, emacs >> %s" 160 | % util.get_pythonexe_path(), 161 | ) 162 | 163 | try: 164 | assert os.path.isdir(os.path.join(sys.prefix, "Scripts")) or ( 165 | sys.version_info >= (3, 5) and __import__("venv") 166 | ) 167 | except (AssertionError, ImportError): 168 | raise exception.IncompatiblePythonError( 169 | "Unsupported python without 'Scripts' folder and 'venv' module" 170 | ) 171 | 172 | return True 173 | 174 | 175 | def find_compatible_pythons( 176 | ignore_pythons=None, raise_exception=True 177 | ): # pylint: disable=too-many-branches 178 | ignore_list = [] 179 | for p in ignore_pythons or []: 180 | ignore_list.extend(glob.glob(p)) 181 | exenames = [ 182 | "python3", # system Python 183 | "python3.11", 184 | "python3.10", 185 | "python3.9", 186 | "python3.8", 187 | "python3.7", 188 | "python", 189 | ] 190 | if util.IS_WINDOWS: 191 | exenames = ["%s.exe" % item for item in exenames] 192 | log.debug("Current environment PATH %s", os.getenv("PATH")) 193 | candidates = [] 194 | for exe in exenames: 195 | for path in os.getenv("PATH").split(os.pathsep): 196 | if not os.path.isfile(os.path.join(path, exe)): 197 | continue 198 | candidates.append(os.path.join(path, exe)) 199 | 200 | if sys.executable in candidates: 201 | candidates.remove(sys.executable) 202 | # put current Python to the top of list 203 | candidates.insert(0, sys.executable) 204 | 205 | result = [] 206 | missed_venv_module = False 207 | for item in candidates: 208 | if item in ignore_list: 209 | continue 210 | log.debug("Checking a Python candidate %s", item) 211 | try: 212 | output = subprocess.check_output( 213 | [ 214 | item, 215 | util.get_installer_script(), 216 | "--no-shutdown-piohome", 217 | "check", 218 | "python", 219 | ], 220 | stderr=subprocess.STDOUT, 221 | ) 222 | result.append(item) 223 | try: 224 | log.debug(output.decode().strip()) 225 | except UnicodeDecodeError: 226 | pass 227 | except subprocess.CalledProcessError as e: 228 | try: 229 | error = e.output.decode() 230 | if error and "`venv` module" in error: 231 | missed_venv_module = True 232 | log.debug(error) 233 | except UnicodeDecodeError: 234 | pass 235 | except Exception as e: # pylint: disable=broad-except 236 | log.debug(e) 237 | 238 | if not result and raise_exception: 239 | if missed_venv_module: 240 | # pylint:disable=line-too-long 241 | raise click.ClickException( 242 | """Can not install PlatformIO Core due to a missed `venv` module in your Python installation. 243 | Please install this package manually using the OS package manager. For example: 244 | 245 | $ apt-get install python3.%d-venv 246 | 247 | (MAY require administrator access `sudo`)""" 248 | % (sys.version_info[1]), 249 | ) 250 | 251 | raise exception.IncompatiblePythonError( 252 | "Could not find compatible Python 3.6 or above in your system." 253 | "Please install the latest official Python 3 and restart installation:\n" 254 | "https://docs.platformio.org/page/faq.html#install-python-interpreter" 255 | ) 256 | 257 | return result 258 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pioinstaller/core.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-present PlatformIO 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # pylint: disable=import-outside-toplevel 16 | 17 | import json 18 | import logging 19 | import os 20 | import platform 21 | import subprocess 22 | import sys 23 | import time 24 | 25 | import click 26 | import semantic_version 27 | 28 | from pioinstaller import __version__, exception, home, util 29 | 30 | log = logging.getLogger(__name__) 31 | 32 | PIO_CORE_DEVELOP_URL = "https://github.com/platformio/platformio/archive/develop.zip" 33 | UPDATE_INTERVAL = 60 * 60 * 24 * 3 # 3 days 34 | 35 | 36 | def get_core_dir(force_to_root=False): 37 | if os.getenv("PLATFORMIO_CORE_DIR"): 38 | return os.getenv("PLATFORMIO_CORE_DIR") 39 | 40 | core_dir = os.path.join(util.expanduser("~"), ".platformio") 41 | if not util.IS_WINDOWS: 42 | return core_dir 43 | 44 | win_root_dir = os.path.splitdrive(core_dir)[0] + "\\.platformio" 45 | if os.path.isdir(win_root_dir): 46 | return win_root_dir 47 | try: 48 | if util.has_non_ascii_char(core_dir) or force_to_root: 49 | os.makedirs(win_root_dir) 50 | with open(os.path.join(win_root_dir, "file.tmp"), "w") as fp: 51 | fp.write("test") 52 | os.remove(os.path.join(win_root_dir, "file.tmp")) 53 | return win_root_dir 54 | except: # pylint:disable=bare-except 55 | pass 56 | 57 | return core_dir 58 | 59 | 60 | def get_cache_dir(): 61 | cache_dir = ( 62 | os.getenv("PLATFORMIO_CACHE_DIR") 63 | if os.getenv("PLATFORMIO_CACHE_DIR") 64 | else os.path.join(get_core_dir(), ".cache") 65 | ) 66 | if not os.path.isdir(cache_dir): 67 | os.makedirs(cache_dir) 68 | return cache_dir 69 | 70 | 71 | def install_platformio_core(shutdown_piohome=True, develop=False, ignore_pythons=None): 72 | try: 73 | return _install_platformio_core( 74 | shutdown_piohome=shutdown_piohome, 75 | develop=develop, 76 | ignore_pythons=ignore_pythons, 77 | ) 78 | except subprocess.CalledProcessError as exc: 79 | # Issue #221: Workaround for Windows OS when username contains a space 80 | # https://github.com/platformio/platformio-core-installer/issues/221 81 | if ( 82 | util.IS_WINDOWS 83 | and " " in get_core_dir() 84 | and " " not in get_core_dir(force_to_root=True) 85 | ): 86 | return _install_platformio_core( 87 | shutdown_piohome=shutdown_piohome, 88 | develop=develop, 89 | ignore_pythons=ignore_pythons, 90 | ) 91 | raise exc 92 | 93 | 94 | def _install_platformio_core(shutdown_piohome=True, develop=False, ignore_pythons=None): 95 | # pylint: disable=bad-option-value, import-outside-toplevel, unused-import, import-error, unused-variable, cyclic-import 96 | from pioinstaller import penv 97 | 98 | if shutdown_piohome: 99 | home.shutdown_pio_home_servers() 100 | 101 | penv_dir = penv.create_core_penv(ignore_pythons=ignore_pythons) 102 | python_exe = os.path.join( 103 | penv.get_penv_bin_dir(penv_dir), "python.exe" if util.IS_WINDOWS else "python" 104 | ) 105 | command = [python_exe, "-m", "pip", "install", "-U"] 106 | if develop: 107 | click.echo("Installing a development version of PlatformIO Core") 108 | command.append(PIO_CORE_DEVELOP_URL) 109 | else: 110 | click.echo("Installing PlatformIO Core") 111 | command.append("platformio") 112 | try: 113 | subprocess.check_call(command) 114 | except Exception as e: # pylint:disable=broad-except 115 | error = str(e) 116 | if util.IS_WINDOWS: 117 | error = ( 118 | "If you have antivirus/firewall/defender software in a system," 119 | " try to disable it for a while.\n %s" % error 120 | ) 121 | raise exception.PIOInstallerException( 122 | "Could not install PlatformIO Core: %s" % error 123 | ) 124 | platformio_exe = os.path.join( 125 | penv.get_penv_bin_dir(penv_dir), 126 | "platformio.exe" if util.IS_WINDOWS else "platformio", 127 | ) 128 | 129 | click.secho( 130 | "\nPlatformIO Core has been successfully installed into an isolated environment `%s`!\n" 131 | % penv_dir, 132 | fg="green", 133 | ) 134 | click.secho("The full path to `platformio.exe` is `%s`" % platformio_exe, fg="cyan") 135 | # pylint:disable=line-too-long 136 | click.secho( 137 | """ 138 | If you need an access to `platformio.exe` from other applications, please install Shell Commands 139 | (add PlatformIO Core binary directory `%s` to the system environment PATH variable): 140 | 141 | See https://docs.platformio.org/page/installation.html#install-shell-commands 142 | """ 143 | % penv.get_penv_bin_dir(penv_dir), 144 | fg="cyan", 145 | ) 146 | return True 147 | 148 | 149 | def check(develop=False, global_=False, auto_upgrade=False, version_spec=None): 150 | from pioinstaller import penv 151 | 152 | python_exe = ( 153 | os.path.normpath(sys.executable) 154 | if global_ 155 | else os.path.join( 156 | penv.get_penv_bin_dir(), "python.exe" if util.IS_WINDOWS else "python" 157 | ) 158 | ) 159 | platformio_exe = ( 160 | util.where_is_program("platformio") 161 | if global_ 162 | else os.path.join( 163 | penv.get_penv_bin_dir(), 164 | "platformio.exe" if util.IS_WINDOWS else "platformio", 165 | ) 166 | ) 167 | if not os.path.isfile(platformio_exe): 168 | raise exception.InvalidPlatformIOCore( 169 | "PlatformIO executable not found in `%s`" % penv.get_penv_bin_dir() 170 | ) 171 | try: 172 | subprocess.check_output([platformio_exe, "--help"], stderr=subprocess.STDOUT) 173 | except subprocess.CalledProcessError as e: 174 | error = e.output.decode() 175 | raise exception.InvalidPlatformIOCore( 176 | "Could not run `%s --help`.\nError: %s" % (platformio_exe, str(error)) 177 | ) 178 | 179 | result = {} 180 | try: 181 | result = fetch_python_state(python_exe) 182 | except subprocess.CalledProcessError as e: 183 | error = e.output.decode() 184 | raise exception.InvalidPlatformIOCore( 185 | "Could not import PlatformIO module. Error: %s" % error 186 | ) 187 | piocore_version = convert_version(result.get("core_version")) 188 | develop = develop or bool(piocore_version.prerelease if piocore_version else False) 189 | result.update( 190 | { 191 | "core_dir": get_core_dir(), 192 | "cache_dir": get_cache_dir(), 193 | "penv_dir": penv.get_penv_dir(), 194 | "penv_bin_dir": penv.get_penv_bin_dir(), 195 | "platformio_exe": platformio_exe, 196 | "installer_version": __version__, 197 | "python_exe": python_exe, 198 | "system": util.get_systype(), 199 | "is_develop_core": develop, 200 | } 201 | ) 202 | 203 | if version_spec: 204 | _check_core_version(piocore_version, version_spec) 205 | if not global_: 206 | _check_platform_version() 207 | if auto_upgrade and not global_: 208 | try: 209 | auto_upgrade_core(platformio_exe, develop) 210 | except: # pylint:disable=bare-except 211 | pass 212 | # re-fetch Pyrhon state 213 | try: 214 | result.update(fetch_python_state(python_exe)) 215 | except: # pylint:disable=bare-except 216 | raise exception.InvalidPlatformIOCore("Could not import PlatformIO module") 217 | 218 | return result 219 | 220 | 221 | def _check_core_version(piocore_version, version_spec): 222 | try: 223 | if piocore_version not in semantic_version.Spec(version_spec): 224 | raise exception.InvalidPlatformIOCore( 225 | "PlatformIO Core version %s does not match version requirements %s." 226 | % (str(piocore_version), version_spec) 227 | ) 228 | except ValueError: 229 | click.secho( 230 | "Invalid version requirements format: %s. " 231 | "More about Semantic Versioning: https://semver.org/" % version_spec 232 | ) 233 | 234 | 235 | def _check_platform_version(): 236 | from pioinstaller import penv 237 | 238 | platform_state = penv.load_state().get("platform") 239 | if not platform_state or not isinstance(platform_state, dict): 240 | raise exception.PIOInstallerException("Broken platform state") 241 | if platform_state.get("platform") == platform.platform(terse=True): 242 | return True 243 | release_state = platform_state.get("release") 244 | if ( 245 | release_state 246 | and release_state.split(".")[0] == (platform.release() or "").split(".")[0] 247 | ): 248 | return True 249 | raise exception.InvalidPlatformIOCore( 250 | "PlatformIO Core was installed using another platform `%s`. " 251 | "Your current platform: %s" 252 | % (platform_state.get("platform"), platform.platform(terse=True)) 253 | ) 254 | 255 | 256 | def fetch_python_state(python_exe): 257 | code = """ 258 | import json 259 | import platform 260 | import sys 261 | 262 | import platformio 263 | 264 | if sys.version_info < (3, 6): 265 | raise Exception( 266 | "Unsupported Python version: %s. " 267 | "Minimum supported Python version is 3.6 or above." 268 | % platform.python_version(), 269 | ) 270 | 271 | state = { 272 | "core_version": platformio.__version__, 273 | "python_version": platform.python_version() 274 | } 275 | print(json.dumps(state)) 276 | """ 277 | state = subprocess.check_output( 278 | [ 279 | python_exe, 280 | "-c", 281 | code, 282 | ], 283 | stderr=subprocess.STDOUT, 284 | ) 285 | return json.loads(state.decode()) 286 | 287 | 288 | def convert_version(version): 289 | try: 290 | return semantic_version.Version(util.pepver_to_semver(version)) 291 | except: # pylint:disable=bare-except 292 | return None 293 | 294 | 295 | def auto_upgrade_core(platformio_exe, develop=False): 296 | from pioinstaller import penv 297 | 298 | state = penv.load_state() 299 | time_now = int(round(time.time())) 300 | last_piocore_version_check = state.get("last_piocore_version_check") 301 | if ( 302 | last_piocore_version_check 303 | and (time_now - int(last_piocore_version_check)) < UPDATE_INTERVAL 304 | ): 305 | return None 306 | 307 | state["last_piocore_version_check"] = time_now 308 | penv.save_state(state) 309 | if not last_piocore_version_check: 310 | return None 311 | 312 | command = [platformio_exe, "upgrade"] 313 | if develop: 314 | command.append("--dev") 315 | try: 316 | subprocess.check_output( 317 | command, 318 | stderr=subprocess.PIPE, 319 | ) 320 | return True 321 | except Exception as e: # pylint:disable=broad-except 322 | raise exception.PIOInstallerException( 323 | "Could not upgrade PlatformIO Core: %s" % str(e) 324 | ) 325 | return False 326 | 327 | 328 | def dump_state(target, state): 329 | assert isinstance(target, str) 330 | 331 | if os.path.isdir(target): 332 | target = os.path.join(target, "get-platformio-core-state.json") 333 | if not os.path.isdir(os.path.dirname(target)): 334 | os.makedirs(os.path.dirname(target)) 335 | 336 | with open(target, "w") as fp: 337 | json.dump(state, fp) 338 | --------------------------------------------------------------------------------