# Copyright (c) 2014-present PlatformIO # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import re import subprocess from hashlib import sha1 from click.testing import CliRunner from platformio import __version__, exception, fs from platformio.compat import IS_MACOS, IS_WINDOWS, hashlib_encode_data from platformio.project.config import ProjectConfig def get_project_dir(): """Get the current project directory. Returns: str: Current working directory path """ return os.getcwd() def is_platformio_project(project_dir=None): """Check if the given directory is a PlatformIO project. Args: project_dir (str, optional): Directory path to check. Defaults to current directory. Returns: bool: True if directory contains platformio.ini file """ if not project_dir: project_dir = get_project_dir() return os.path.isfile(os.path.join(project_dir, "platformio.ini")) def find_project_dir_above(path): """Find PlatformIO project directory by traversing up the directory tree. Args: path (str): Starting path to search from Returns: str or None: Path to project directory if found, None otherwise """ if os.path.isfile(path): path = os.path.dirname(path) if is_platformio_project(path): return path if os.path.isdir(os.path.dirname(path)): return find_project_dir_above(os.path.dirname(path)) return None def get_project_watch_lib_dirs(): """Get library directories that should be watched for changes. Used by platformio-node-helpers.project.observer.fetchLibDirs Returns: list: List of library directory paths """ config = ProjectConfig.get_instance() result = [ config.get("platformio", "globallib_dir"), config.get("platformio", "lib_dir"), ] libdeps_dir = config.get("platformio", "libdeps_dir") if not os.path.isdir(libdeps_dir): return result for d in os.listdir(libdeps_dir): if os.path.isdir(os.path.join(libdeps_dir, d)): result.append(os.path.join(libdeps_dir, d)) return result # Backward compatibility alias get_project_all_lib_dirs = get_project_watch_lib_dirs def get_project_cache_dir(): """Get project cache directory. Deprecated, use ProjectConfig.get("platformio", "cache_dir") instead Returns: str: Cache directory path """ return ProjectConfig.get_instance().get("platformio", "cache_dir") def get_default_projects_dir(): """Get the default directory for PlatformIO projects. Returns: str: Default projects directory path (usually ~/Documents/PlatformIO/Projects) """ docs_dir = os.path.join(fs.expanduser("~"), "Documents") try: assert IS_WINDOWS import ctypes.wintypes # pylint: disable=import-outside-toplevel buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, buf) docs_dir = buf.value except: # pylint: disable=bare-except if not IS_MACOS: try: docs_dir = ( subprocess.check_output(["xdg-user-dir", "DOCUMENTS"]) .decode("utf-8") .strip() ) except FileNotFoundError: # command not found pass return os.path.join(docs_dir, "PlatformIO", "Projects") def compute_project_checksum(config): """Compute project checksum based on configuration and file structure. Args: config (ProjectConfig): Project configuration instance Returns: str: Hexadecimal checksum string """ # rebuild when PIO Core version changes checksum = sha1(hashlib_encode_data(__version__)) # configuration file state config_data = config.to_json() if IS_WINDOWS: # issue #4600: fix drive letter config_data = re.sub( r"([A-Z]):\\", lambda match: "%s:\\" % match.group(1).lower(), config_data, flags=re.I, ) checksum.update(hashlib_encode_data(config_data)) # project file structure check_suffixes = (".c", ".cc", ".cpp", ".h", ".hpp", ".s", ".S") for d in ( config.get("platformio", "include_dir"), config.get("platformio", "src_dir"), config.get("platformio", "lib_dir"), ): if not os.path.isdir(d): continue chunks = [] for root, _, files in os.walk(d): for f in files: path = os.path.join(root, f) if path.endswith(check_suffixes): chunks.append(path) if not chunks: continue chunks_to_str = ",".join(sorted(chunks)) if IS_WINDOWS: # case insensitive OS chunks_to_str = chunks_to_str.lower() checksum.update(hashlib_encode_data(chunks_to_str)) return checksum.hexdigest() def load_build_metadata(project_dir, env_or_envs, cache=False, build_type=None): """Load build metadata for specified environments. Args: project_dir (str): Project directory path env_or_envs (str or list): Environment name(s) to load metadata for cache (bool, optional): Use cached metadata if available. Defaults to False. build_type (str, optional): Build type (release/debug). Defaults to None. Returns: dict or None: Build metadata dictionary or None if not available """ assert env_or_envs env_names = env_or_envs if not isinstance(env_names, list): env_names = [env_names] with fs.cd(project_dir): result = _get_cached_build_metadata(env_names) if cache else {} # incompatible build-type data for env_name in list(result.keys()): if build_type is None: build_type = ProjectConfig.get_instance().get( f"env:{env_name}", "build_type" ) if result[env_name].get("build_type", "") != build_type: del result[env_name] missed_env_names = set(env_names) - set(result.keys()) if missed_env_names: result.update( _load_build_metadata(project_dir, missed_env_names, build_type) ) if not isinstance(env_or_envs, list) and env_or_envs in result: return result[env_or_envs] return result or None # Backward compatibility with dev-platforms load_project_ide_data = load_build_metadata def _load_build_metadata(project_dir, env_names, build_type=None): """Internal function to load build metadata by running idedata target. Args: project_dir (str): Project directory path env_names (list): List of environment names build_type (str, optional): Build type. Defaults to None. Returns: dict: Build metadata dictionary Raises: exception.UserSideException: If build metadata generation fails """ # pylint: disable=import-outside-toplevel from platformio.run.cli import cli as cmd_run args = ["--project-dir", project_dir, "--target", "__idedata"] if build_type == "debug": args.extend(["--target", "__debug"]) # if build_type == "test": # args.extend(["--target", "__test"]) for name in env_names: args.extend(["-e", name]) result = CliRunner().invoke(cmd_run, args) if result.exit_code != 0 and not isinstance( result.exception, exception.ReturnErrorCode ): raise result.exception if '"includes":' not in result.output: raise exception.UserSideException(result.output) return _get_cached_build_metadata(env_names) def _get_cached_build_metadata(env_names): """Get cached build metadata from idedata.json files. Args: env_names (list): List of environment names Returns: dict: Dictionary mapping environment names to their build metadata """ build_dir = ProjectConfig.get_instance().get("platformio", "build_dir") result = {} for env_name in env_names: if not os.path.isfile(os.path.join(build_dir, env_name, "idedata.json")): continue result[env_name] = fs.load_json( os.path.join(build_dir, env_name, "idedata.json") ) return result