Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 176 additions & 4 deletions builder/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -791,13 +791,16 @@ def build_fs_router(target, source, env):

def switch_off_ldf():
"""
Disables LDF (Library Dependency Finder) for uploadfs, uploadfsota, buildfs,
download_fs, and erase targets.
Disables LDF (Library Dependency Finder) for uploadfs, uploadfsota, buildfs,
download_fs, erase, and clang-format targets.

This optimization prevents unnecessary library dependency scanning and compilation
when only filesystem operations are performed.
when only filesystem operations or code formatting is performed.
"""
fs_targets = {"uploadfs", "uploadfsota", "buildfs", "erase", "download_fs"}
fs_targets = {
"uploadfs", "uploadfsota", "buildfs", "erase", "download_fs",
"clangformat", "clangformat-write",
}
if fs_targets & set(COMMAND_LINE_TARGETS):
# Disable LDF by modifying project configuration directly
env_section = "env:" + env["PIOENV"]
Expand Down Expand Up @@ -1820,6 +1823,154 @@ def download_fs_action(target, source, env):
else:
sys.stderr.write("Warning! Unknown upload protocol %s\n" % upload_protocol)


# ---------------------------------------------------------------------------
# clang-format support
# ---------------------------------------------------------------------------

# Maximum number of files per clang-format invocation to avoid OS arg-length limits
_CLANG_FORMAT_BATCH_SIZE = 200


def _clang_format_run(target, source, env, force_mode=None):
"""
Run clang-format on project source files.

Configuration via platformio.ini:
clang_format = check ; only check formatting (dry-run)
clang_format = write ; format files in-place
clang_format_dirs = ; directories to scan (default: src, include)
clang_format_extensions = ; file extensions (default: .c,.cpp,.h,.hpp,.cc,.cxx,.ino)
clang_format_args = ; extra arguments passed to clang-format

A .clang-format file in the project root is automatically respected.

Args:
force_mode: If set, overrides the mode from platformio.ini ("check" or "write")

Returns:
int: 0 on success, non-zero on error
"""
project_dir = Path(get_project_dir())

# Resolve clang-format executable from platform package - install on demand
clang_format_pkg = platform.packages_dir / "tool-clang-format"
if not clang_format_pkg.is_dir():
print("tool-clang-format not found, installing ...")
if not platform.install_tool("tool-clang-format"):
print("Error: tool-clang-format installation failed.")
return 1
if not clang_format_pkg.is_dir():
print("Error: tool-clang-format package directory not found after install.")
return 1

clang_format_bin = clang_format_pkg / "clang-format"
if IS_WINDOWS:
clang_format_bin = clang_format_bin.with_suffix(".exe")

if not clang_format_bin.is_file():
print(f"Error: clang-format binary not found at {clang_format_bin}")
return 1

# Determine mode: forced by target or from platformio.ini
mode = force_mode or env.GetProjectOption("clang_format", "check").strip().lower()

# Directories to scan
scan_dirs_raw = env.GetProjectOption("clang_format_dirs", "")
if scan_dirs_raw:
scan_dirs = [d.strip() for d in scan_dirs_raw.split(",") if d.strip()]
else:
scan_dirs = ["src", "include"]

# File extensions
extensions_raw = env.GetProjectOption("clang_format_extensions", "")
if extensions_raw:
extensions = tuple(
ext.strip() if ext.strip().startswith(".") else f".{ext.strip()}"
for ext in extensions_raw.split(",")
if ext.strip()
)
else:
extensions = (".c", ".cpp", ".h", ".hpp", ".cc", ".cxx", ".ino")

# Collect source files
source_files = []
for scan_dir in scan_dirs:
dir_path = project_dir / scan_dir
if not dir_path.is_dir():
continue
for f in sorted(dir_path.rglob("*")):
if f.is_file() and f.suffix in extensions:
source_files.append(str(f))

if not source_files:
print("No source files found for clang-format.")
return 0

# Build base command
base_cmd = [str(clang_format_bin)]

# Use .clang-format from project root if present
clang_format_file = project_dir / ".clang-format"
if clang_format_file.is_file():
base_cmd.extend(["--style=file:" + str(clang_format_file)])
print(f"Using style from {clang_format_file}")

if mode == "write":
base_cmd.append("-i")
print(f"Formatting {len(source_files)} file(s) in-place ...")
else:
base_cmd.extend(["--dry-run", "--Werror"])
print(f"Checking formatting of {len(source_files)} file(s) ...")

# Extra arguments from platformio.ini
extra_args = env.GetProjectOption("clang_format_args", "")
if extra_args:
base_cmd.extend(shlex.split(extra_args))

# CLI arguments after --
if "--" in sys.argv:
dash_index = sys.argv.index("--")
if dash_index + 1 < len(sys.argv):
base_cmd.extend(sys.argv[dash_index + 1:])

# Process files in batches to avoid OS command-line length limits
try:
for i in range(0, len(source_files), _CLANG_FORMAT_BATCH_SIZE):
batch = source_files[i:i + _CLANG_FORMAT_BATCH_SIZE]
result = subprocess.run(
base_cmd + batch, check=False, capture_output=False # noqa: S603 - developer-controlled args
)
Comment thread
Jason2866 marked this conversation as resolved.
if result.returncode != 0:
if mode != "write":
print("clang-format: formatting issues found (see above).")
else:
print(f"clang-format exited with code {result.returncode}")
return result.returncode
except FileNotFoundError:
print(f"Error: clang-format executable not found: {clang_format_bin}")
return 1
except OSError as e:
print(f"Error running clang-format: {e}")
return 1

if mode == "write":
print("clang-format: all files formatted successfully.")
else:
print("clang-format: all files are correctly formatted.")
return 0


def clang_format_check(target, source, env):
"""clang-format: check only (dry-run)."""
return _clang_format_run(target, source, env, force_mode="check")


def clang_format_write(target, source, env):
"""clang-format: format files in-place."""
return _clang_format_run(target, source, env, force_mode="write")


# Register upload targets
env.AddPlatformTarget("upload", target_firm, upload_actions, "Upload")
env.AddPlatformTarget(
Expand Down Expand Up @@ -1908,6 +2059,27 @@ def download_fs_action(target, source, env):
always_build=True,
)

# Register clang-format targets
env.AddTarget(
name="clangformat",
dependencies=None,
actions=clang_format_check,
title="clang-format (Check)",
description="Check source code formatting with clang-format",
group="Advanced",
always_build=True,
)

env.AddTarget(
name="clangformat-write",
dependencies=None,
actions=clang_format_write,
title="clang-format (Write)",
description="Format source code in-place with clang-format",
group="Advanced",
always_build=True,
)

# Override memory inspection behavior
env.SConscript("sizedata.py", exports="env")

Expand Down
7 changes: 7 additions & 0 deletions platform.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@
"package-version": "21.1.0",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/clangtidy-v21.1.0.zip"
},
"tool-clang-format": {
"type": "tool",
"optional": true,
"owner": "pioarduino",
"package-version": "21.1.0",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/clang-format-v21.1.0.zip"
},
"tool-pvs-studio": {
"type": "tool",
"optional": true,
Expand Down
29 changes: 29 additions & 0 deletions platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@
"tool-pvs-studio"
]

FORMAT_PACKAGES = [
"tool-clang-format"
]

# System-specific configuration
# Set Platformio env var to use windows_amd64 for all windows architectures
# only windows_amd64 native espressif toolchains are available
Expand Down Expand Up @@ -522,6 +526,17 @@ def install_tool(self, tool_name: str) -> bool:
not status['has_tools_json']):
return self._handle_existing_tool(tool_name, paths)

# Case 3: Package not yet downloaded - fetch skeleton then install
if status['has_idf_tools'] and not status['tool_exists']:
version = self.packages.get(tool_name, {}).get("version", "")
if version:
logger.info(f"Downloading {tool_name} ...")
pm.install(version)
# Re-check after download
status = self._check_tool_status(tool_name)
if status['has_tools_json']:
return self._install_with_idf_tools(tool_name, paths, penv_python)

logger.debug(f"Tool {tool_name} already configured")
return True

Expand Down Expand Up @@ -713,6 +728,19 @@ def _configure_rom_elfs_for_exception_decoder(self, variables: Dict) -> None:
logger.info("esp32_exception_decoder filter detected, installing tool-esp-rom-elfs")
self.install_tool("tool-esp-rom-elfs")

def _configure_clang_format(self, variables: Dict) -> None:
"""Configure clang-format tool if enabled in platformio.ini."""
value = variables.get("clang_format", "")
if isinstance(value, bool):
enabled = value
elif isinstance(value, int):
enabled = bool(value)
else:
enabled = str(value).strip().lower() in ("1", "true", "yes", "on", "check", "write")
if enabled:
for package in FORMAT_PACKAGES:
self.install_tool(package)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _configure_check_tools(self, variables: Dict) -> None:
"""Configure static analysis and check tools based on configuration."""
check_tools = variables.get("check_tool", [])
Expand Down Expand Up @@ -774,6 +802,7 @@ def configure_default_packages(self, variables: Dict, targets: List[str]) -> Any
self._install_common_idf_packages()

self._configure_rom_elfs_for_exception_decoder(variables)
self._configure_clang_format(variables)
self._configure_check_tools(variables)
self._handle_dfuutil_tool(variables)

Expand Down