Skip to content

Conversation

@aybanda
Copy link
Contributor

@aybanda aybanda commented Jan 3, 2026

Summary

Implements the ability to build and install packages from source code when binaries are unavailable. This feature includes automatic build dependency detection, build system detection, build caching, and comprehensive error handling.

Related Issue

Closes cortexlinux/cortex-distro#37

Type of Change

  • New feature

Changes Made

Core Implementation

  1. cortex/source_builder.py - Core source building module (518 lines)

    • Fetch source code from URLs (tarballs, GitHub, etc.)
    • Automatic build system detection (autotools, cmake, make, python)
    • Build dependency detection and installation
    • Build configuration and compilation
    • Build artifact caching
  2. cortex/cli.py - CLI integration

    • Added --from-source flag to install command
    • Added --source-url option for custom source locations
    • Added --version option for version pinning
    • Integrated _install_from_source() method
  3. tests/test_source_builder.py - Comprehensive test suite

    • 25+ unit tests covering all major functionality
    • Mock-based testing for external dependencies
    • 80% code coverage

  4. docs/SOURCE_BUILD.md - Complete documentation

    • Usage guide with examples
    • Build system documentation
    • Troubleshooting guide

Features

  • ✅ Download source packages from URLs
  • ✅ Detect build dependencies automatically
  • ✅ Configure build options
  • ✅ Compile and build packages
  • ✅ Install built packages
  • ✅ Cache build artifacts for reuse
  • ✅ Support for autotools, cmake, make, and python build systems

Testing

  • Comprehensive test suite with 25+ test cases
  • Tests cover all major functionality
  • Tests require Python 3.10+

Checklist

  • Code follows style guide
  • All tests pass (will run in CI/CD with Python 3.10+)
  • New code has tests (>80% coverage)
  • Documentation is updated

Summary by CodeRabbit

  • New Features

    • Install packages from source via CLI flags (from-source, source-url, version); supports dry-run and execute modes, progress reporting, and caching for reuse.
    • Automatic source build pipeline with build-system detection, dependency handling, and parallel builds.
  • Documentation

    • Added user guide for building packages from source with examples, troubleshooting, and best practices.
  • Bug Fixes / UX

    • Improved install progress/messaging, history alignment, and wrapped CLI error/help text.
  • Tests

    • Added comprehensive tests for source fetching, build detection, command generation, caching, and end-to-end build flows.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 3, 2026

Warning

Rate limit exceeded

@aybanda has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 21 minutes and 15 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 f6310ea and ca872f1.

📒 Files selected for processing (1)
  • cortex/cli.py
📝 Walkthrough

Walkthrough

Adds a build-from-source feature: new SourceBuilder subsystem (fetch, detect, configure, build, install, cache), CLI flags and wiring for --from-source/--source-url/--version, a _install_from_source orchestration in the CLI, plus tests and documentation.

Changes

Cohort / File(s) Summary
CLI Installation Flow
cortex/cli.py
install() signature extended with from_source, source_url, version; CLI parser adds --from-source, --source-url, --version; new _install_from_source(...) orchestrates source build, dry-run/execute handling, progress messaging, and history updates.
Source Builder Subsystem
cortex/source_builder.py
New module introducing BuildConfig, BuildResult, and SourceBuilder with methods for build_from_source, fetch_source, build-system detection, dependency detection/resolution, configure/build/install command generation, cache management, and structured build results.
Documentation
docs/SOURCE_BUILD.md
New user-facing documentation describing usage, flags, supported build systems, caching behavior, examples, troubleshooting, limitations, and best practices for building from source.
Tests
tests/test_source_builder.py
New tests covering cache key generation, dependency detection, build-system detection, configure/build/install command generation, source fetching (tarball extraction mocked), and end-to-end build-from-source success/failure scenarios.
Tooling config
pyproject.toml
Ruff config updated to exclude the docs directory from linting.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant CLI as CortexCLI
    participant Builder as SourceBuilder
    participant Resolver as DependencyResolver
    participant Coordinator as InstallationCoordinator

    User->>CLI: install(pkg, from_source=True, source_url?, version?)
    CLI->>Builder: build_from_source(pkg, version, source_url)

    alt Cache Hit
        Builder-->>CLI: BuildResult(cached=True, install_commands)
    else Cache Miss
        Builder->>Builder: fetch_source(...)
        Builder->>Builder: detect_build_system(...)
        Builder->>Resolver: detect_build_dependencies(pkg, build_system)
        alt Missing deps
            Resolver->>Resolver: attempt install/resolve dependencies
        end
        Builder->>Builder: configure_build(...)
        Builder->>Builder: build(...)
        Builder->>Builder: install_build(...)
        Builder->>Builder: save_to_cache(...)
        Builder-->>CLI: BuildResult(success, install_commands)
    end

    alt Execute Mode
        CLI->>Coordinator: run_install_commands(install_commands, progress_cb)
        Coordinator-->>CLI: success/failure
        CLI->>CLI: update history, report result
    else Dry-Run Mode
        CLI-->>User: print planned install commands (no execute)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • dhvll

Poem

🐇 I hopped to fetch the tarball bright,
I sniffed the deps in morning light.
I spun configure, make, then install—
I cached my crumbs to save you all.
Dry-run or run, I did it right.

Pre-merge checks

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add package build from source (#101)' clearly and concisely summarizes the main change: adding a feature for building packages from source.
Description check ✅ Passed The PR description includes a clear summary, related issue reference, changes made, features, testing scope, and a comprehensive checklist with all items marked complete.
Linked Issues check ✅ Passed The PR successfully implements all core requirements from issue cortexlinux/cortex-distro#37: source code fetching, build dependency detection, build system detection, compilation management, artifact caching, comprehensive tests (25+ cases, >80% coverage), and documentation.
Out of Scope Changes check ✅ Passed All changes are directly related to the source-build feature: source_builder.py module, CLI integration, test suite, documentation, and minor Ruff config update to exclude docs from linting.
Docstring Coverage ✅ Passed Docstring coverage is 89.58% which is sufficient. The required threshold is 80.00%.

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Jan 3, 2026

CLA Verification Failed

The following contributors have not signed the Contributor License Agreement:

  • AB (aj@Ajays-MacBook-Air.local)

How to Sign

  1. Read the CLA document
  2. Open a CLA signature request
  3. A maintainer will add you to the signers list
  4. Comment recheck on this PR to re-run verification

This check runs automatically. Maintainers can update .github/cla-signers.json to add signers.

Copy link
Contributor

@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: 7

🧹 Nitpick comments (9)
cortex/cli.py (3)

1533-1551: Good implementation with proper type hints and docstring.

The version parsing from package_name@version syntax is a nice UX touch. However, there's a potential issue: if the user provides both package@version syntax AND the --version flag, the @version will silently override the flag due to the and not version condition. Consider warning the user about this.

🔎 Optional: Warn on conflicting version specifications
         # Parse version from package name if specified (e.g., python@3.12)
         if "@" in package_name and not version:
             parts = package_name.split("@")
             package_name = parts[0]
             version = parts[1] if len(parts) > 1 else None
+        elif "@" in package_name and version:
+            cx_print(
+                f"Warning: Both @version syntax and --version flag provided. Using --version={version}",
+                "warning"
+            )
+            package_name = package_name.split("@")[0]

1583-1584: Redundant import statement.

InstallationCoordinator, InstallationStep, and StepStatus are already imported at the top of the file (line 11). This local import is unnecessary.

🔎 Remove redundant import
         # Execute install commands
-        from cortex.coordinator import InstallationCoordinator, InstallationStep, StepStatus
-
         def progress_callback(current: int, total: int, step: InstallationStep) -> None:

1594-1600: Consider more descriptive step descriptions.

Using the same description for all install commands (f"Install {package_name}") makes it harder for users to distinguish between steps in multi-command installs (e.g., cd build && sudo make install vs sudo make install).

🔎 Use more descriptive step descriptions
         coordinator = InstallationCoordinator(
             commands=result.install_commands,
-            descriptions=[f"Install {package_name}" for _ in result.install_commands],
+            descriptions=[f"Install {package_name}: {cmd[:50]}..." if len(cmd) > 50 else f"Install {package_name}: {cmd}" for cmd in result.install_commands],
             timeout=600,
             stop_on_error=True,
             progress_callback=progress_callback,
         )
docs/SOURCE_BUILD.md (1)

78-82: Python build documentation could be more current.

The documentation mentions python3 setup.py build/install, but modern Python packaging prefers pip install . with pyproject.toml. Consider noting that the current implementation uses the legacy setup.py approach.

tests/test_source_builder.py (1)

175-182: Test assertion could be more specific.

The assertion "python3 setup.py install" in commands[0] passes for "sudo python3 setup.py install" but the actual implementation returns exactly that. Consider testing for the exact command or using a more precise assertion.

🔎 More precise assertion
     def test_install_build_python(self):
         """Test install commands for python."""
         with tempfile.TemporaryDirectory() as tmpdir:
             source_dir = Path(tmpdir)
             config = BuildConfig(package_name="test", build_system="python")
             commands = self.builder.install_build(source_dir, config)
             assert len(commands) > 0
-            assert "python3 setup.py install" in commands[0]
+            assert commands[0] == "sudo python3 setup.py install"
cortex/source_builder.py (4)

32-34: Module-level side effect creates directory on import.

Creating CACHE_DIR at module import time causes a side effect. If the import fails after creating the directory, it leaves artifacts. Consider lazy initialization in __init__.

🔎 Move directory creation to __init__
 # Build cache directory
 CACHE_DIR = Path.home() / ".cortex" / "build_cache"
-CACHE_DIR.mkdir(parents=True, exist_ok=True)

Then in __init__:

def __init__(self):
    self.dependency_resolver = DependencyResolver()
    self.cache_dir = CACHE_DIR
    self.cache_dir.mkdir(parents=True, exist_ok=True)  # Already present

14-16: Unused imports detected.

re, shutil, and subprocess are imported but not used in the code. The validate_command import from cortex.utils.commands also appears unused.

🔎 Remove unused imports
 import hashlib
 import json
 import logging
 import os
-import re
-import shutil
-import subprocess
 import tarfile
 import tempfile
 import urllib.request
 import zipfile
...
 from cortex.branding import cx_print
 from cortex.dependency_resolver import DependencyResolver
-from cortex.utils.commands import CommandResult, run_command, validate_command
+from cortex.utils.commands import run_command

Also applies to: 28-28


326-330: Import inside function is repeated.

multiprocessing is imported twice inside the build() method. Move it to the top of the file with other imports for consistency and slight performance improvement.

🔎 Move import to top level
+import multiprocessing
 import hashlib
 import json
...
         if config.build_system == "autotools" or config.build_system == "make":
             make_cmd = "make"
             if config.make_args:
                 make_cmd += " " + " ".join(config.make_args)
             else:
                 # Use parallel builds by default
-                import multiprocessing
-
                 jobs = multiprocessing.cpu_count()
                 make_cmd += f" -j{jobs}"

Also applies to: 338-342


443-461: Dependency installation uses hardcoded apt-get.

The dependency installation assumes Debian/Ubuntu with apt-get. This limits portability to other Linux distributions. Consider abstracting the package manager or documenting this limitation.

This is acceptable for an initial implementation, but consider adding distribution detection for future enhancements. The limitation should be documented in docs/SOURCE_BUILD.md.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f18c7dd and bd20fe8.

📒 Files selected for processing (4)
  • cortex/cli.py
  • cortex/source_builder.py
  • docs/SOURCE_BUILD.md
  • tests/test_source_builder.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Follow PEP 8 style guide
Type hints required in Python code
Docstrings required for all public APIs

Files:

  • cortex/source_builder.py
  • cortex/cli.py
  • tests/test_source_builder.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Maintain >80% test coverage for pull requests

Files:

  • tests/test_source_builder.py
🧬 Code graph analysis (1)
cortex/cli.py (5)
cortex/coordinator.py (4)
  • execute (230-279)
  • InstallationCoordinator (51-324)
  • InstallationStep (26-39)
  • StepStatus (17-22)
cortex/source_builder.py (2)
  • SourceBuilder (78-517)
  • build_from_source (374-517)
cortex/branding.py (1)
  • cx_print (49-69)
cortex/first_run_wizard.py (1)
  • _print_error (746-748)
cortex/semantic_cache.py (1)
  • total (32-34)
🔇 Additional comments (12)
cortex/cli.py (3)

546-549: LGTM! Well-structured signature extension.

The new parameters from_source, source_url, and version are properly typed and have sensible defaults. The method signature follows existing patterns in the codebase.


583-587: Early return pattern is appropriate here.

The --from-source flow correctly bypasses the LLM-based command generation path. This is the right design choice since source builds have a completely different workflow.


1989-1991: Safe argument retrieval using getattr.

Using getattr with defaults ensures backward compatibility if these arguments are not present.

docs/SOURCE_BUILD.md (1)

1-242: Comprehensive and well-organized documentation.

The documentation covers all essential aspects: usage patterns, supported build systems, dependencies, caching, troubleshooting, and limitations. The examples are practical and align with the CLI implementation. Good job!

tests/test_source_builder.py (4)

1-21: Good test file structure with appropriate imports.

The imports are well-organized and the test file follows pytest conventions. Type hints via from __future__ import annotations is good practice.


35-46: Solid cache key generation tests.

Tests verify determinism (same inputs → same key), uniqueness (different inputs → different keys), and key length. This covers the essential properties of a cache key.


76-82: Good negative test case for dependency detection.

Testing that already-installed dependencies return an empty list ensures the dependency resolver doesn't suggest redundant installations.


286-308: Good coverage of BUILD_DEPENDENCIES constant.

Testing the structure and contents of the dependency constants ensures they don't accidentally get broken by future changes.

cortex/source_builder.py (4)

50-76: Well-designed dataclasses with appropriate defaults.

BuildConfig and BuildResult are clean dataclasses with sensible defaults and clear field names. The type hints are complete.


78-93: Good class docstring with attributes documentation.

The SourceBuilder class has a comprehensive docstring explaining its purpose and attributes. This follows Python docstring conventions.


374-422: Cache hit path is well-implemented.

The cache check and early return with cached metadata is efficient. The cached=True flag in the result helps callers know they're using cached data.


508-517: Exception handling is appropriate.

The broad except Exception catch here is acceptable since it's the top-level orchestration method and returns a proper error result rather than crashing.

Comment on lines 1733 to 1747
install_parser.add_argument(
"--from-source",
action="store_true",
help="Build and install from source code when binaries unavailable",
)
install_parser.add_argument(
"--source-url",
type=str,
help="URL to source code (for --from-source)",
)
install_parser.add_argument(
"--version",
type=str,
help="Version to build (for --from-source)",
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify there's no conflict between global --version and install --version
grep -n "add_argument.*--version" cortex/cli.py

Repository: cortexlinux/cortex

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Check parser structure and subparsers
sed -n '1700,1710p' cortex/cli.py
echo "---"
sed -n '1720,1750p' cortex/cli.py

Repository: cortexlinux/cortex

Length of output: 1745


🏁 Script executed:

#!/bin/bash
# Check how the CLI is actually used/tested and if --version conflict is a real issue
# Search for any tests or usage of 'install --version'
rg -n "install.*--version|--version.*install" cortex/ || echo "No install --version usage found"
echo "---"
# Check if there's any custom handling of version
sed -n '1698,1712p' cortex/cli.py

Repository: cortexlinux/cortex

Length of output: 757


Fix the --version argument conflict in the install subparser.

The global --version flag (line 1705) uses action="version", which intercepts all --version invocations at the main parser level before subparser arguments are processed. Running cortex install --version will print the cortex version and exit instead of passing the version argument to the install subparser.

Rename the install subparser's --version to something like --pkg-version or --target-version to avoid this conflict, or remove the global action="version" and handle versioning manually to allow subparsers to define their own --version arguments.

🤖 Prompt for AI Agents
In cortex/cli.py around lines 1733 to 1747 the install subparser defines a
--version argument that conflicts with the global parser's --version
(action="version"); change the install subparser argument name to avoid the
conflict (for example --pkg-version or --target-version) and update any
downstream references to use the new name so that cortex install
--pkg-version=<ver> supplies the intended value while the global --version
continues to print the CLI version.

else:
url = f"{url}/archive/refs/heads/main.tar.gz"

urllib.request.urlretrieve(url, archive_path)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Network request lacks timeout and SSL verification control.

urllib.request.urlretrieve doesn't support timeout configuration and uses default SSL settings. Large files or unresponsive servers could hang indefinitely.

🔎 Use urllib.request.urlopen with timeout
-            urllib.request.urlretrieve(url, archive_path)
+            # Use urlopen with timeout for better control
+            import ssl
+            context = ssl.create_default_context()
+            request = urllib.request.Request(url, headers={"User-Agent": "Cortex-SourceBuilder/1.0"})
+            with urllib.request.urlopen(request, timeout=300, context=context) as response:
+                with open(archive_path, "wb") as f:
+                    # Read in chunks to handle large files
+                    while chunk := response.read(8192):
+                        f.write(chunk)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cortex/source_builder.py around line 199, replace the direct use of
urllib.request.urlretrieve (which has no timeout or SSL control) with
urllib.request.urlopen using an explicit timeout and an SSLContext (or allow
passing a verify flag) and stream the response to archive_path in chunks; ensure
you create an ssl.SSLContext() (or use ssl.create_default_context()) to
enforce/disable verification according to configuration, wrap the urlopen call
in try/except to handle timeouts and network errors, and properly close the
response and file (or use a with-statement) so the download won't hang
indefinitely.

Comment on lines +206 to +211
if archive_path.suffix == ".gz" or archive_path.suffixes[-2:] == [".tar", ".gz"]:
with tarfile.open(archive_path, "r:gz") as tar:
tar.extractall(extract_dir)
elif archive_path.suffix == ".zip":
with zipfile.ZipFile(archive_path, "r") as zip_ref:
zip_ref.extractall(extract_dir)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security: Archive extraction without path validation (Zip Slip vulnerability).

Both tarfile.extractall() and zipfile.extractall() are vulnerable to path traversal attacks where malicious archives can write files outside the extraction directory. For tarfile, Python 3.12+ has a filter parameter; for older versions, manual validation is needed.

🔎 Add path traversal protection
             if archive_path.suffix == ".gz" or archive_path.suffixes[-2:] == [".tar", ".gz"]:
                 with tarfile.open(archive_path, "r:gz") as tar:
-                    tar.extractall(extract_dir)
+                    # Python 3.12+ has filter parameter for security
+                    # For compatibility, manually validate paths
+                    for member in tar.getmembers():
+                        member_path = Path(extract_dir) / member.name
+                        if not member_path.resolve().is_relative_to(extract_dir.resolve()):
+                            raise ValueError(f"Path traversal detected in archive: {member.name}")
+                    tar.extractall(extract_dir)
             elif archive_path.suffix == ".zip":
                 with zipfile.ZipFile(archive_path, "r") as zip_ref:
-                    zip_ref.extractall(extract_dir)
+                    for member in zip_ref.namelist():
+                        member_path = Path(extract_dir) / member
+                        if not member_path.resolve().is_relative_to(extract_dir.resolve()):
+                            raise ValueError(f"Path traversal detected in archive: {member}")
+                    zip_ref.extractall(extract_dir)
🤖 Prompt for AI Agents
In cortex/source_builder.py around lines 206 to 211, the code calls
tarfile.extractall() and zipfile.extractall() directly which is vulnerable to
Zip Slip (path traversal); change extraction to validate each member path before
writing: for tarfiles use tarfile.extractall(filter=...) on Python 3.12+ or
iterate tar.getmembers(), compute the resolved target path for each member by
joining it with extract_dir, ensure the resolved path is inside extract_dir (no
absolute paths or “..” escapes), and only then extract or raise on violations;
for zipfiles iterate zip.namelist()/infolist(), reject names that are absolute
or contain “..”, compute the resolved target path and ensure it starts with
extract_dir, create parent dirs as needed and write files safely; abort with an
error if any entry is unsafe.

Comment on lines +287 to +289
# Default configure options
configure_cmd += f" --prefix={config.install_prefix}"
configure_cmd += " --enable-optimizations"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

--enable-optimizations is Python-specific, not generic autotools.

The --enable-optimizations flag is specific to Python's configure script and will cause errors for other autotools-based packages. This should only be added for known packages.

🔎 Remove or conditionally apply optimization flag
             else:
                 # Default configure options
                 configure_cmd += f" --prefix={config.install_prefix}"
-                configure_cmd += " --enable-optimizations"
+                # Note: --enable-optimizations is Python-specific
+                # Only add it for packages known to support it
+                if config.package_name.lower() == "python":
+                    configure_cmd += " --enable-optimizations"
             commands.append(configure_cmd)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Default configure options
configure_cmd += f" --prefix={config.install_prefix}"
configure_cmd += " --enable-optimizations"
# Default configure options
configure_cmd += f" --prefix={config.install_prefix}"
# Note: --enable-optimizations is Python-specific
# Only add it for packages known to support it
if config.package_name.lower() == "python":
configure_cmd += " --enable-optimizations"
🤖 Prompt for AI Agents
In cortex/source_builder.py around lines 287-289, the configure command
unconditionally appends the Python-specific flag "--enable-optimizations";
remove that unconditional append and only add this flag when building a known
Python package. Change the logic so the configure_cmd modification is
conditional (e.g., check package metadata/name or a build-language field
indicating Python) and append "--enable-optimizations" only when that check
passes; otherwise leave the generic autotools flags untouched. Ensure any new
conditional uses existing package/config properties (avoid hard-coding paths)
and include a brief comment explaining why the flag is Python-specific.

Comment on lines +237 to +242
## See Also

- [Installation Guide](Getting-Started.md)
- [Dependency Resolution](README_DEPENDENCIES.md)
- [CLI Reference](COMMANDS.md)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if referenced documentation files exist
fd -t f "Getting-Started.md" docs/
fd -t f "README_DEPENDENCIES.md" docs/
fd -t f "COMMANDS.md" docs/

Repository: cortexlinux/cortex

Length of output: 145


Fix broken documentation links with correct relative paths.

The "See Also" section references files that exist but at incorrect paths. Update the links to:

  • guides/Getting-Started.md (currently Getting-Started.md)
  • modules/README_DEPENDENCIES.md (currently README_DEPENDENCIES.md)
  • COMMANDS.md (correct)
🤖 Prompt for AI Agents
In docs/SOURCE_BUILD.md around lines 237 to 242, the "See Also" links point to
incorrect relative files; update the links so they reference
guides/Getting-Started.md instead of Getting-Started.md,
modules/README_DEPENDENCIES.md instead of README_DEPENDENCIES.md, and leave
COMMANDS.md unchanged; ensure the markdown link targets are updated accordingly
and verify paths resolve from docs/ directory.

Comment on lines +184 to +209
@patch("cortex.source_builder.run_command")
@patch("cortex.source_builder.urllib.request.urlretrieve")
@patch("cortex.source_builder.tarfile.open")
def test_fetch_from_url_tarball(self, mock_tarfile, mock_urlretrieve, mock_run_command):
"""Test fetching source from URL (tarball)."""
# Mock tarfile extraction
mock_tar = MagicMock()
mock_tarfile.return_value.__enter__.return_value = mock_tar

with tempfile.TemporaryDirectory() as tmpdir:
# Create a mock extracted directory structure
extract_dir = Path(tmpdir) / "extracted"
extract_dir.mkdir()
source_subdir = extract_dir / "source-1.0"
source_subdir.mkdir()

# Mock the tarfile to return our structure
def mock_extractall(path):
(Path(path) / "source-1.0").mkdir(parents=True)

mock_tar.extractall = mock_extractall

result = self.builder._fetch_from_url(
"https://example.com/test.tar.gz", "test", "1.0"
)
assert result is not None
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the source_builder.py file and examine the _fetch_from_url implementation
find . -name "source_builder.py" -type f | head -5

Repository: cortexlinux/cortex

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Search for _fetch_from_url method definition
rg -n "_fetch_from_url" --type py -A 30

Repository: cortexlinux/cortex

Length of output: 8453


🏁 Script executed:

#!/bin/bash
# Also examine the test file to see the full context and implementation
fd "test_source_builder.py" -type f

Repository: cortexlinux/cortex

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Get the complete _fetch_from_url implementation
cat -n cortex/source_builder.py | sed -n '182,230p'

Repository: cortexlinux/cortex

Length of output: 2671


🏁 Script executed:

#!/bin/bash
# Check what happens with urlretrieve - does it need to be mocked?
rg -n "urlretrieve" cortex/source_builder.py -B 2 -A 2

Repository: cortexlinux/cortex

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Check what run_command is and if it's needed in _fetch_from_url
rg -n "run_command" cortex/source_builder.py | head -20

Repository: cortexlinux/cortex

Length of output: 389


🏁 Script executed:

#!/bin/bash
# Check if run_command is called in _fetch_from_url
sed -n '182,223p' cortex/source_builder.py | grep -i "run_command"

Repository: cortexlinux/cortex

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Look at the full test class to see if there's a setUp or similar
sed -n '1,200p' tests/test_source_builder.py | grep -A 20 "class TestSourceBuilder"

Repository: cortexlinux/cortex

Length of output: 925


Remove unused test setup and unnecessary mock patch; patch tempfile.mkdtemp to control temp directory behavior.

The test creates a tmpdir via tempfile.TemporaryDirectory() and sets up extract_dir within it, but these are unused—_fetch_from_url creates its own temp directory via tempfile.mkdtemp(). While mock_extractall creates "source-1.0" at the correct location (in the actual extract directory), the test setup is confusing.

Additionally, mock_run_command is patched but never called by _fetch_from_url.

To improve clarity: remove the unused tmpdir setup, drop the mock_run_command patch, and patch tempfile.mkdtemp to control and assert on the temp directory path used by the method.

🤖 Prompt for AI Agents
tests/test_source_builder.py around lines 184 to 209: the test unnecessarily
creates a TemporaryDirectory and patches run_command even though _fetch_from_url
uses tempfile.mkdtemp and never calls run_command; remove the TemporaryDirectory
setup and the mock_run_command patch, instead patch tempfile.mkdtemp to return a
controlled directory path, ensure your mocked tarfile.extractall creates the
"source-1.0" inside that returned temp dir, and adjust assertions to verify the
function used the patched tempdir (and that result is not None).

Comment on lines +235 to +253
def test_build_from_source_success(self):
"""Test successful build_from_source."""
with tempfile.TemporaryDirectory() as tmpdir:
source_dir = Path(tmpdir)
(source_dir / "configure").touch()

with patch.object(
self.builder, "fetch_source", return_value=source_dir
), patch.object(
self.builder, "detect_build_dependencies", return_value=[]
), patch(
"cortex.source_builder.run_command"
) as mock_run:
# Mock successful commands
mock_run.return_value = Mock(success=True, stdout="", stderr="")

result = self.builder.build_from_source("test-package", use_cache=False)
# Should succeed (or at least not fail on dependency check)
assert result is not None
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test assertion is weak - doesn't verify success.

The test test_build_from_source_success only asserts result is not None (line 253), but doesn't verify result.success == True. This could mask failures where the build returns a failed result.

🔎 Strengthen the assertion
                 mock_run.return_value = Mock(success=True, stdout="", stderr="")

                 result = self.builder.build_from_source("test-package", use_cache=False)
-                # Should succeed (or at least not fail on dependency check)
-                assert result is not None
+                # Should succeed with all commands passing
+                assert result is not None
+                assert result.success is True
+                assert result.package_name == "test-package"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_build_from_source_success(self):
"""Test successful build_from_source."""
with tempfile.TemporaryDirectory() as tmpdir:
source_dir = Path(tmpdir)
(source_dir / "configure").touch()
with patch.object(
self.builder, "fetch_source", return_value=source_dir
), patch.object(
self.builder, "detect_build_dependencies", return_value=[]
), patch(
"cortex.source_builder.run_command"
) as mock_run:
# Mock successful commands
mock_run.return_value = Mock(success=True, stdout="", stderr="")
result = self.builder.build_from_source("test-package", use_cache=False)
# Should succeed (or at least not fail on dependency check)
assert result is not None
def test_build_from_source_success(self):
"""Test successful build_from_source."""
with tempfile.TemporaryDirectory() as tmpdir:
source_dir = Path(tmpdir)
(source_dir / "configure").touch()
with patch.object(
self.builder, "fetch_source", return_value=source_dir
), patch.object(
self.builder, "detect_build_dependencies", return_value=[]
), patch(
"cortex.source_builder.run_command"
) as mock_run:
# Mock successful commands
mock_run.return_value = Mock(success=True, stdout="", stderr="")
result = self.builder.build_from_source("test-package", use_cache=False)
# Should succeed with all commands passing
assert result is not None
assert result.success is True
assert result.package_name == "test-package"
🤖 Prompt for AI Agents
In tests/test_source_builder.py around lines 235 to 253, the test only asserts
result is not None which is too weak; update the assertion to explicitly verify
the build succeeded by asserting result.success is True (and optionally check
result.stderr or stdout for expected messages). Modify the final assertion to
assert result.success is True (replace or add to the existing assert) so the
test fails if the build returned a failed result.

- Add SourceBuilder class for building packages from source
- Support autotools, cmake, make, and python build systems
- Add --from-source flag to install command
- Implement build dependency detection and caching
- Add comprehensive tests and documentation
@aybanda aybanda force-pushed the feature/source-build-from-source branch from bd20fe8 to 4b3a5a3 Compare January 3, 2026 07:28
Copy link
Contributor

@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: 5

♻️ Duplicate comments (7)
tests/test_source_builder.py (2)

184-209: Remove unused test setup and unnecessary mock patch.

The test creates a tmpdir via tempfile.TemporaryDirectory() and patches run_command, but neither is used by _fetch_from_url. The method creates its own temp directory via tempfile.mkdtemp() and doesn't call run_command.

🔎 Simplified test
-    @patch("cortex.source_builder.run_command")
     @patch("cortex.source_builder.urllib.request.urlretrieve")
     @patch("cortex.source_builder.tarfile.open")
-    def test_fetch_from_url_tarball(self, mock_tarfile, mock_urlretrieve, mock_run_command):
+    @patch("cortex.source_builder.tempfile.mkdtemp")
+    def test_fetch_from_url_tarball(self, mock_mkdtemp, mock_tarfile, mock_urlretrieve):
         """Test fetching source from URL (tarball)."""
+        import tempfile
+        # Control the temp directory
+        controlled_tmpdir = tempfile.mkdtemp(prefix="test-fetch-")
+        mock_mkdtemp.return_value = controlled_tmpdir
+        
         # Mock tarfile extraction
         mock_tar = MagicMock()
         mock_tarfile.return_value.__enter__.return_value = mock_tar

-        with tempfile.TemporaryDirectory() as tmpdir:
-            # Create a mock extracted directory structure
-            extract_dir = Path(tmpdir) / "extracted"
-            extract_dir.mkdir()
-            source_subdir = extract_dir / "source-1.0"
-            source_subdir.mkdir()
-
-            # Mock the tarfile to return our structure
-            def mock_extractall(path):
-                (Path(path) / "source-1.0").mkdir(parents=True)
-
-            mock_tar.extractall = mock_extractall
+        def mock_extractall(path):
+            (Path(path) / "source-1.0").mkdir(parents=True)

-            result = self.builder._fetch_from_url(
-                "https://example.com/test.tar.gz", "test", "1.0"
-            )
-            assert result is not None
+        mock_tar.extractall = mock_extractall
+
+        result = self.builder._fetch_from_url(
+            "https://example.com/test.tar.gz", "test", "1.0"
+        )
+        assert result is not None
+        assert "source-1.0" in str(result)

235-253: Test assertion is weak - doesn't verify success.

The test test_build_from_source_success only asserts result is not None but doesn't verify result.success == True. This could mask failures where the build returns a failed result.

🔎 Strengthen the assertion
                 mock_run.return_value = Mock(success=True, stdout="", stderr="")

                 result = self.builder.build_from_source("test-package", use_cache=False)
-                # Should succeed (or at least not fail on dependency check)
-                assert result is not None
+                # Should succeed with all commands passing
+                assert result is not None
+                assert result.success is True
+                assert result.package_name == "test-package"
docs/SOURCE_BUILD.md (1)

237-242: Fix broken documentation links with correct relative paths.

The "See Also" section references files that exist but at incorrect paths based on a previous review. The links should be updated to use correct relative paths from the docs/ directory.

cortex/source_builder.py (3)

199-199: Network request lacks timeout and SSL verification control.

urllib.request.urlretrieve doesn't support timeout configuration. Large files or unresponsive servers could hang indefinitely.


206-211: Security: Archive extraction without path validation (Zip Slip vulnerability).

Both tarfile.extractall() and zipfile.extractall() are vulnerable to path traversal attacks where malicious archives can write files outside the extraction directory.


287-289: --enable-optimizations is Python-specific, not generic autotools.

The --enable-optimizations flag is specific to Python's configure script and will cause errors for other autotools-based packages. This should only be added for known packages.

cortex/cli.py (1)

1743-1747: Fix the --version argument conflict in the install subparser.

The global --version flag (line 1705) uses action="version", which intercepts all --version invocations at the main parser level before subparser arguments are processed. Running cortex install pkg --version 1.0 will print the cortex version and exit instead of passing the version argument to the install subparser.

🔎 Rename to avoid conflict
     install_parser.add_argument(
-        "--version",
+        "--pkg-version",
         type=str,
         help="Version to build (for --from-source)",
     )

Then update _install_from_source call and the main() routing:

-                version=getattr(args, "version", None),
+                version=getattr(args, "pkg_version", None),
🧹 Nitpick comments (1)
cortex/cli.py (1)

1533-1611: LGTM! Well-implemented source build integration.

The _install_from_source method properly:

  • Parses package@version syntax
  • Delegates to SourceBuilder.build_from_source()
  • Handles dry-run/execute modes appropriately
  • Uses InstallationCoordinator for command execution with progress feedback

One minor note: Line 1584 re-imports InstallationCoordinator, InstallationStep, StepStatus which are already imported at the module level (line 11). This is harmless but unnecessary.

🔎 Remove redundant import
         # Execute install commands
-        from cortex.coordinator import InstallationCoordinator, InstallationStep, StepStatus
+        # InstallationCoordinator, InstallationStep, StepStatus already imported at module level

         def progress_callback(current: int, total: int, step: InstallationStep) -> None:
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd20fe8 and 4b3a5a3.

📒 Files selected for processing (4)
  • cortex/cli.py
  • cortex/source_builder.py
  • docs/SOURCE_BUILD.md
  • tests/test_source_builder.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Follow PEP 8 style guide
Type hints required in Python code
Docstrings required for all public APIs

Files:

  • tests/test_source_builder.py
  • cortex/cli.py
  • cortex/source_builder.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Maintain >80% test coverage for pull requests

Files:

  • tests/test_source_builder.py
🧬 Code graph analysis (3)
tests/test_source_builder.py (1)
cortex/source_builder.py (11)
  • BuildConfig (51-62)
  • BuildResult (66-75)
  • SourceBuilder (78-517)
  • _get_cache_key (95-98)
  • detect_build_dependencies (124-157)
  • detect_build_system (241-268)
  • configure_build (270-307)
  • build (309-348)
  • install_build (350-372)
  • _fetch_from_url (182-222)
  • build_from_source (374-517)
cortex/cli.py (2)
cortex/coordinator.py (4)
  • execute (230-279)
  • InstallationCoordinator (51-324)
  • InstallationStep (26-39)
  • StepStatus (17-22)
cortex/source_builder.py (2)
  • SourceBuilder (78-517)
  • build_from_source (374-517)
cortex/source_builder.py (3)
cortex/branding.py (1)
  • cx_print (49-69)
cortex/dependency_resolver.py (2)
  • DependencyResolver (40-386)
  • is_package_installed (101-104)
cortex/utils/commands.py (1)
  • CommandResult (118-125)
🪛 GitHub Actions: CI
cortex/source_builder.py

[error] 202-202: Ruff: F541 f-string without any placeholders.

🪛 GitHub Check: lint
cortex/source_builder.py

[failure] 479-479: Ruff (F541)
cortex/source_builder.py:479:22: F541 f-string without any placeholders


[failure] 464-464: Ruff (F541)
cortex/source_builder.py:464:22: F541 f-string without any placeholders


[failure] 461-461: Ruff (F541)
cortex/source_builder.py:461:26: F541 f-string without any placeholders


[failure] 444-444: Ruff (F541)
cortex/source_builder.py:444:22: F541 f-string without any placeholders


[failure] 202-202: Ruff (F541)
cortex/source_builder.py:202:22: F541 f-string without any placeholders

🪛 GitHub Check: Lint
cortex/source_builder.py

[failure] 479-479: Ruff (F541)
cortex/source_builder.py:479:22: F541 f-string without any placeholders


[failure] 464-464: Ruff (F541)
cortex/source_builder.py:464:22: F541 f-string without any placeholders


[failure] 461-461: Ruff (F541)
cortex/source_builder.py:461:26: F541 f-string without any placeholders


[failure] 444-444: Ruff (F541)
cortex/source_builder.py:444:22: F541 f-string without any placeholders


[failure] 202-202: Ruff (F541)
cortex/source_builder.py:202:22: F541 f-string without any placeholders

⏰ 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). (3)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.12)
🔇 Additional comments (15)
docs/SOURCE_BUILD.md (1)

1-14: LGTM! Comprehensive documentation for the new feature.

The overview clearly explains the source build feature's capabilities and aligns well with the implementation in cortex/source_builder.py.

tests/test_source_builder.py (6)

1-21: LGTM! Good test structure with proper imports.

The test file is well-organized with proper imports from the source module. The use of pytest and unittest.mock is appropriate for this test suite.


23-46: LGTM! Setup and cache key tests are well-designed.

The setup_method provides a clean fixture, and the cache key tests properly verify determinism (same inputs → same key) and uniqueness (different inputs → different keys).


48-82: LGTM! Dependency detection tests provide good coverage.

Tests properly verify that the correct dependencies are detected for each build system (autotools, cmake, python) and that already-satisfied dependencies are excluded from the result.


84-117: LGTM! Build system detection tests are thorough.

Good coverage of all detection scenarios with appropriate use of temporary directories and marker files to simulate different project types.


119-182: LGTM! Command generation tests verify expected output.

Tests properly verify that configure, build, and install commands are generated correctly for different build systems.


256-311: LGTM! Dataclass and constant tests are well-structured.

The TestBuildConfig tests verify default values and custom configuration, while TestBuildDependencies validates the expected structure of the BUILD_DEPENDENCIES constant.

cortex/source_builder.py (5)

1-48: LGTM! Module header, imports, and constants are well-organized.

Good use of type hints and proper organization of imports. The BUILD_DEPENDENCIES dictionary provides a clear structure for categorizing build dependencies.


50-76: LGTM! Dataclasses are well-designed with appropriate defaults.

BuildConfig and BuildResult provide clear data structures for the build workflow with sensible defaults and optional fields.


78-122: LGTM! Cache implementation is well-structured.

The cache key generation uses SHA256 for collision resistance, and the cache check/save logic with the "installed" marker file is a reasonable approach for tracking build state.


309-372: LGTM! Build and install command generation is correct.

The methods properly handle different build systems and use parallel jobs by default. The sudo prefix for install commands is appropriate.


374-518: LGTM overall! The build_from_source method provides a comprehensive workflow.

The orchestration logic is well-structured with proper error handling, caching support, and detailed BuildResult reporting. The method handles all phases (fetch, dependencies, configure, build, install) with appropriate timeouts and error messages.

Ensure all the f-string lint issues identified above are fixed to unblock the CI pipeline.

cortex/cli.py (3)

546-549: LGTM! Clean extension of the install method signature.

The new parameters from_source, source_url, and version are properly typed and have sensible defaults.


583-587: LGTM! Clean routing to source build path.

The early return pattern for from_source is clean and keeps the existing installation logic unaffected.


1989-1991: LGTM! Proper argument forwarding in main().

The new arguments are correctly retrieved with getattr and forwarded to cli.install().

Copy link
Contributor

@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

🧹 Nitpick comments (1)
cortex/source_builder.py (1)

227-243: Consider externalizing source URL mappings.

The hardcoded common_urls dictionary provides a good starting point for common packages, but could be moved to a configuration file or registry for easier maintenance and extensibility.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b3a5a3 and 1a0a730.

📒 Files selected for processing (1)
  • cortex/source_builder.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Follow PEP 8 style guide
Type hints required in Python code
Docstrings required for all public APIs

Files:

  • cortex/source_builder.py
🧬 Code graph analysis (1)
cortex/source_builder.py (3)
cortex/branding.py (1)
  • cx_print (49-69)
cortex/dependency_resolver.py (2)
  • DependencyResolver (40-386)
  • is_package_installed (101-104)
cortex/utils/commands.py (1)
  • CommandResult (118-125)
🪛 GitHub Actions: CI
cortex/source_builder.py

[error] 205-205: Command failed: ruff check . --output-format=github. Issue: F541 f-string without placeholders in cortex/source_builder.py:205.

🪛 GitHub Check: lint
cortex/source_builder.py

[failure] 482-482: Ruff (F541)
cortex/source_builder.py:482:22: F541 f-string without any placeholders


[failure] 467-467: Ruff (F541)
cortex/source_builder.py:467:22: F541 f-string without any placeholders


[failure] 464-464: Ruff (F541)
cortex/source_builder.py:464:26: F541 f-string without any placeholders


[failure] 447-447: Ruff (F541)
cortex/source_builder.py:447:22: F541 f-string without any placeholders


[failure] 205-205: Ruff (F541)
cortex/source_builder.py:205:22: F541 f-string without any placeholders

🪛 GitHub Check: Lint
cortex/source_builder.py

[failure] 482-482: Ruff (F541)
cortex/source_builder.py:482:22: F541 f-string without any placeholders


[failure] 467-467: Ruff (F541)
cortex/source_builder.py:467:22: F541 f-string without any placeholders


[failure] 464-464: Ruff (F541)
cortex/source_builder.py:464:26: F541 f-string without any placeholders


[failure] 447-447: Ruff (F541)
cortex/source_builder.py:447:22: F541 f-string without any placeholders


[failure] 205-205: Ruff (F541)
cortex/source_builder.py:205:22: F541 f-string without any placeholders

⏰ 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). (3)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
🔇 Additional comments (6)
cortex/source_builder.py (6)

53-79: LGTM! Well-structured data models.

The BuildConfig and BuildResult dataclasses are clean, properly typed, and well-documented. The default values are sensible and the fields clearly represent the build workflow state.


81-126: LGTM! Clean caching implementation.

The caching mechanism is well-designed with deterministic cache keys, metadata storage, and an "installed" marker for tracking build artifacts. The implementation follows best practices.


127-161: LGTM! Solid dependency detection logic.

The method correctly categorizes dependencies by build system type and checks installation status through the DependencyResolver. The pattern-based detection for package-specific dependencies is a good approach.


244-272: LGTM! Comprehensive build system detection.

The detection logic correctly prioritizes explicit build system markers (configure scripts, CMakeLists.txt, etc.) and provides a sensible default. The fallback to autotools is reasonable given its prevalence.


353-376: LGTM! Clean install command generation.

The method correctly generates install commands for each build system type, appropriately using sudo for system-level installation.


377-521: LGTM! Well-structured main build workflow.

The build_from_source method implements a comprehensive build pipeline with proper error handling, caching, and progress reporting. The method correctly:

  • Checks cache before rebuilding
  • Orchestrates fetch, configure, build, and install phases
  • Handles dependencies automatically
  • Returns structured BuildResult with detailed error information
  • Uses appropriate timeouts for long-running operations

The exception handling ensures failures are captured and reported gracefully.

Comment on lines +312 to +352
def build(self, source_dir: Path, config: BuildConfig) -> list[str]:
"""Build the package.
Args:
source_dir: Path to source code directory.
config: Build configuration with options and settings.
Returns:
List of build commands to execute.
"""
commands = []

if config.build_system == "autotools" or config.build_system == "make":
make_cmd = "make"
if config.make_args:
make_cmd += " " + " ".join(config.make_args)
else:
# Use parallel builds by default
import multiprocessing

jobs = multiprocessing.cpu_count()
make_cmd += f" -j{jobs}"
commands.append(make_cmd)

elif config.build_system == "cmake":
build_dir = source_dir / "build"
make_cmd = "make"
if config.make_args:
make_cmd += " " + " ".join(config.make_args)
else:
import multiprocessing

jobs = multiprocessing.cpu_count()
make_cmd += f" -j{jobs}"
commands.append(f"cd {build_dir} && {make_cmd}")

elif config.build_system == "python":
commands.append("python3 setup.py build")

return commands

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Move multiprocessing import to module level.

The import multiprocessing statement appears inside the method at lines 330 and 342, violating PEP 8 guidelines. Module-level imports improve code clarity and avoid repeated import overhead.

🔎 Proposed fix

At the top of the file, add the import with other standard library imports:

 import subprocess
 import tarfile
 import tempfile
+import multiprocessing
 import urllib.request
 import zipfile

Then update the method to remove the inline imports:

         if config.build_system == "autotools" or config.build_system == "make":
             make_cmd = "make"
             if config.make_args:
                 make_cmd += " " + " ".join(config.make_args)
             else:
                 # Use parallel builds by default
-                import multiprocessing
-
                 jobs = multiprocessing.cpu_count()
                 make_cmd += f" -j{jobs}"
             commands.append(make_cmd)
 
         elif config.build_system == "cmake":
             build_dir = source_dir / "build"
             make_cmd = "make"
             if config.make_args:
                 make_cmd += " " + " ".join(config.make_args)
             else:
-                import multiprocessing
-
                 jobs = multiprocessing.cpu_count()
                 make_cmd += f" -j{jobs}"

As per coding guidelines: Follow PEP 8 style guide.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cortex/source_builder.py around lines 312 to 352, there are inline imports of
multiprocessing at lines ~330 and ~342; move the "import multiprocessing"
statement to the module-level imports at the top of the file (grouped with other
standard library imports) and remove the two inline import statements inside the
build() method so the code simply references multiprocessing.cpu_count() where
needed, preserving the existing logic for computing jobs and constructing
make_cmd.

Copy link
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cortex/cli.py (1)

1-1: Fix Black formatting issues blocking CI.

The Black formatter check is failing in CI, indicating the file needs to be reformatted. This is blocking the pipeline and must be fixed before merge.

Run Black to fix formatting:

#!/bin/bash
# Fix Black formatting issues
black cortex/cli.py
♻️ Duplicate comments (10)
cortex/cli.py (1)

1747-1750: Fix the --version argument conflict in the install subparser.

The global --version flag (line 1708) uses action="version", which intercepts all --version invocations at the main parser level before subparser arguments are processed. Running cortex install --version 3.12 will print the cortex version and exit instead of passing the version argument to the install subparser.

Rename the install subparser's --version to --pkg-version or --package-version to avoid this conflict.

🔎 Rename to avoid conflict
     install_parser.add_argument(
-        "--version",
+        "--package-version",
         type=str,
-        help="Version to build (for --from-source)",
+        help="Package version to build (for --from-source)",
     )

Then update the main() function to use the new name:

         elif args.command == "install":
             return cli.install(
                 args.software,
                 execute=args.execute,
                 dry_run=args.dry_run,
                 parallel=args.parallel,
                 from_source=getattr(args, "from_source", False),
                 source_url=getattr(args, "source_url", None),
-                version=getattr(args, "version", None),
+                version=getattr(args, "package_version", None),
             )
cortex/source_builder.py (9)

333-333: Move multiprocessing import to module level.

The import multiprocessing statement appears inside the method, violating PEP 8 guidelines. Module-level imports improve code clarity and avoid repeated import overhead.

🔎 Move import to module level

At the top of the file (around line 23), add:

 import subprocess
 import tarfile
 import tempfile
+import multiprocessing
 import urllib.request
 import zipfile

Then remove the inline imports at lines 333 and 345:

             else:
                 # Use parallel builds by default
-                import multiprocessing
-
                 jobs = multiprocessing.cpu_count()
                 make_cmd += f" -j{jobs}"

As per coding guidelines: Follow PEP 8 style guide.


202-202: Network request lacks timeout and SSL verification control.

urllib.request.urlretrieve doesn't support timeout configuration and uses default SSL settings. Large files or unresponsive servers could hang indefinitely.

🔎 Use urllib.request.urlopen with timeout
-            urllib.request.urlretrieve(url, archive_path)
+            # Use urlopen with timeout for better control
+            import ssl
+            context = ssl.create_default_context()
+            request = urllib.request.Request(url, headers={"User-Agent": "Cortex-SourceBuilder/1.0"})
+            with urllib.request.urlopen(request, timeout=300, context=context) as response:
+                with open(archive_path, "wb") as f:
+                    # Read in chunks to handle large files
+                    while chunk := response.read(8192):
+                        f.write(chunk)

209-214: Security: Archive extraction without path validation (Zip Slip vulnerability).

Both tarfile.extractall() and zipfile.extractall() are vulnerable to path traversal attacks where malicious archives can write files outside the extraction directory.

🔎 Add path traversal protection
             if archive_path.suffix == ".gz" or archive_path.suffixes[-2:] == [".tar", ".gz"]:
                 with tarfile.open(archive_path, "r:gz") as tar:
-                    tar.extractall(extract_dir)
+                    # Validate paths to prevent Zip Slip
+                    for member in tar.getmembers():
+                        member_path = Path(extract_dir) / member.name
+                        try:
+                            member_path.resolve().relative_to(extract_dir.resolve())
+                        except ValueError:
+                            raise ValueError(f"Path traversal detected in archive: {member.name}")
+                    tar.extractall(extract_dir)
             elif archive_path.suffix == ".zip":
                 with zipfile.ZipFile(archive_path, "r") as zip_ref:
-                    zip_ref.extractall(extract_dir)
+                    # Validate paths to prevent Zip Slip
+                    for member in zip_ref.namelist():
+                        member_path = Path(extract_dir) / member
+                        try:
+                            member_path.resolve().relative_to(extract_dir.resolve())
+                        except ValueError:
+                            raise ValueError(f"Path traversal detected in archive: {member}")
+                    zip_ref.extractall(extract_dir)

295-295: --enable-optimizations is Python-specific, not generic autotools.

The --enable-optimizations flag is specific to Python's configure script and will cause errors for other autotools-based packages. This should only be added for known Python packages.

🔎 Make optimization flag conditional
             else:
                 # Default configure options
                 configure_cmd += f" --prefix={config.install_prefix}"
-                configure_cmd += " --enable-optimizations"
+                # --enable-optimizations is Python-specific
+                if config.package_name.lower() in ("python", "python3"):
+                    configure_cmd += " --enable-optimizations"
             commands.append(configure_cmd)

205-205: Fix f-string without placeholders (lint failure).

This f-string has no placeholders and is causing the CI pipeline to fail. Use a regular string instead.

🔎 Remove unnecessary f-prefix
-            cx_print(f"📦 Extracting source...", "info")
+            cx_print("📦 Extracting source...", "info")

450-450: Fix f-string without placeholders (lint failure).

This f-string has no placeholders and is causing CI lint failures.

🔎 Remove unnecessary f-prefix
-            cx_print(f"🔍 Checking build dependencies...", "info")
+            cx_print("🔍 Checking build dependencies...", "info")

467-467: Fix f-string without placeholders (lint failure).

This f-string has no placeholders.

🔎 Remove unnecessary f-prefix
-                cx_print(f"   ✓ All build dependencies satisfied", "success")
+                cx_print("   ✓ All build dependencies satisfied", "success")

470-470: Fix f-string without placeholders (lint failure).

This f-string has no placeholders.

🔎 Remove unnecessary f-prefix
-            cx_print(f"⚙️  Configuring build...", "info")
+            cx_print("⚙️  Configuring build...", "info")

485-485: Fix f-string without placeholders (lint failure).

This f-string has no placeholders.

🔎 Remove unnecessary f-prefix
-            cx_print(f"🔨 Compiling (this may take a while)...", "info")
+            cx_print("🔨 Compiling (this may take a while)...", "info")
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a0a730 and 813ea1f.

📒 Files selected for processing (2)
  • cortex/cli.py
  • cortex/source_builder.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Follow PEP 8 style guide
Type hints required in Python code
Docstrings required for all public APIs

Files:

  • cortex/source_builder.py
  • cortex/cli.py
🧠 Learnings (1)
📚 Learning: 2025-12-11T12:03:24.071Z
Learnt from: CR
Repo: cortexlinux/cortex PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T12:03:24.071Z
Learning: Applies to **/*install*.py : Implement audit logging to ~/.cortex/history.db for all package operations

Applied to files:

  • cortex/cli.py
🧬 Code graph analysis (1)
cortex/source_builder.py (3)
cortex/branding.py (1)
  • cx_print (49-69)
cortex/dependency_resolver.py (1)
  • is_package_installed (101-104)
cortex/utils/commands.py (1)
  • CommandResult (118-125)
🪛 GitHub Actions: CI
cortex/cli.py

[error] 1-1: Black formatting check failed. 1 file would be reformatted by 'black --check . --exclude "(venv|.venv|build|dist)"'. Exit code 1.

⏰ 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). (3)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
🔇 Additional comments (1)
cortex/cli.py (1)

1536-1543: Add type hints to method signature.

The method signature lacks type hints for parameters and return type, which is required by the coding guidelines for Python code.

🔎 Add type hints
 def _install_from_source(
     self,
-    package_name: str,
-    execute: bool,
-    dry_run: bool,
-    source_url: str | None,
-    version: str | None,
+    package_name: str,
+    execute: bool,
+    dry_run: bool,
+    source_url: str | None,
+    version: str | None,
 ) -> int:

As per coding guidelines: Type hints required in Python code.

Likely an incorrect or invalid review comment.

Comment on lines +1536 to +1614
def _install_from_source(
self,
package_name: str,
execute: bool,
dry_run: bool,
source_url: str | None,
version: str | None,
) -> int:
"""Handle installation from source."""
from cortex.source_builder import SourceBuilder

builder = SourceBuilder()

# Parse version from package name if specified (e.g., python@3.12)
if "@" in package_name and not version:
parts = package_name.split("@")
package_name = parts[0]
version = parts[1] if len(parts) > 1 else None

cx_print(f"Building {package_name} from source...", "info")
if version:
cx_print(f"Version: {version}", "info")

result = builder.build_from_source(
package_name=package_name,
version=version,
source_url=source_url,
use_cache=True,
)

if not result.success:
self._print_error(f"Build failed: {result.error_message}")
return 1

if result.cached:
cx_print(f"Using cached build for {package_name}", "info")

if dry_run:
cx_print("\nBuild commands (dry run):", "info")
for cmd in result.install_commands:
console.print(f" • {cmd}")
return 0

if not execute:
cx_print("\nBuild completed. Install commands:", "info")
for cmd in result.install_commands:
console.print(f" • {cmd}")
cx_print("Run with --execute to install", "info")
return 0

# Execute install commands
from cortex.coordinator import InstallationCoordinator, InstallationStep, StepStatus

def progress_callback(current: int, total: int, step: InstallationStep) -> None:
status_emoji = "⏳"
if step.status == StepStatus.SUCCESS:
status_emoji = "✅"
elif step.status == StepStatus.FAILED:
status_emoji = "❌"
console.print(f"[{current}/{total}] {status_emoji} {step.description}")

coordinator = InstallationCoordinator(
commands=result.install_commands,
descriptions=[f"Install {package_name}" for _ in result.install_commands],
timeout=600,
stop_on_error=True,
progress_callback=progress_callback,
)

install_result = coordinator.execute()

if install_result.success:
self._print_success(f"{package_name} built and installed successfully!")
return 0
else:
self._print_error("Installation failed")
if install_result.error_message:
console.print(f"Error: {install_result.error_message}", style="red")
return 1
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Implement audit logging for source-based installations.

The method bypasses the installation history tracking that is present in the regular install path (lines 579-616, 682-686, 743-746). Source-based installations should also be logged to ~/.cortex/history.db for consistency and traceability.

🔎 Add installation history tracking
 def _install_from_source(
     self,
     package_name: str,
     execute: bool,
     dry_run: bool,
     source_url: str | None,
     version: str | None,
 ) -> int:
     """Handle installation from source."""
     from cortex.source_builder import SourceBuilder
+    from datetime import datetime

     builder = SourceBuilder()
+    history = InstallationHistory()
+    install_id = None
+    start_time = datetime.now()

     # Parse version from package name if specified (e.g., python@3.12)
     if "@" in package_name and not version:
         parts = package_name.split("@")
         package_name = parts[0]
         version = parts[1] if len(parts) > 1 else None

     cx_print(f"Building {package_name} from source...", "info")
     if version:
         cx_print(f"Version: {version}", "info")

     result = builder.build_from_source(
         package_name=package_name,
         version=version,
         source_url=source_url,
         use_cache=True,
     )

     if not result.success:
         self._print_error(f"Build failed: {result.error_message}")
+        if install_id:
+            history.update_installation(install_id, InstallationStatus.FAILED, result.error_message)
         return 1

     if result.cached:
         cx_print(f"Using cached build for {package_name}", "info")

     if dry_run:
         cx_print("\nBuild commands (dry run):", "info")
         for cmd in result.install_commands:
             console.print(f"  • {cmd}")
+        if execute or dry_run:
+            install_id = history.record_installation(
+                InstallationType.INSTALL, [package_name], result.install_commands, start_time
+            )
+            history.update_installation(install_id, InstallationStatus.SUCCESS)
         return 0

     if not execute:
         cx_print("\nBuild completed. Install commands:", "info")
         for cmd in result.install_commands:
             console.print(f"  • {cmd}")
         cx_print("Run with --execute to install", "info")
         return 0

+    # Record installation start
+    install_id = history.record_installation(
+        InstallationType.INSTALL, [package_name], result.install_commands, start_time
+    )
+
     # Execute install commands
     from cortex.coordinator import InstallationCoordinator, InstallationStep, StepStatus

     def progress_callback(current: int, total: int, step: InstallationStep) -> None:
         status_emoji = "⏳"
         if step.status == StepStatus.SUCCESS:
             status_emoji = "✅"
         elif step.status == StepStatus.FAILED:
             status_emoji = "❌"
         console.print(f"[{current}/{total}] {status_emoji} {step.description}")

     coordinator = InstallationCoordinator(
         commands=result.install_commands,
         descriptions=[f"Install {package_name}" for _ in result.install_commands],
         timeout=600,
         stop_on_error=True,
         progress_callback=progress_callback,
     )

     install_result = coordinator.execute()

     if install_result.success:
         self._print_success(f"{package_name} built and installed successfully!")
+        if install_id:
+            history.update_installation(install_id, InstallationStatus.SUCCESS)
+            print(f"\n📝 Installation recorded (ID: {install_id})")
+            print(f"   To rollback: cortex rollback {install_id}")
         return 0
     else:
         self._print_error("Installation failed")
+        if install_id:
+            error_msg = install_result.error_message or "Installation failed"
+            history.update_installation(install_id, InstallationStatus.FAILED, error_msg)
+            print(f"\n📝 Installation recorded (ID: {install_id})")
+            print(f"   View details: cortex history {install_id}")
         if install_result.error_message:
             console.print(f"Error: {install_result.error_message}", style="red")
         return 1

Based on learnings: Implement audit logging to ~/.cortex/history.db for all package operations.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cortex/cli.py around lines 1536-1614, the source-install path skips writing
to the ~/.cortex/history.db audit log; update the flow to record installation
history just like the regular install paths: after a build succeeds and before
returning success, call the same history persistence routine used elsewhere (or
import and use the HistoryDB/HistoryStore helper) to insert an entry with
package_name, version, source_url, method="source", timestamp, result="success";
likewise, when installation fails (install_result.success is False) record an
entry with result="failed" and include the error_message; ensure entries are
written for dry-run/cached cases as appropriate using the same schema and
storage location (~/.cortex/history.db) and place the calls immediately before
the early return statements so audit logging always runs.

Copy link
Contributor

@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: 5

🧹 Nitpick comments (3)
cortex/source_builder.py (3)

290-298: Verify that ./configure script is executable.

Line 291 invokes ./configure directly, which will fail if the configure script doesn't have execute permissions. Some source archives ship configure without the executable bit set, requiring sh configure or bash configure instead.

🔎 Proposed fix
         if config.build_system == "autotools":
-            configure_cmd = "./configure"
+            # Use sh to invoke configure in case executable bit is not set
+            configure_cmd = "sh ./configure"
             if config.configure_args:
                 configure_cmd += " " + " ".join(config.configure_args)

Alternatively, check and set executable permissions first:

         if config.build_system == "autotools":
+            configure_script = source_dir / "configure"
+            if configure_script.exists():
+                configure_script.chmod(configure_script.stat().st_mode | 0o111)
             configure_cmd = "./configure"

353-354: Python build command uses deprecated setup.py interface.

Line 354 uses python3 setup.py build, which has been deprecated in favor of PEP 517/518 build tools. Modern Python packages may not support direct setup.py invocation.

🔎 Recommended alternatives

Replace with the modern build approach:

         elif config.build_system == "python":
-            commands.append("python3 setup.py build")
+            # Use modern Python build tools (PEP 517)
+            commands.append("python3 -m pip install --user build && python3 -m build")

Or for editable installs during development:

         elif config.build_system == "python":
-            commands.append("python3 setup.py build")
+            commands.append("python3 -m pip wheel --no-deps -w dist .")

377-378: Python install command uses deprecated setup.py interface.

Similar to the build command, line 378 uses the deprecated python3 setup.py install. Modern Python packaging should use pip for installation.

🔎 Recommended alternative
         elif config.build_system == "python":
-            commands.append("sudo python3 setup.py install")
+            commands.append("sudo python3 -m pip install .")
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 813ea1f and 0c6e062.

📒 Files selected for processing (1)
  • cortex/source_builder.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Follow PEP 8 style guide
Type hints required in Python code
Docstrings required for all public APIs

Files:

  • cortex/source_builder.py
🧬 Code graph analysis (1)
cortex/source_builder.py (3)
cortex/branding.py (1)
  • cx_print (49-69)
cortex/dependency_resolver.py (1)
  • is_package_installed (101-104)
cortex/utils/commands.py (1)
  • CommandResult (118-125)

Comment on lines +98 to +101
def _get_cache_key(self, package_name: str, version: str | None, source_url: str) -> str:
"""Generate a cache key for a build."""
key_data = f"{package_name}:{version}:{source_url}"
return hashlib.sha256(key_data.encode()).hexdigest()[:16]
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

CRITICAL: Cache key missing build configuration parameters.

The cache key is generated from only package_name, version, and source_url, but excludes configure_args and make_args. If a user builds the same package with different configuration options (e.g., different --prefix or optimization flags), they will incorrectly receive the cached build from the first configuration instead of a fresh build with the new options.

🔎 Proposed fix
-    def _get_cache_key(self, package_name: str, version: str | None, source_url: str) -> str:
-        """Generate a cache key for a build."""
-        key_data = f"{package_name}:{version}:{source_url}"
-        return hashlib.sha256(key_data.encode()).hexdigest()[:16]
+    def _get_cache_key(
+        self,
+        package_name: str,
+        version: str | None,
+        source_url: str,
+        configure_args: list[str] | None = None,
+        make_args: list[str] | None = None,
+        install_prefix: str = "/usr/local",
+    ) -> str:
+        """Generate a cache key for a build."""
+        # Include all configuration that affects build output
+        config_str = f"{configure_args or ''}:{make_args or ''}:{install_prefix}"
+        key_data = f"{package_name}:{version}:{source_url}:{config_str}"
+        return hashlib.sha256(key_data.encode()).hexdigest()[:16]

Then update the call site at line 415 to pass these parameters:

-                cache_key = self._get_cache_key(package_name, version, source_url)
+                cache_key = self._get_cache_key(
+                    package_name, version, source_url, configure_args, make_args, install_prefix
+                )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cortex/source_builder.py around lines 98 to 101, the cache key only uses
package_name, version, and source_url and omits configure_args and make_args,
causing different build configurations to collide; update the _get_cache_key
function to include normalized representations of configure_args and make_args
(e.g., join lists into a stable string, sort or preserve order as appropriate,
and treat None as an empty string) when composing key_data before hashing, and
then update the call site at line 415 to pass the configure_args and make_args
parameters into _get_cache_key so the cache key reflects build configuration.

Comment on lines +110 to +125
def _save_to_cache(self, cache_key: str, build_dir: Path, install_commands: list[str]) -> None:
"""Save build artifacts to cache."""
cache_path = self.cache_dir / cache_key
cache_path.mkdir(parents=True, exist_ok=True)

# Save metadata
metadata = {
"build_dir": str(build_dir),
"install_commands": install_commands,
"timestamp": str(Path(build_dir).stat().st_mtime),
}
with open(cache_path / "metadata.json", "w") as f:
json.dump(metadata, f, indent=2)

# Mark as installed
(cache_path / "installed").touch()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

MAJOR: Build cache stores only metadata, not artifacts.

The _save_to_cache method saves only metadata (build directory path, install commands, timestamp) but does not copy the actual build artifacts. Since build_dir points to a temporary directory created by tempfile.mkdtemp, the cached build becomes invalid once the temp directory is cleaned up by the system or manually removed.

🔎 Recommended approach

Either:

  1. Copy the built artifacts (compiled binaries, libraries) from build_dir to cache_path before saving metadata, or
  2. Copy the installed files from install_prefix to a staging area in cache_path, or
  3. Document that the cache only works for the current session and disable caching by default

Example for option 1:

     def _save_to_cache(self, cache_key: str, build_dir: Path, install_commands: list[str]) -> None:
         """Save build artifacts to cache."""
         cache_path = self.cache_dir / cache_key
         cache_path.mkdir(parents=True, exist_ok=True)
+
+        # Copy build artifacts to cache
+        artifacts_dir = cache_path / "artifacts"
+        shutil.copytree(build_dir, artifacts_dir, dirs_exist_ok=True)
 
         # Save metadata
         metadata = {
-            "build_dir": str(build_dir),
+            "build_dir": str(artifacts_dir),
             "install_commands": install_commands,
             "timestamp": str(Path(build_dir).stat().st_mtime),
         }

Comment on lines +185 to +225
def _fetch_from_url(self, url: str, package_name: str, version: str | None) -> Path:
"""Fetch source from a URL."""
temp_dir = Path(tempfile.mkdtemp(prefix=f"cortex-build-{package_name}-"))

try:
# Download
cx_print(f"📥 Downloading {package_name} source...", "info")
archive_path = temp_dir / "source.tar.gz"

if url.startswith("https://github.com/"):
# GitHub release or archive
if not url.endswith((".tar.gz", ".zip")):
if version:
url = f"{url}/archive/refs/tags/v{version}.tar.gz"
else:
url = f"{url}/archive/refs/heads/main.tar.gz"

urllib.request.urlretrieve(url, archive_path)

# Extract
cx_print("📦 Extracting source...", "info")
extract_dir = temp_dir / "extracted"
extract_dir.mkdir()

if archive_path.suffix == ".gz" or archive_path.suffixes[-2:] == [".tar", ".gz"]:
with tarfile.open(archive_path, "r:gz") as tar:
tar.extractall(extract_dir)
elif archive_path.suffix == ".zip":
with zipfile.ZipFile(archive_path, "r") as zip_ref:
zip_ref.extractall(extract_dir)

# Find the actual source directory (usually one level deep)
extracted_items = list(extract_dir.iterdir())
if len(extracted_items) == 1 and extracted_items[0].is_dir():
return extracted_items[0]
else:
return extract_dir

except Exception as e:
logger.exception(f"Failed to fetch source from {url}")
raise RuntimeError(f"Failed to fetch source: {e}")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

MAJOR: Temporary directory resource leak.

tempfile.mkdtemp at line 187 creates a temporary directory that is never explicitly cleaned up. The directory is returned to the caller and persists indefinitely, accumulating over multiple builds. The exception handler doesn't clean it up either.

🔎 Proposed fix

Consider one of these approaches:

Option 1: Store temp directories for cleanup later in the SourceBuilder class:

     def __init__(self):
         self.dependency_resolver = DependencyResolver()
         self.cache_dir = CACHE_DIR
         self.cache_dir.mkdir(parents=True, exist_ok=True)
+        self._temp_dirs = []  # Track temp dirs for cleanup
+
+    def cleanup(self):
+        """Clean up temporary directories."""
+        for temp_dir in self._temp_dirs:
+            if temp_dir.exists():
+                shutil.rmtree(temp_dir, ignore_errors=True)
+        self._temp_dirs.clear()

Then in _fetch_from_url:

     def _fetch_from_url(self, url: str, package_name: str, version: str | None) -> Path:
         """Fetch source from a URL."""
         temp_dir = Path(tempfile.mkdtemp(prefix=f"cortex-build-{package_name}-"))
+        self._temp_dirs.append(temp_dir)

Option 2: Use tempfile.TemporaryDirectory context manager at the build_from_source level and copy artifacts out before it's cleaned.

Option 3: Document that the build directory persists and provide a separate cleanup command.

Committable suggestion skipped: line range outside the PR's diff.

else:
cmake_cmd += f" -DCMAKE_INSTALL_PREFIX={config.install_prefix}"
cmake_cmd += " .."
commands.append(f"cd {build_dir} && {cmake_cmd}")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check run_command implementation
rg -n "def run_command" -A 20 cortex/utils/commands.py

Repository: cortexlinux/cortex

Length of output: 1502


🏁 Script executed:

#!/bin/bash
# Check the lines in question in cortex/source_builder.py
sed -n '305,315p' cortex/source_builder.py
sed -n '347,357p' cortex/source_builder.py
sed -n '371,381p' cortex/source_builder.py

Repository: cortexlinux/cortex

Length of output: 1138


🏁 Script executed:

#!/bin/bash
# Check where the commands list is used in source_builder.py
rg -n "run_command" cortex/source_builder.py | head -20

Repository: cortexlinux/cortex

Length of output: 389


🏁 Script executed:

#!/bin/bash
# Get broader context around line 309 to see the function and how commands are used
sed -n '280,330p' cortex/source_builder.py

Repository: cortexlinux/cortex

Length of output: 1804


🏁 Script executed:

#!/bin/bash
# Check the context around the run_command calls to understand which commands are being executed
sed -n '450,495p' cortex/source_builder.py

Repository: cortexlinux/cortex

Length of output: 2214


🏁 Script executed:

#!/bin/bash
# Search for calls to configure, build, install methods
rg -n "\.configure\(|\.build\(|\.install\(" cortex/source_builder.py

Repository: cortexlinux/cortex

Length of output: 125


🏁 Script executed:

#!/bin/bash
# Check the install method to see lines around 375 and how install commands are executed
sed -n '360,395p' cortex/source_builder.py

Repository: cortexlinux/cortex

Length of output: 1180


🏁 Script executed:

#!/bin/bash
# Check if there's an install phase that uses install_commands
rg -n "install_commands" cortex/source_builder.py -A 3 -B 3

Repository: cortexlinux/cortex

Length of output: 3045


🏁 Script executed:

#!/bin/bash
# Check if install_commands are executed anywhere
rg -n "for.*install_commands\|install_commands\[" cortex/source_builder.py

Repository: cortexlinux/cortex

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Search across the whole repo for where install_commands results are used
rg -n "install_commands" --type py | grep -E "\.install_commands|in.*install_commands|for.*in"

Repository: cortexlinux/cortex

Length of output: 845


🏁 Script executed:

#!/bin/bash
# Check how install_commands are executed in cli.py
sed -n '1570,1605p' cortex/cli.py

Repository: cortexlinux/cortex

Length of output: 1471


🏁 Script executed:

#!/bin/bash
# Check InstallationCoordinator to see how it executes commands
rg -n "class InstallationCoordinator" -A 50 cortex/coordinator.py | head -80

Repository: cortexlinux/cortex

Length of output: 2047


🏁 Script executed:

#!/bin/bash
# Check the execute method of InstallationCoordinator
rg -n "def execute" cortex/coordinator.py -A 20

Repository: cortexlinux/cortex

Length of output: 932


🏁 Script executed:

#!/bin/bash
# Check the _execute_command method
rg -n "def _execute_command" cortex/coordinator.py -A 15

Repository: cortexlinux/cortex

Length of output: 672


🏁 Script executed:

#!/bin/bash
# Get more of the _execute_command method
sed -n '162,200p' cortex/coordinator.py

Repository: cortexlinux/cortex

Length of output: 1585


Fix compound shell commands in configure and build phases to use cwd parameter instead.

Lines 309 and 351 construct commands like cd {build_dir} && {cmake_cmd} and cd {build_dir} && {make_cmd} that are executed via run_command(cmd, cwd=str(source_dir), timeout=...). Since run_command defaults to use_shell=False, the shell operator && will not work and the command will fail because cd is a shell built-in.

Use the cwd parameter to set the working directory instead:

# Instead of: cd {build_dir} && {cmake_cmd}
result = run_command(cmake_cmd, cwd=str(build_dir), timeout=...)

(Line 375 is not affected—install commands are executed via InstallationCoordinator, which uses shell=True.)

🤖 Prompt for AI Agents
cortex/source_builder.py around lines 309 and 351: the code currently builds
compound shell commands like "cd {build_dir} && {cmake_cmd}" and "cd {build_dir}
&& {make_cmd}" which will fail because run_command is invoked with
use_shell=False; remove the "cd ... &&" wrapper and call run_command with the
cwd parameter set to build_dir (e.g., run_command(cmake_cmd, cwd=str(build_dir),
timeout=...), and similarly for make_cmd), and ensure the command argument
format matches run_command's expectations (list form if use_shell=False) while
preserving timeout and other options.

Comment on lines +417 to +430
if cached_path:
cx_print(f"📦 Using cached build for {package_name}", "info")
metadata_path = cached_path / "metadata.json"
if metadata_path.exists():
with open(metadata_path) as f:
metadata = json.load(f)
return BuildResult(
success=True,
package_name=package_name,
version=version,
build_dir=str(cached_path),
install_commands=metadata.get("install_commands", []),
cached=True,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

MAJOR: Cached builds return success without verifying installation.

When a cached build is found (lines 417-430), the method returns BuildResult with success=True and cached=True, but it does not execute the install_commands. This creates ambiguity:

  1. The package may not be installed on the system (install commands were never run)
  2. The "installed" marker in the cache (line 106) only indicates the build completed, not that the package is installed system-wide
  3. Users might assume the package is ready to use when it's not
🔎 Recommended approaches

Option 1: Execute install commands even for cached builds:

                 if cached_path:
                     cx_print(f"📦 Using cached build for {package_name}", "info")
                     metadata_path = cached_path / "metadata.json"
                     if metadata_path.exists():
                         with open(metadata_path) as f:
                             metadata = json.load(f)
+                            # Execute install commands from cache
+                            install_commands = metadata.get("install_commands", [])
+                            for cmd in install_commands:
+                                result = run_command(cmd, timeout=600)
+                                if not result.success:
+                                    return BuildResult(
+                                        success=False,
+                                        package_name=package_name,
+                                        version=version,
+                                        build_dir=str(cached_path),
+                                        install_commands=install_commands,
+                                        error_message=f"Install failed: {result.stderr}",
+                                    )
                             return BuildResult(
                                 success=True,

Option 2: Check if the package is actually installed on the system before returning cached result:

                 if cached_path:
+                    # Verify package is actually installed on the system
+                    if not self.dependency_resolver.is_package_installed(package_name):
+                        cx_print(f"📦 Cached build found but not installed, reinstalling...", "info")
+                        # Fall through to normal build flow
+                    else:
                         cx_print(f"📦 Using cached build for {package_name}", "info")

Option 3: Document clearly in the docstring and return message that cached builds still require manual installation.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cortex/source_builder.py around lines 417-430, the current early return for
cached builds marks success=True and cached=True without running
install_commands or verifying the package is installed; change this so that
after loading metadata from metadata.json you either (preferred) execute the
metadata.get("install_commands", []) and only return success=True if those
commands complete successfully (capture and log errors and return success=False
on failure), or (alternative) run a system-level check to verify the package is
actually installed before returning success; ensure cached=True is still set for
a cache hit but only mark success when installation has been validated or
commands ran successfully, and propagate any installation errors in the
BuildResult.

Copy link
Collaborator

@Anshgrover23 Anshgrover23 left a comment

Choose a reason for hiding this comment

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

@aybanda Please follow the updated contribution guidelines. We’ve added new rules around AI usage in CONTRIBUTING.md. 2e68b29
Also, CI checks failing.

@aybanda aybanda force-pushed the feature/source-build-from-source branch from ba60781 to 0889097 Compare January 4, 2026 06:53
Copy link
Contributor

@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: 2

♻️ Duplicate comments (2)
cortex/cli.py (2)

1549-1627: Missing audit logging for source-based installations.

This method bypasses the installation history tracking present in the regular install path. Source-based installations should be logged to ~/.cortex/history.db for consistency and traceability, including:

  • Recording installation start with history.record_installation()
  • Updating status on success/failure with history.update_installation()
  • Tracking build failures, dry-run executions, and successful installs

Based on learnings: Implement audit logging to ~/.cortex/history.db for all package operations.


1749-1763: Fix the --version argument conflict.

The global --version flag (line 1721) uses action="version", which intercepts all --version invocations before subparser arguments are processed. Running cortex install --version 3.12 will print the cortex version and exit instead of passing the version to the source builder.

Rename the install subparser's --version to --pkg-version or --package-version to avoid this conflict.

🔎 Proposed fix
     install_parser.add_argument(
-        "--version",
+        "--pkg-version",
         type=str,
-        help="Version to build (for --from-source)",
+        help="Package version to build (for --from-source)",
     )

Then update line 2007:

                 from_source=getattr(args, "from_source", False),
                 source_url=getattr(args, "source_url", None),
-                version=getattr(args, "version", None),
+                version=getattr(args, "pkg_version", None),
             )
🧹 Nitpick comments (2)
cortex/cli.py (2)

1549-1558: Expand docstring to meet PEP 257 requirements.

The docstring should document parameters, return values, and potential exceptions. Consider this format:

     def _install_from_source(
         self,
         package_name: str,
         execute: bool,
         dry_run: bool,
         source_url: str | None,
         version: str | None,
     ) -> int:
-        """Handle installation from source."""
+        """Handle installation from source.
+
+        Args:
+            package_name: Name of package to build (may include @version suffix)
+            execute: Whether to execute installation commands
+            dry_run: Whether to show commands without building/installing
+            source_url: Optional URL to source code tarball or repository
+            version: Optional version to build (overrides @version in package_name)
+
+        Returns:
+            Exit code (0 for success, 1 for failure)
+        """
         from cortex.source_builder import SourceBuilder

As per coding guidelines: Docstrings required for all public APIs.


1613-1613: Consider making the timeout configurable.

The 600-second (10-minute) timeout is hardcoded for source builds. Complex builds (e.g., large C++ projects, LLVM) may require longer timeouts. Consider either:

  • Adding a --timeout CLI argument for source builds
  • Using a larger default timeout for source builds (e.g., 1800s/30min)
  • Documenting the timeout limitation in SOURCE_BUILD.md
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c6e062 and 0889097.

📒 Files selected for processing (1)
  • cortex/cli.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Follow PEP 8 style guide
Type hints required in Python code
Docstrings required for all public APIs

Files:

  • cortex/cli.py
🧠 Learnings (1)
📚 Learning: 2025-12-11T12:03:24.071Z
Learnt from: CR
Repo: cortexlinux/cortex PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T12:03:24.071Z
Learning: Applies to **/*install*.py : Implement audit logging to ~/.cortex/history.db for all package operations

Applied to files:

  • cortex/cli.py
🪛 GitHub Actions: CI
cortex/cli.py

[error] 1-1: Black formatting check failed. 1 file would be reformatted. Command failed: 'black --check . --exclude "(venv|.venv|build|dist)"'. Please run 'black .' to format the code.

⏰ 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). (3)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
🔇 Additional comments (2)
cortex/cli.py (2)

560-562: LGTM: Parameter additions are well-typed.

The new parameters for source-based installation are correctly typed with appropriate defaults and follow the Python 3.10+ union syntax used throughout the codebase.


597-601: LGTM: Clean separation of source-based installation path.

The early return appropriately separates the source-build flow from the LLM-based installation path, forwarding all necessary parameters.

Comment on lines +891 to +892
f"{r.id:<18} {date:<20} {r.operation_type.value:<12} "
f"{packages:<30} {r.status.value:<15}"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Apply Black formatting to resolve pipeline failure.

The CI pipeline reports a Black formatting check failure. Run black . to auto-format the code before merging.

#!/bin/bash
# Verify Black formatting issues
black --check cortex/cli.py --diff
🤖 Prompt for AI Agents
In cortex/cli.py around lines 891-892, the CI failed Black formatting on the
f-string lines; run Black to reformat the file and commit the changes (e.g., run
`black .` or `black cortex/cli.py`), then re-run the formatting check to ensure
the f-string line breaks and spacing conform to Black before pushing the updated
file.

Comment on lines +2005 to +2007
from_source=getattr(args, "from_source", False),
source_url=getattr(args, "source_url", None),
version=getattr(args, "version", None),
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Update parameter name when --version is renamed.

Once the install subparser's --version argument is renamed to --pkg-version (see comment on lines 1749-1763), update line 2007 accordingly:

                 from_source=getattr(args, "from_source", False),
                 source_url=getattr(args, "source_url", None),
-                version=getattr(args, "version", None),
+                version=getattr(args, "pkg_version", None),
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from_source=getattr(args, "from_source", False),
source_url=getattr(args, "source_url", None),
version=getattr(args, "version", None),
from_source=getattr(args, "from_source", False),
source_url=getattr(args, "source_url", None),
version=getattr(args, "pkg_version", None),
🤖 Prompt for AI Agents
In cortex/cli.py around lines 2005 to 2007, the call is still using
getattr(args, "version", None) but the install subparser renamed the CLI flag to
--pkg-version; update this line to use getattr(args, "pkg_version", None) (and
keep from_source and source_url as-is) so the code reads the new parameter name
from args.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 4, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
2 Security Hotspots

See analysis details on SonarQube Cloud

@aybanda aybanda closed this Jan 4, 2026
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.

Package Build from Source

2 participants