Skip to content

Conversation

Jason2866
Copy link
Owner

@Jason2866 Jason2866 commented Aug 31, 2025

Summary by CodeRabbit

  • New Features

    • Checksum-verified runtime downloads with GitHub checksum retrieval, local caching, configurable retries/backoff and network timeouts.
    • Improved Linux musl detection to select correct runtime variants and better cross-platform runtime handling.
  • Bug Fixes

    • More reliable virtual environment creation and installs with timeouts, clearer user-facing error messages, and granular error handling.
    • Safer download/extraction flows, robust state file I/O (UTF-8) and more resilient Python version probing.

Copy link

coderabbitai bot commented Aug 31, 2025

Warning

Rate limit exceeded

@github-actions[bot] has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 15 minutes and 19 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between fa04019 and 6cf2f4b.

📒 Files selected for processing (2)
  • pioinstaller/__init__.py (3 hunks)
  • pioinstaller/penv.py (14 hunks)

Walkthrough

Adds a checksum-verified, retryable uv download/install flow to pioinstaller/penv.py, with GitHub checksum retrieval, musl-aware platform detection, layered download/extract/install steps, new path/executable constants, timeouts, and improved state I/O and error handling.

Changes

Cohort / File(s) Summary of changes
Download & checksum logic
pioinstaller/penv.py
Added DownloadConfig, UV_API_URL, _parse_digest_string, fetch_uv_checksums_from_github, get_expected_checksum, and verify_download to support SHA256 checksum retrieval/parsing and optional verification against GitHub asset checksums.
Download/install orchestration
pioinstaller/penv.py
Added download_and_install_uv(cache_dir, retries=DEFAULT_RETRIES, retry_delay=DEFAULT_RETRY_DELAY), _attempt_download, _prepare_download_dirs, _process_downloaded_archive; introduced DEFAULT_RETRIES/DEFAULT_RETRY_DELAY and retry/backoff control.
Platform, paths & constants
pioinstaller/penv.py
Added detect_musl, PYTHON_EXE, BIN_DIR, UV_EXE; updated platform selection for musl-based Linux and centralized binary/cache path handling.
Robustness, timeouts & subprocess handling
pioinstaller/penv.py
Added timeouts and explicit TimeoutExpired/OSError handling for subprocesses (venv creation, uv install, pip), tightened error handling and rethrowing for unexpected exceptions.
State I/O & probing
pioinstaller/penv.py
load_state/save_state now use UTF-8 with explicit error handling; init_state records Python version with a timeout.
Public API surface
pioinstaller/penv.py
New exported symbols: DownloadConfig, UV_API_URL, DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, PYTHON_EXE, BIN_DIR, UV_EXE, detect_musl, fetch_uv_checksums_from_github, get_expected_checksum, verify_download, and download_and_install_uv (signature updated).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant Penv as penv.download_and_install_uv
  participant Prep as _prepare_download_dirs
  participant GH as GitHub Checksums
  participant Net as Downloader
  participant Ver as verify_download
  participant Proc as _process_downloaded_archive
  participant FS as Filesystem

  Caller->>Penv: download_and_install_uv(cache_dir, retries, retry_delay)
  Penv->>Prep: create temp/cache dirs
  alt checksum available
    Penv->>GH: fetch_uv_checksums_from_github()
    GH-->>Penv: checksums
    Penv->>Penv: get_expected_checksum()
  else no checksum
    Penv->>Penv: proceed without checksum
  end

  loop up to retries
    Penv->>Net: download uv archive (with timeout)
    Net-->>Penv: archive file
    Penv->>Ver: verify_download(archive, checksum?)
    alt verified or no checksum
      Penv->>Proc: extract & install uv
      Proc->>FS: place UV_EXE, chmod
      Proc-->>Penv: path to UV_EXE
      Penv-->>Caller: return UV_EXE
    else checksum mismatch or failure
      Penv->>FS: discard archive
      Note over Penv: wait retry_delay then retry
    end
  end

  alt retries exhausted
    Penv-->>Caller: raise installation error
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I fetched the moon in checksummed light,
Hopped past musl’s alpine night,
With retries set and tail held high,
I cached a star and gave it a try,
Now uv gleams in burrowed bin—🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sha_check_retry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
pioinstaller/penv.py (7)

172-174: Prefer logging.exception inside except blocks.

You’re in an exception handler; include traceback for faster diagnosis.

-        log.error("Failed to verify checksum for %s: %s",
-                  os.path.basename(file_path), e)
+        log.exception("Failed to verify checksum for %s",
+                      os.path.basename(file_path))

100-117: Make musl detection more portable (no hardcoded x86_64 path, avoid partial executable path).

Use shutil.which for ldd and glob for ld-musl across arches.

-def detect_musl():
+def detect_musl():
@@
-    methods = [
-        lambda: b"musl" in subprocess.check_output(
-            ["ldd", "--version"], stderr=subprocess.STDOUT
-        ),
-        lambda: os.path.exists("/lib/libc.musl-x86_64.so.1"),
-        lambda: "musl" in os.environ.get("LD_LIBRARY_PATH", ""),
-    ]
+    ldd = shutil.which("ldd")
+    methods = [
+        lambda: ldd and b"musl" in subprocess.check_output(
+            [ldd, "--version"], stderr=subprocess.STDOUT
+        ),
+        # match any arch, e.g. /lib/ld-musl-aarch64.so.1
+        lambda: any(fn for fn in glob.glob("/lib/ld-musl-*.so.1")),
+        lambda: "musl" in os.environ.get("LD_LIBRARY_PATH", ""),
+    ]

Add imports:

+import glob

(near the other imports)


237-270: Retry flow: don’t delete the archive preemptively; delete only on failure.

Removing the file before every attempt disables util.download_file’s HEAD/cache optimization. Delete it only after a failed verify or exception.

-        # Remove potentially corrupted file from previous attempt
-        if os.path.exists(config.archive_path):
-            os.remove(config.archive_path)
         util.download_file(config.uv_url, config.archive_path)
@@
-                raise exception.PIOInstallerException(
-                    "Downloaded uv archive failed checksum verification"
-                )
+                try:
+                    os.remove(config.archive_path)
+                except OSError:
+                    pass
+                raise exception.PIOInstallerException(
+                    "Downloaded uv archive failed checksum verification"
+                )
@@
-    ) as e:
-        log.debug("Attempt %d/%d failed: %s", attempt, retries, str(e))
+    ) as e:
+        log.debug("Attempt %d/%d failed: %s", attempt, retries, str(e))
+        try:
+            if os.path.exists(config.archive_path):
+                os.remove(config.archive_path)
+        except OSError:
+            pass
         return False

301-309: Surface a clear error when all retries fail.

Either raise or at least log at error level so callers/users get context.

-    # Clean up on final failure
+    # Clean up on final failure
     if os.path.exists(archive_path):
         try:
             os.remove(archive_path)
         except OSError:
             pass
-
-    return None
+    log.error("Failed to download/install uv after %d attempts", retries)
+    return None

386-428: Emit traceback on unexpected venv-creation errors.

Switch to logging.exception in the broad handler.

-    except Exception as e:
-        log.error("Unexpected error creating venv with uv: %s", str(e))
-        raise
+    except Exception:
+        log.exception("Unexpected error creating venv with uv")
+        raise

466-480: Include traceback when probing Python version fails.

Improves diagnosability.

-    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
-        log.error("Failed to get python version: %s", e)
+    except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
+        log.exception("Failed to get python version")
         raise exception.PIOInstallerException(
             "Could not determine python version") from e

504-510: Minor: prefer logging.exception on JSON load errors (optional).

Not required, but consistent with other handlers.

-    except (OSError, json.JSONDecodeError) as e:
-        raise exception.PIOInstallerException(
-            f"Could not load state file: {e}"
-        ) from e
+    except (OSError, json.JSONDecodeError) as e:
+        log.exception("Failed to load state file %s", state_path)
+        raise exception.PIOInstallerException(
+            f"Could not load state file: {e}"
+        ) from e
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d2bf743 and 16cb3be.

📒 Files selected for processing (1)
  • pioinstaller/penv.py (13 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.
📚 Learning: 2025-08-29T14:31:26.756Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:02.793Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:16.386Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T13:36:47.065Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.

Applied to files:

  • pioinstaller/penv.py
🧬 Code graph analysis (1)
pioinstaller/penv.py (3)
pioinstaller/exception.py (1)
  • PIOInstallerException (16-24)
pioinstaller/util.py (2)
  • safe_remove_dir (149-155)
  • download_file (89-113)
pioinstaller/core.py (1)
  • check (224-293)
🪛 Ruff (0.12.2)
pioinstaller/penv.py

104-104: Starting a process with a partial executable path

(S607)


136-136: Consider moving this statement to an else block

(TRY300)


170-170: Consider moving this statement to an else block

(TRY300)


172-173: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


185-186: Avoid specifying long messages outside the exception class

(TRY003)


194-195: Avoid specifying long messages outside the exception class

(TRY003)


224-226: Avoid specifying long messages outside the exception class

(TRY003)


231-231: os.chmod setting a permissive mask 0o755 on file or directory

(S103)


253-255: Abstract raise to an inner function

(TRY301)


253-255: Avoid specifying long messages outside the exception class

(TRY003)


259-259: Consider moving this statement to an else block

(TRY300)


277-280: Avoid specifying long messages outside the exception class

(TRY003)


426-426: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


452-453: Avoid specifying long messages outside the exception class

(TRY003)


456-457: Avoid specifying long messages outside the exception class

(TRY003)


468-468: subprocess call: check for execution of untrusted input

(S603)


477-477: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


478-479: Avoid specifying long messages outside the exception class

(TRY003)


508-510: Avoid specifying long messages outside the exception class

(TRY003)


520-520: Consider moving this statement to an else block

(TRY300)


522-524: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build (macos-latest, 3.9)
  • GitHub Check: build (macos-13, 3.13)
  • GitHub Check: build (macos-13, 3.10)
  • GitHub Check: build (macos-13, 3.9)
🔇 Additional comments (4)
pioinstaller/penv.py (4)

94-97: LGTM: musl-aware platform selection.

Replacing -gnu with -musl when detected is correct and matches uv asset names. (docs.wakemeops.com)


177-197: LGTM: basic traversal guard retained.

Path prefix checks for zip/tar are fine for this trusted upstream and align with accepted project trade-offs.


430-458: LGTM: install uv (PyPI package) into the venv via system uv.

Matches the project’s intent to keep the venv self-contained without depending on the bootstrap CLI.


34-46: LGTM: cohesive platform constants and penv plumbing.

  • PYTHON_EXE/BIN_DIR/UV_EXE simplify cross-platform path handling.
  • get_penv_bin_dir changes align with core.py usage.
  • get_uv_executable correctly prefers PATH, then cache, then download.
  • Download/extract/placement helpers are clean and predictable.

Also applies to: 343-349, 379-383, 311-333, 208-214, 216-235

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
pioinstaller/penv.py (1)

181-201: Normalize checksum comparison to accept “sha256:” and case differences

Prevents false mismatches when the expected digest includes the algorithm prefix or different casing.

-        calculated_hash = sha256_hash.hexdigest()
-        is_valid = calculated_hash == expected_sha256
+        calculated_hash = sha256_hash.hexdigest().lower()
+        expected = (expected_sha256 or "").strip().lower()
+        if ":" in expected:
+            expected = expected.split(":", 1)[1]
+        is_valid = calculated_hash == expected
🧹 Nitpick comments (4)
pioinstaller/penv.py (4)

119-137: Return lowercased digest from parser to keep comparisons consistent

Minor normalization; complements the verify change.

-    return hash_value.strip()
+    return hash_value.strip().lower()

102-108: Avoid invoking a bare 'ldd' path; gate on availability

Prevents S607 and avoids needless exceptions on systems without ldd.

-def detect_musl():
+def detect_musl():
     """Detect musl libc more robustly."""
-    methods = [
-        lambda: b"musl" in subprocess.check_output(
-            ["ldd", "--version"], stderr=subprocess.STDOUT
-        ),
+    ldd_path = shutil.which("ldd")
+    methods = [
+        lambda: bool(ldd_path)
+        and b"musl" in subprocess.check_output([ldd_path, "--version"], stderr=subprocess.STDOUT),
         lambda: os.path.exists("/lib/libc.musl-x86_64.so.1"),
         lambda: "musl" in os.environ.get("LD_LIBRARY_PATH", ""),
     ]
 
-    for method in methods:
+    for method in methods:
         try:
             if method():
                 return True
         except (subprocess.CalledProcessError, FileNotFoundError, OSError):
             continue
     return False

Also applies to: 110-115


203-205: Prefer logging.exception for caught exceptions; keep messages in exception types

Small logging/style polish aligning with Ruff TRY400/TRY003.

-        log.error("Failed to verify checksum for %s: %s",
-                  os.path.basename(file_path), e)
+        log.exception("Failed to verify checksum for %s", os.path.basename(file_path))
-        log.error("Unexpected error creating venv with uv: %s", str(e))
+        log.exception("Unexpected error creating venv with uv")
-        log.error("Failed to get python version: %s", e)
-        raise exception.PIOInstallerException(
-            "Could not determine python version") from e
+        log.exception("Failed to get python version")
+        raise exception.PIOInstallerException("Could not determine python version") from e
-    except (OSError, json.JSONDecodeError) as e:
-        raise exception.PIOInstallerException(
-            f"Could not load state file: {e}"
-        ) from e
+    except (OSError, json.JSONDecodeError) as e:
+        raise exception.PIOInstallerException(f"Could not load state file: {e}") from e
-    except OSError as e:
-        raise exception.PIOInstallerException(
-            f"Could not save state file: {e}"
-        ) from e
+    except OSError as e:
+        raise exception.PIOInstallerException(f"Could not save state file: {e}") from e

Also applies to: 457-458, 508-511, 539-541, 553-555


426-433: Optional: capture uv output on failure for easier diagnostics

Right now all output is suppressed; capturing stderr on failure would speed up triage.

-        subprocess.run(
-            cmd,
-            check=True,
-            timeout=300,  # 5 minutes timeout
-            stdout=subprocess.DEVNULL,
-            stderr=subprocess.STDOUT,
-        )
+        proc = subprocess.run(
+            cmd,
+            check=False,
+            timeout=300,  # 5 minutes timeout
+            capture_output=True,
+            text=True,
+        )
+        if proc.returncode != 0:
+            log.debug("uv venv failed (rc=%s): %s", proc.returncode, proc.stderr or proc.stdout)
+            proc.check_returncode()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 16cb3be and 73a4d3b.

📒 Files selected for processing (1)
  • pioinstaller/penv.py (13 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.
📚 Learning: 2025-08-29T14:31:26.756Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:16.386Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:02.793Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T13:36:47.065Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.

Applied to files:

  • pioinstaller/penv.py
🧬 Code graph analysis (1)
pioinstaller/penv.py (4)
pioinstaller/exception.py (1)
  • PIOInstallerException (16-24)
pioinstaller/util.py (2)
  • safe_remove_dir (149-155)
  • download_file (89-113)
pioinstaller/core.py (1)
  • check (224-293)
pioinstaller/python.py (1)
  • check (126-169)
🪛 Ruff (0.12.2)
pioinstaller/penv.py

104-104: Starting a process with a partial executable path

(S607)


166-166: Consider moving this statement to an else block

(TRY300)


201-201: Consider moving this statement to an else block

(TRY300)


203-204: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


216-217: Avoid specifying long messages outside the exception class

(TRY003)


225-226: Avoid specifying long messages outside the exception class

(TRY003)


255-257: Avoid specifying long messages outside the exception class

(TRY003)


262-262: os.chmod setting a permissive mask 0o755 on file or directory

(S103)


284-286: Abstract raise to an inner function

(TRY301)


284-286: Avoid specifying long messages outside the exception class

(TRY003)


290-290: Consider moving this statement to an else block

(TRY300)


308-311: Avoid specifying long messages outside the exception class

(TRY003)


457-457: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


483-484: Avoid specifying long messages outside the exception class

(TRY003)


487-488: Avoid specifying long messages outside the exception class

(TRY003)


499-499: subprocess call: check for execution of untrusted input

(S603)


508-508: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


509-510: Avoid specifying long messages outside the exception class

(TRY003)


539-541: Avoid specifying long messages outside the exception class

(TRY003)


551-551: Consider moving this statement to an else block

(TRY300)


553-555: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (1)
pioinstaller/penv.py (1)

139-171: Unify UV asset URL and checksum retrieval in one API call
Fetch both browser_download_url and SHA-256 digest from the same release snapshot to eliminate release-flip races. Replace fetch_uv_checksums_from_github() with fetch_uv_release_assets() (returning {"checksum", "url"}), update get_expected_checksum() and the DownloadConfig instantiation (with fallback) as in the suggested diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
pioinstaller/penv.py (1)

183-204: Normalize checksum comparison to avoid false mismatches.

Lowercase both sides and accept optional “sha256:” prefix.

-        calculated_hash = sha256_hash.hexdigest()
-        is_valid = calculated_hash == expected_sha256
+        calculated_hash = sha256_hash.hexdigest().lower()
+        expected = (expected_sha256 or "").strip().lower()
+        if ":" in expected:
+            expected = expected.split(":", 1)[1]
+        is_valid = calculated_hash == expected
         if not is_valid:
             log.error("Checksum mismatch for %s: expected %s, got %s",
-                      os.path.basename(file_path), expected_sha256,
+                      os.path.basename(file_path), expected,
                       calculated_hash)
🧹 Nitpick comments (6)
pioinstaller/penv.py (6)

102-119: Broaden musl detection for non-x86_64 and Alpine.

Current checks miss common cases like /etc/alpine-release and ld-musl-* on non-x86_64. Add a couple of cheap probes.

 def detect_musl():
@@
-    methods = [
+    methods = [
         lambda: b"musl" in subprocess.check_output(
             ["ldd", "--version"], stderr=subprocess.STDOUT
         ),
-        lambda: os.path.exists("/lib/libc.musl-x86_64.so.1"),
+        # common across arches: ld-musl-<arch>.so.1
+        lambda: any(
+            f.startswith("ld-musl-") and f.endswith(".so.1")
+            for f in os.listdir("/lib")
+        ),
+        # x86_64 libc path (kept for backward compatibility)
+        lambda: os.path.exists("/lib/libc.musl-x86_64.so.1"),
+        # Alpine indicator
+        lambda: os.path.exists("/etc/alpine-release"),
         lambda: "musl" in os.environ.get("LD_LIBRARY_PATH", ""),
     ]

249-268: Clean up extraction directory after installing uv.

Leaving uv-extract around grows the cache over time.

     uv_dest = os.path.join(cache_dir, UV_EXE)
     shutil.copy2(uv_binary, uv_dest)
     if not util.IS_WINDOWS:
         os.chmod(uv_dest, 0o755)
 
     log.debug("uv installed at %s", uv_dest)
+    # best-effort cleanup
+    util.safe_remove_dir(extract_dir)
     return uv_dest

270-303: Fail closed when checksum is unavailable (env-guarded).

Given the PR’s goal (“with SHA check”), proceeding unverified by default weakens the guarantee. Recommend failing by default with an override.

-        if config.is_checksum_available():
-            if not verify_download(config.archive_path,
-                                 config.expected_checksum):
-                raise exception.PIOInstallerException(
-                    "Downloaded uv archive failed checksum verification"
-                )
-        else:
-            log.warning("No checksum available for verification")
+        if config.is_checksum_available():
+            if not verify_download(config.archive_path, config.expected_checksum):
+                raise exception.PIOInstallerException(
+                    "Downloaded uv archive failed checksum verification"
+                )
+        else:
+            if os.environ.get("PLATFORMIO_UV_ALLOW_UNVERIFIED", "").lower() not in (
+                "1",
+                "true",
+                "yes",
+            ):
+                raise exception.PIOInstallerException(
+                    "No checksum available for verification; set PLATFORMIO_UV_ALLOW_UNVERIFIED=1 to proceed unverified"
+                )
+            log.warning(
+                "No checksum available for verification; proceeding unverified due to PLATFORMIO_UV_ALLOW_UNVERIFIED"
+            )

315-317: Allow pinning a specific uv tag for reproducibility.

Support an env override to avoid “latest” drift, useful for CI and rollbacks.

-    uv_url = UV_URL.format(platform=uv_platform, ext=ext)
+    tag = os.environ.get("PLATFORMIO_UV_TAG")
+    if tag:
+        uv_url = f"https://github.com/astral-sh/uv/releases/download/{tag}/uv-{uv_platform}.{ext}"
+    else:
+        uv_url = UV_URL.format(platform=uv_platform, ext=ext)

430-435: Capture subprocess output and use absolute path for uv.

Improves debuggability and satisfies the partial-path lint.

-        cmd = [uv_exe, "venv", "--python", python_exe, penv_dir]
+        uv_exe = os.path.abspath(uv_exe)
+        cmd = [uv_exe, "venv", "--python", python_exe, penv_dir]
         subprocess.run(
             cmd,
             check=True,
-            timeout=300,  # 5 minutes timeout
-            stdout=subprocess.DEVNULL,
-            stderr=subprocess.STDOUT,
+            timeout=300,  # 5 minutes timeout
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            text=True,
         )
@@
-    except subprocess.CalledProcessError as e:
-        log.debug("Failed to create venv with uv: %s", str(e))
+    except subprocess.CalledProcessError as e:
+        log.debug("Failed to create venv with uv: %s\n%s", str(e), e.stdout or "")
         return None

Also applies to: 450-461


510-514: Prefer log.exception to preserve traceback.

Keeps context when python version probing fails.

-    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
-        log.error("Failed to get python version: %s", e)
+    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
+        log.exception("Failed to get python version: %s", e)
         raise exception.PIOInstallerException(
             "Could not determine python version") from e
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 73a4d3b and b651ce4.

📒 Files selected for processing (1)
  • pioinstaller/penv.py (13 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.
📚 Learning: 2025-08-29T14:31:26.756Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:02.793Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:16.386Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T13:36:47.065Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.

Applied to files:

  • pioinstaller/penv.py
🧬 Code graph analysis (1)
pioinstaller/penv.py (3)
pioinstaller/exception.py (1)
  • PIOInstallerException (16-24)
pioinstaller/util.py (2)
  • safe_remove_dir (149-155)
  • download_file (89-113)
pioinstaller/core.py (1)
  • check (224-293)
🪛 Ruff (0.12.2)
pioinstaller/penv.py

106-106: Starting a process with a partial executable path

(S607)


168-168: Consider moving this statement to an else block

(TRY300)


203-203: Consider moving this statement to an else block

(TRY300)


204-204: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


218-219: Avoid specifying long messages outside the exception class

(TRY003)


227-228: Avoid specifying long messages outside the exception class

(TRY003)


257-259: Avoid specifying long messages outside the exception class

(TRY003)


264-264: os.chmod setting a permissive mask 0o755 on file or directory

(S103)


286-288: Abstract raise to an inner function

(TRY301)


286-288: Avoid specifying long messages outside the exception class

(TRY003)


292-292: Consider moving this statement to an else block

(TRY300)


310-313: Avoid specifying long messages outside the exception class

(TRY003)


460-460: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


486-487: Avoid specifying long messages outside the exception class

(TRY003)


490-491: Avoid specifying long messages outside the exception class

(TRY003)


502-502: subprocess call: check for execution of untrusted input

(S603)


511-511: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


512-513: Avoid specifying long messages outside the exception class

(TRY003)


542-544: Avoid specifying long messages outside the exception class

(TRY003)


554-554: Consider moving this statement to an else block

(TRY300)


556-558: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build (macos-latest, 3.13)
  • GitHub Check: build (macos-latest, 3.9)
  • GitHub Check: build (macos-latest, 3.10)
  • GitHub Check: build (macos-13, 3.13)
🔇 Additional comments (3)
pioinstaller/penv.py (3)

94-99: Musl remapping for ARMv7 fixed — good catch.

The additional -gnueabihfmusleabihf mapping closes a real gap on Alpine armv7. Looks good.


141-169: Digest fetch + headers implemented correctly.

Using the GitHub API headers and parsing string digests addresses the prior breakage. Nice.


478-491: Good: bounded install with timeout + clear error path.

Installing Python “uv” inside the venv via the system uv is consistent with previous decisions and keeps the venv self-contained.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
pioinstaller/penv.py (2)

94-99: Musl mapping fix is correct (adds gnueabihf → musleabihf).

This resolves Alpine ARMv7 targeting issues.


183-210: Log normalized expected digest to avoid confusion.

Comparison uses normalized value but error logs print the raw input.

Apply:

-        if not is_valid:
-            log.error("Checksum mismatch for %s: expected %s, got %s",
-                      os.path.basename(file_path), expected_sha256,
-                      calculated_hash)
-        else:
-            log.debug("Checksum verified for %s", os.path.basename(file_path))
+        if not is_valid:
+            log.error(
+                "Checksum mismatch for %s: expected %s, got %s",
+                os.path.basename(file_path),
+                expected,
+                calculated_hash,
+            )
+        else:
+            log.debug("Checksum verified for %s", os.path.basename(file_path))
🧹 Nitpick comments (7)
pioinstaller/penv.py (7)

48-66: Consider a dataclass for DownloadConfig.

Cuts boilerplate, makes intent clearer, and adds repr/eq.

Apply:

+from dataclasses import dataclass
...
-class DownloadConfig:
-    """Configuration class for download parameters."""
-
-    def __init__(self, uv_url, archive_path, uv_platform, ext):
-        self.uv_url = uv_url
-        self.archive_path = archive_path
-        self.uv_platform = uv_platform
-        self.ext = ext
-        self.expected_checksum = None
+@dataclass
+class DownloadConfig:
+    """Configuration class for download parameters."""
+    uv_url: str
+    archive_path: str
+    uv_platform: str
+    ext: str
+    expected_checksum: str | None = None

102-119: Broaden musl detection, reduce arch specificity.

Checking only x86_64’s libc path may miss other arches; glob the musl soname instead.

Apply:

 def detect_musl():
@@
-    methods = [
+    methods = [
         lambda: bool(ldd_path)
         and b"musl" in subprocess.check_output([ldd_path, "--version"], stderr=subprocess.STDOUT),
-        lambda: os.path.exists("/lib/libc.musl-x86_64.so.1"),
+        lambda: any(
+            os.path.exists(p)
+            for p in (
+                "/lib/libc.musl-x86_64.so.1",
+                "/lib/ld-musl-x86_64.so.1",
+                "/lib/ld-musl-aarch64.so.1",
+                "/lib/ld-musl-armhf.so.1",
+                "/lib/ld-musl-arm.so.1",
+            )
+        ),
         lambda: "musl" in os.environ.get("LD_LIBRARY_PATH", ""),
     ]

252-271: Clean up temp files/dirs after successful install.

Avoids cache bloat; wrap with try/finally or remove after copy.

Apply:

 def _process_downloaded_archive(archive_path, extract_dir, cache_dir):
     """Process downloaded archive: extract and install binary."""
-    util.safe_remove_dir(extract_dir)
-    os.makedirs(extract_dir, exist_ok=True)
-    _extract_uv_archive(archive_path, extract_dir)
+    util.safe_remove_dir(extract_dir)
+    os.makedirs(extract_dir, exist_ok=True)
+    _extract_uv_archive(archive_path, extract_dir)
@@
-    log.debug("uv installed at %s", uv_dest)
-    return uv_dest
+    log.debug("uv installed at %s", uv_dest)
+    # Best-effort cleanup
+    try:
+        util.safe_remove_dir(extract_dir)
+        if os.path.exists(archive_path):
+            os.remove(archive_path)
+    except OSError:
+        pass
+    return uv_dest

273-306: Narrow exception scope in _attempt_download.

tar/zip errors can’t occur here (only download + hash). Keep it focused.

Apply:

-    except (
-        requests.RequestException,
-        tarfile.TarError,
-        zipfile.BadZipFile,
-        OSError,
-        exception.PIOInstallerException,
-    ) as e:
+    except (requests.RequestException, OSError, exception.PIOInstallerException) as e:

308-346: Optional: add jitter to backoff.

Reduces synchronized retries when many instances run concurrently.

Apply:

-                actual_delay = retry_delay * (2 ** (attempt - 1))
+                import random
+                base = retry_delay * (2 ** (attempt - 1))
+                actual_delay = base + random.uniform(0, base * 0.2)
-                log.debug("Retrying in %d seconds...", actual_delay)
+                log.debug("Retrying in %.1f seconds...", actual_delay)

441-451: Target the venv explicitly when installing the Python ‘uv’ package.

Pass the venv’s Python to “uv pip” for determinism; VIRTUAL_ENV alone can be flaky.

Apply:

-        expected_python = os.path.join(get_penv_bin_dir(penv_dir), PYTHON_EXE)
+        expected_python = os.path.join(get_penv_bin_dir(penv_dir), PYTHON_EXE)
@@
-    cmd = [system_uv_exe, "pip", "install", "uv"]
+    venv_python = os.path.join(get_penv_bin_dir(penv_dir), PYTHON_EXE)
+    cmd = [system_uv_exe, "pip", "install", "--python", venv_python, "uv"]

Also applies to: 475-485


534-547: Nit: error message grammar.

Consider “Could not find state.json file…” on Line 539.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b651ce4 and 9c57f28.

📒 Files selected for processing (1)
  • pioinstaller/penv.py (13 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.
📚 Learning: 2025-08-29T14:31:26.756Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:16.386Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T13:36:47.065Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.

Applied to files:

  • pioinstaller/penv.py
🧬 Code graph analysis (1)
pioinstaller/penv.py (4)
pioinstaller/exception.py (1)
  • PIOInstallerException (16-24)
pioinstaller/util.py (2)
  • safe_remove_dir (149-155)
  • download_file (89-113)
pioinstaller/core.py (1)
  • check (224-293)
pioinstaller/python.py (1)
  • check (126-169)
🪛 Ruff (0.12.2)
pioinstaller/penv.py

107-107: subprocess call: check for execution of untrusted input

(S603)


168-168: Consider moving this statement to an else block

(TRY300)


206-206: Consider moving this statement to an else block

(TRY300)


221-222: Avoid specifying long messages outside the exception class

(TRY003)


230-231: Avoid specifying long messages outside the exception class

(TRY003)


260-262: Avoid specifying long messages outside the exception class

(TRY003)


267-267: os.chmod setting a permissive mask 0o755 on file or directory

(S103)


289-291: Abstract raise to an inner function

(TRY301)


289-291: Avoid specifying long messages outside the exception class

(TRY003)


295-295: Consider moving this statement to an else block

(TRY300)


313-316: Avoid specifying long messages outside the exception class

(TRY003)


489-490: Avoid specifying long messages outside the exception class

(TRY003)


493-494: Avoid specifying long messages outside the exception class

(TRY003)


505-505: subprocess call: check for execution of untrusted input

(S603)


515-516: Avoid specifying long messages outside the exception class

(TRY003)


545-547: Avoid specifying long messages outside the exception class

(TRY003)


557-557: Consider moving this statement to an else block

(TRY300)


559-561: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (7)
pioinstaller/penv.py (7)

34-37: Good: Centralized URLs for uv binary and API.

Stable, readable, and easy to maintain.


38-46: Good: Sensible defaults and cross-platform constants.

Retry defaults and BIN/PYTHON/UV_EXE constants look correct.


121-139: Checksum string parsing looks solid.

Handles prefix, casing, and whitespace correctly.


141-174: GitHub API usage and asset digest handling LGTM.

Headers and digest parsing are correct; logging is helpful.


213-233: Archive extraction path checks are acceptable for this project.

Matches prior agreement to keep traversal checks basic for trusted uv source.


356-363: Good: reuse cached uv when present.

Simple and fast path with executable check.


550-562: Good: state I/O with UTF-8 and exception wrapping.

Clear errors and stable encoding.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
pioinstaller/penv.py (2)

121-139: Digest string parser is correct and robust.

Handles "sha256:", trims/normalizes case, and ignores non-sha256. Matches GitHub’s format.


141-169: GitHub API call + headers and checksum harvesting are implemented correctly.

Using Accept + API-Version + UA, iterating assets, and capturing digests only for archive assets is aligned with the new API behavior.

🧹 Nitpick comments (7)
pioinstaller/penv.py (7)

190-206: Minor perf: increase hash read chunk size.

Larger chunks speed up hashing of multi‑MB archives without changing behavior.

Apply:

-        with open(file_path, "rb") as f:
-            for chunk in iter(lambda: f.read(4096), b""):
+        with open(file_path, "rb") as f:
+            for chunk in iter(lambda: f.read(1024 * 1024), b""):
                 sha256_hash.update(chunk)

221-223: Include entry name in “Unsafe path in archive” errors.

A tiny context boost for user diagnostics; doesn’t alter behavior.

Apply:

-                    raise exception.PIOInstallerException(
-                        "Unsafe path in archive")
+                    raise exception.PIOInstallerException(
+                        f"Unsafe path in archive entry: {m.filename}")
-                    raise exception.PIOInstallerException(
-                        "Unsafe path in archive")
+                    raise exception.PIOInstallerException(
+                        f"Unsafe path in archive entry: {m.name}")

Also applies to: 230-232


264-270: Make install atomic and ensure cache_dir exists.

Avoids partial updates if multiple installers race or the process is interrupted mid‑copy.

Apply:

-    uv_dest = os.path.join(cache_dir, UV_EXE)
-    shutil.copy2(uv_binary, uv_dest)
-    if not util.IS_WINDOWS:
-        os.chmod(uv_dest, 0o755)
+    uv_dest = os.path.join(cache_dir, UV_EXE)
+    os.makedirs(cache_dir, exist_ok=True)
+    uv_dest_tmp = uv_dest + ".tmp"
+    shutil.copy2(uv_binary, uv_dest_tmp)
+    if not util.IS_WINDOWS:
+        os.chmod(uv_dest_tmp, 0o755)
+    os.replace(uv_dest_tmp, uv_dest)

286-295: Reduce log noise when checksums are unavailable.

Warn once; subsequent retries can be debug level.

Apply:

-        else:
-            log.warning("No checksum available for verification")
+        else:
+            if attempt == 1:
+                log.warning("No checksum available for verification")
+            else:
+                log.debug("No checksum available for verification")

344-345: Actionable failure message on final retry.

Give users clear next steps when download fails.

Apply:

-    log.error("Failed to download/install uv after %d attempts", retries)
+    log.error(
+        "Failed to download/install uv after %d attempts. "
+        "Check network/proxy, GitHub availability, or install uv manually "
+        "and ensure it is on PATH.",
+        retries,
+    )

48-65: Use a dataclass for DownloadConfig.

Removes boilerplate and adds lightweight type hints.

Apply:

-class DownloadConfig:
-    """Configuration class for download parameters."""
-
-    def __init__(self, uv_url, archive_path, uv_platform, ext):
-        self.uv_url = uv_url
-        self.archive_path = archive_path
-        self.uv_platform = uv_platform
-        self.ext = ext
-        self.expected_checksum = None
-
-    def set_checksum(self, checksum):
-        """Set the expected checksum for verification."""
-        self.expected_checksum = checksum
-
-    def is_checksum_available(self):
-        """Check if checksum is available for verification."""
-        return self.expected_checksum is not None
+from dataclasses import dataclass
+
+@dataclass
+class DownloadConfig:
+    """Configuration for uv download."""
+    uv_url: str
+    archive_path: str
+    uv_platform: str
+    ext: str
+    expected_checksum: str | None = None
+
+    def set_checksum(self, checksum: str | None) -> None:
+        self.expected_checksum = checksum
+
+    def is_checksum_available(self) -> bool:
+        return self.expected_checksum is not None

Note: add the import if not already present.


538-540: Fix grammar in error message.

Tiny polish for user-facing text.

Apply:

-        raise exception.PIOInstallerException(
-            "Could not found state.json file in `%s`" % state_path
-        )
+        raise exception.PIOInstallerException(
+            "Could not find state.json file at `%s`" % state_path
+        )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9c57f28 and 76df5ea.

📒 Files selected for processing (1)
  • pioinstaller/penv.py (13 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.
📚 Learning: 2025-08-29T14:31:26.756Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:16.386Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:02.793Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T13:36:47.065Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.

Applied to files:

  • pioinstaller/penv.py
🧬 Code graph analysis (1)
pioinstaller/penv.py (4)
pioinstaller/exception.py (1)
  • PIOInstallerException (16-24)
pioinstaller/util.py (2)
  • safe_remove_dir (149-155)
  • download_file (89-113)
pioinstaller/core.py (1)
  • check (224-293)
pioinstaller/python.py (1)
  • check (126-169)
🪛 Ruff (0.12.2)
pioinstaller/penv.py

107-107: subprocess call: check for execution of untrusted input

(S603)


168-168: Consider moving this statement to an else block

(TRY300)


206-206: Consider moving this statement to an else block

(TRY300)


221-222: Avoid specifying long messages outside the exception class

(TRY003)


230-231: Avoid specifying long messages outside the exception class

(TRY003)


260-262: Avoid specifying long messages outside the exception class

(TRY003)


267-267: os.chmod setting a permissive mask 0o755 on file or directory

(S103)


289-291: Abstract raise to an inner function

(TRY301)


289-291: Avoid specifying long messages outside the exception class

(TRY003)


295-295: Consider moving this statement to an else block

(TRY300)


313-316: Avoid specifying long messages outside the exception class

(TRY003)


489-490: Avoid specifying long messages outside the exception class

(TRY003)


493-494: Avoid specifying long messages outside the exception class

(TRY003)


505-505: subprocess call: check for execution of untrusted input

(S603)


515-516: Avoid specifying long messages outside the exception class

(TRY003)


545-547: Avoid specifying long messages outside the exception class

(TRY003)


557-557: Consider moving this statement to an else block

(TRY300)


559-561: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (2)
pioinstaller/penv.py (2)

94-99: Musl platform mapping fix looks good.

Covers both gnu and gnueabihf → musl variants. This should pick correct assets on Alpine x86_64 and ARMv7.


423-461: Venv creation flow with timeouts/readability is solid.

Timeouts and explicit logging around subprocess calls are appropriate; fallback across candidate Pythons is clear.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
pioinstaller/penv.py (1)

141-174: Digest parsing: also accept dict shape; keep connect/read timeout split

GitHub has returned both str and dict digests in the wild. Handle both to future‑proof. Minor: use (connect, read) timeouts.

-        response = requests.get(UV_API_URL, headers=headers, timeout=30)
+        response = requests.get(UV_API_URL, headers=headers, timeout=(5, 30))
@@
-            if asset_name.endswith((".tar.gz", ".zip")):
-                # GitHub digest is a string like "sha256:abc123..."
-                digest_str = asset.get("digest")
-                digest_hash = _parse_digest_string(digest_str)
-                if digest_hash:
-                    checksums[asset_name] = digest_hash
+            if asset_name.endswith((".tar.gz", ".zip")):
+                digest = asset.get("digest")
+                digest_hash = None
+                if isinstance(digest, str):
+                    digest_hash = _parse_digest_string(digest)
+                elif isinstance(digest, dict):
+                    val = digest.get("sha256")
+                    if val:
+                        digest_hash = val.strip().lower()
+                if digest_hash:
+                    checksums[asset_name] = digest_hash
🧹 Nitpick comments (7)
pioinstaller/penv.py (7)

48-61: Use a dataclass for DownloadConfig to cut boilerplate and clarify intent

Simplifies init, typing, and default handling.

Apply:

+from dataclasses import dataclass
@@
-class DownloadConfig:
-    """Configuration class for download parameters."""
-
-    def __init__(self, uv_url, archive_path, uv_platform, ext):
-        self.uv_url = uv_url
-        self.archive_path = archive_path
-        self.uv_platform = uv_platform
-        self.ext = ext
-        self.expected_checksum = None
-
-    def set_checksum(self, checksum):
-        """Set the expected checksum for verification."""
-        self.expected_checksum = checksum
-
-    def is_checksum_available(self):
-        """Check if checksum is available for verification."""
-        return self.expected_checksum is not None
+@dataclass
+class DownloadConfig:
+    """Configuration class for download parameters."""
+    uv_url: str
+    archive_path: str
+    uv_platform: str
+    ext: str
+    expected_checksum: str | None = None
+
+    def set_checksum(self, checksum: str | None) -> None:
+        self.expected_checksum = checksum
+
+    def is_checksum_available(self) -> bool:
+        return self.expected_checksum is not None

Add the import at the top of the file:

from dataclasses import dataclass

102-119: Harden musl detection with an Alpine heuristic

BusyBox ldd and minimal images can evade current checks. Add a cheap Alpine fingerprint.

 def detect_musl():
@@
-    methods = [
+    methods = [
         lambda: bool(ldd_path)
         and b"musl" in subprocess.check_output([ldd_path, "--version"], stderr=subprocess.STDOUT),
         lambda: os.path.exists("/lib/libc.musl-x86_64.so.1"),
         lambda: "musl" in os.environ.get("LD_LIBRARY_PATH", ""),
+        lambda: os.path.exists("/etc/alpine-release"),
     ]

221-223: Include the offending member name in the “Unsafe path in archive” error

Helps diagnostics without changing behavior.

-                    raise exception.PIOInstallerException(
-                        "Unsafe path in archive")
+                    raise exception.PIOInstallerException(
+                        f"Unsafe path in archive: {m.filename}")
@@
-                    raise exception.PIOInstallerException(
-                        "Unsafe path in archive")
+                    raise exception.PIOInstallerException(
+                        f"Unsafe path in archive: {m.name}")

Also applies to: 230-231


252-271: Clean up temp extract dir (and optionally the archive) on success

Prevents cache/tmp bloat across runs.

     uv_dest = os.path.join(cache_dir, UV_EXE)
     shutil.copy2(uv_binary, uv_dest)
     if not util.IS_WINDOWS:
         os.chmod(uv_dest, 0o755)
 
     log.debug("uv installed at %s", uv_dest)
-    return uv_dest
+    # Best-effort cleanup
+    try:
+        util.safe_remove_dir(extract_dir)
+        os.remove(archive_path)
+    except OSError:
+        log.debug("Cleanup of temp files failed: %s, %s", extract_dir, archive_path)
+    return uv_dest

283-294: Skip HEAD when we always force a re-download in retries

You delete the file before downloading; disabling cache avoids the extra HEAD round‑trip per attempt.

-        util.download_file(config.uv_url, config.archive_path)
+        util.download_file(config.uv_url, config.archive_path, cache=False)

431-436: Capture uv venv output for better diagnostics on failure (still silent on success)

Currently errors lose stderr/stdout details. Capture them and emit on exception.

-        subprocess.run(
-            cmd,
-            check=True,
-            timeout=300,  # 5 minutes timeout
-            stdout=subprocess.DEVNULL,
-            stderr=subprocess.STDOUT,
-        )
+        subprocess.run(
+            cmd,
+            check=True,
+            timeout=300,  # 5 minutes timeout
+            capture_output=True,  # silence on success, available on error
+            text=False,
+        )
@@
-    except subprocess.CalledProcessError as e:
-        log.debug("Failed to create venv with uv: %s", str(e))
+    except subprocess.CalledProcessError as e:
+        out = (e.stdout or b"") + (e.stderr or b"")
+        log.debug("Failed to create venv with uv (rc=%s): %s", e.returncode, out.decode(errors="ignore"))
         return None

Also applies to: 451-456


503-511: Minor: use text=True to avoid manual decode/strip

Saves a decode and is clearer.

-        python_version = (
-            subprocess.check_output(
-                [python_exe, "-c", version_code],
-                stderr=subprocess.PIPE,
-                timeout=30
-            )
-            .decode()
-            .strip()
-        )
+        python_version = subprocess.check_output(
+            [python_exe, "-c", version_code],
+            stderr=subprocess.PIPE,
+            timeout=30,
+            text=True,
+        ).strip()

Also applies to: 512-516

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 76df5ea and ea7f844.

📒 Files selected for processing (1)
  • pioinstaller/penv.py (13 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.
📚 Learning: 2025-08-29T14:31:26.756Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:16.386Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T13:36:47.065Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.

Applied to files:

  • pioinstaller/penv.py
🧬 Code graph analysis (1)
pioinstaller/penv.py (3)
pioinstaller/exception.py (1)
  • PIOInstallerException (16-24)
pioinstaller/util.py (2)
  • safe_remove_dir (149-155)
  • download_file (89-113)
pioinstaller/core.py (1)
  • check (224-293)
🪛 Ruff (0.12.2)
pioinstaller/penv.py

107-107: subprocess call: check for execution of untrusted input

(S603)


168-168: Consider moving this statement to an else block

(TRY300)


206-206: Consider moving this statement to an else block

(TRY300)


221-222: Avoid specifying long messages outside the exception class

(TRY003)


230-231: Avoid specifying long messages outside the exception class

(TRY003)


260-262: Avoid specifying long messages outside the exception class

(TRY003)


267-267: os.chmod setting a permissive mask 0o755 on file or directory

(S103)


289-291: Abstract raise to an inner function

(TRY301)


289-291: Avoid specifying long messages outside the exception class

(TRY003)


295-295: Consider moving this statement to an else block

(TRY300)


311-314: Avoid specifying long messages outside the exception class

(TRY003)


477-477: subprocess call: check for execution of untrusted input

(S603)


488-489: Avoid specifying long messages outside the exception class

(TRY003)


492-493: Avoid specifying long messages outside the exception class

(TRY003)


504-504: subprocess call: check for execution of untrusted input

(S603)


514-515: Avoid specifying long messages outside the exception class

(TRY003)


544-546: Avoid specifying long messages outside the exception class

(TRY003)


556-556: Consider moving this statement to an else block

(TRY300)


558-560: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (4)
pioinstaller/penv.py (4)

94-99: Musl mapping fix (incl. ARMv7) looks good

Covers both gnu and gnueabihf suffixes. This should resolve Alpine/ARMv7 asset selection mismatches.


183-206: Checksum verification normalization is correct

Lowercasing and optional “sha256:” stripping avoids false negatives. Good error messaging on mismatch.


390-401: PATH update for uv dir is a nice touch

Ensures helper calls to “uv” work even if only the cached binary exists.


473-490: Installing the Python “uv” into the venv via system uv matches project intent

Aligned with prior decision to self-contain the venv. Timeouts and error surfacing look good.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
pioinstaller/penv.py (1)

141-174: GitHub API headers + digest parsing fix are on point

This addresses the API digest shape and ensures stable responses.

🧹 Nitpick comments (8)
pioinstaller/penv.py (8)

49-65: Use slots on DownloadConfig

Small memory/readability win; no behavior change.

-@dataclass
+@dataclass(slots=True)
 class DownloadConfig:

102-119: Cache musl detection (avoids repeated subprocess checks)

Musl probing can be relatively expensive; cache once per process.

+from functools import lru_cache
@@
-def detect_musl():
+@lru_cache(maxsize=1)
+def detect_musl():
     """Detect musl libc more robustly."""

141-174: Memoize checksum fetch to reduce API traffic and rate-limit risk

Results rarely change during a run; cache them.

@@
-def fetch_uv_checksums_from_github():
+from functools import lru_cache
+
+@lru_cache(maxsize=1)
+def fetch_uv_checksums_from_github():
     """Fetch SHA256 checksums from GitHub API."""

252-271: Clean up extracted temp directory after install (and optionally the archive)

Avoids leaving ~cache/tmp/uv-extract on disk.

     uv_dest = os.path.join(cache_dir, UV_EXE)
     shutil.copy2(uv_binary, uv_dest)
     if not util.IS_WINDOWS:
         os.chmod(uv_dest, 0o755)
 
     log.debug("uv installed at %s", uv_dest)
+    # Cleanup extraction directory
+    util.safe_remove_dir(extract_dir)
     return uv_dest

Outside this hunk (in download_and_install_uv), also delete the archive after success:

-        return _process_downloaded_archive(archive_path, extract_dir,
-                                         cache_dir)
+        uv_path = _process_downloaded_archive(archive_path, extract_dir, cache_dir)
+        try:
+            os.remove(archive_path)
+        except OSError:
+            pass
+        return uv_path

306-344: Add jitter to backoff and clean tmp on final failure

Reduces thundering herd and tidies leftovers.

@@
-                actual_delay = retry_delay * (2 ** (attempt - 1))
-                log.debug("Retrying in %d seconds...", actual_delay)
-                time.sleep(actual_delay)
+                actual_delay = retry_delay * (2 ** (attempt - 1))
+                # Add ~10% jitter
+                import random as _rnd  # local import to avoid global dependency
+                jitter = int(max(1, actual_delay * 0.1))
+                sleep_for = actual_delay + _rnd.randint(0, jitter)
+                log.debug("Retrying in %d seconds...", sleep_for)
+                time.sleep(sleep_for)
@@
-    if os.path.exists(archive_path):
+    if os.path.exists(archive_path):
         try:
             os.remove(archive_path)
         except OSError:
             pass
+    # Also remove any extraction leftovers
+    util.safe_remove_dir(extract_dir)

356-356: Windows: relax executable check for cached uv

On Windows, X_OK can be misleading; .exe existence is enough.

-    if os.path.isfile(cached_uv) and os.access(cached_uv, os.X_OK):
+    if os.path.isfile(cached_uv) and (util.IS_WINDOWS or os.access(cached_uv, os.X_OK)):

431-436: Optional: capture uv venv output only on failure

Helps diagnose failures without spamming stdout in the happy path.

-        subprocess.run(
-            cmd,
-            check=True,
-            timeout=300,  # 5 minutes timeout
-            stdout=subprocess.DEVNULL,
-            stderr=subprocess.STDOUT,
-        )
+        completed = subprocess.run(
+            cmd, timeout=300, capture_output=True, text=True, check=False
+        )
+        if completed.returncode != 0:
+            log.debug("uv venv failed: %s", completed.stdout or completed.stderr)
+            return None

Also applies to: 451-463


549-561: Write state.json atomically

Prevents partial writes on crash/kill.

-    try:
-        with open(state_path, "w", encoding="utf-8") as fp:
-            json.dump(state, fp, indent=2)
-        return state_path
+    try:
+        tmp_path = state_path + ".tmp"
+        with open(tmp_path, "w", encoding="utf-8") as fp:
+            json.dump(state, fp, indent=2)
+        os.replace(tmp_path, state_path)
+        return state_path
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ea7f844 and fa04019.

📒 Files selected for processing (1)
  • pioinstaller/penv.py (14 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/pack/packer.py:28-33
Timestamp: 2025-08-30T12:37:02.793Z
Learning: In the pioarduino-core-installer project, the pack command and create_wheels function are used in development/CI contexts where uv is already available (e.g., via `uv run make pack`). The uv availability is ensured by the workflow context rather than needing explicit checks in the pack function itself.
📚 Learning: 2025-08-29T14:31:26.756Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#7
File: pioinstaller/penv.py:198-206
Timestamp: 2025-08-29T14:31:26.756Z
Learning: In the pioarduino-installer project, the `install_uv_in_venv_with_system_uv` function intentionally installs the Python "uv" package from PyPI into the virtual environment using `uv pip install uv`. This is not the Rust CLI tool but rather a Python package that provides uv functionality within the venv, allowing the environment to be self-contained without dependency on the bootstrap uv CLI.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T12:37:16.386Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/penv.py:71-89
Timestamp: 2025-08-30T12:37:16.386Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in _extract_uv_archive for uv downloads because astral-sh/uv is considered a trusted source, and additional security hardening against path traversal via symlinks/hardlinks is not required for this use case.

Applied to files:

  • pioinstaller/penv.py
📚 Learning: 2025-08-30T13:36:47.065Z
Learnt from: Jason2866
PR: Jason2866/pioarduino-core-installer#9
File: pioinstaller/util.py:116-127
Timestamp: 2025-08-30T13:36:47.065Z
Learning: In the pioarduino-installer project, Jason2866 accepts the current archive extraction implementation in unpack_archive function with basic path traversal protection. Additional security hardening against symlinks/hardlinks/device files is not required for this use case, following the principle that if it doesn't fail, it's good enough.

Applied to files:

  • pioinstaller/penv.py
🧬 Code graph analysis (1)
pioinstaller/penv.py (4)
pioinstaller/exception.py (1)
  • PIOInstallerException (16-24)
pioinstaller/util.py (2)
  • safe_remove_dir (149-155)
  • download_file (89-113)
pioinstaller/core.py (1)
  • check (224-293)
pioinstaller/python.py (1)
  • check (126-169)
🪛 Ruff (0.12.2)
pioinstaller/penv.py

107-107: subprocess call: check for execution of untrusted input

(S603)


168-168: Consider moving this statement to an else block

(TRY300)


206-206: Consider moving this statement to an else block

(TRY300)


221-222: Avoid specifying long messages outside the exception class

(TRY003)


230-231: Avoid specifying long messages outside the exception class

(TRY003)


260-262: Avoid specifying long messages outside the exception class

(TRY003)


267-267: os.chmod setting a permissive mask 0o755 on file or directory

(S103)


289-291: Abstract raise to an inner function

(TRY301)


289-291: Avoid specifying long messages outside the exception class

(TRY003)


295-295: Consider moving this statement to an else block

(TRY300)


311-314: Avoid specifying long messages outside the exception class

(TRY003)


477-477: subprocess call: check for execution of untrusted input

(S603)


488-489: Avoid specifying long messages outside the exception class

(TRY003)


492-493: Avoid specifying long messages outside the exception class

(TRY003)


504-504: subprocess call: check for execution of untrusted input

(S603)


514-515: Avoid specifying long messages outside the exception class

(TRY003)


544-546: Avoid specifying long messages outside the exception class

(TRY003)


556-556: Consider moving this statement to an else block

(TRY300)


558-560: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (11)
pioinstaller/penv.py (11)

35-47: Good constants factoring

Centralizing URL, retry, and platform-specific exe/bin names looks solid.


94-99: musl mapping fix is correct

The gnueabihf→musleabihf handling closes the ARMv7 gap.


121-139: Robust digest-string parser

Handles "sha256:" cleanly and normalizes case. LGTM.


176-181: LGTM: expected checksum lookup

Simple filename match against fetched assets is fine.


183-211: LGTM: checksum verification

Normalization and streaming hash are correct.


221-223: Path traversal guard in archive extraction is good

The prefix check prevents writing outside extract_dir.

Also applies to: 230-232


244-250: LGTM: temp paths creation

Predictable tmp layout under cache_dir is fine.


273-304: LGTM: single-attempt download with verification

Removes stale partials and gates success on checksum when available.


371-384: LGTM: dir helpers docstrings

Clear and consistent with new constants.


473-475: LGTM: install uv (Python package) into venv via system uv

Matches the accepted workflow for a self-contained penv.

Also applies to: 480-494


496-516: LGTM: timed python version probe with clear error

Timeout and exception mapping are appropriate.

@Jason2866 Jason2866 merged commit e6b5515 into pioarduino Aug 31, 2025
@Jason2866 Jason2866 deleted the sha_check_retry branch August 31, 2025 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant