Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b931951
feat: add package build from source (#101)
aybanda Jan 4, 2026
88b50fa
fix: address CodeRabbit review issues
aybanda Jan 4, 2026
c7cea00
fix: apply black formatting to cli.py
aybanda Jan 4, 2026
5ed2a4b
fix: use Python 3.10+ compatible tarfile extraction
aybanda Jan 4, 2026
0bb1b85
fix: update test to handle members parameter in tarfile.extractall
aybanda Jan 4, 2026
23790fe
fix: improve test mock to handle Path objects correctly
aybanda Jan 4, 2026
8e2439d
fix: improve test_fetch_from_url_tarball mock setup
aybanda Jan 4, 2026
4ae5dbc
fix: update tests for new install method signature and improve fetch …
aybanda Jan 4, 2026
7e6687a
fix: update test_cli_extended.py and improve test_build_from_source_s…
aybanda Jan 4, 2026
bac29c2
fix: resolve command validation failures in configure_build
aybanda Jan 4, 2026
e75f2b2
fix: sanitize make_args in autotools/make builds
aybanda Jan 4, 2026
882d4a9
fix: update test_build_cmake to handle tuple return format
aybanda Jan 4, 2026
38462e0
fix: correct test_build_cmake assertion for tuple format
aybanda Jan 4, 2026
fdd96d9
fix: add error cleanup for temp directory and fix CodeQL workflow
aybanda Jan 4, 2026
a471a6a
fix: remove custom queries from CodeQL workflow to avoid default setu…
aybanda Jan 4, 2026
7a6f6e8
Merge branch 'main' into feature/package-build-from-source--101
Anshgrover23 Jan 7, 2026
8b23ce8
feat: add audit logging and SSRF protection to source build
aybanda Jan 7, 2026
dbb3aea
fix: resolve YAML syntax error in spam-protection workflow
aybanda Jan 7, 2026
cdd5e0f
Resolved conflicts in .github/workflows/codeql.
aybanda Jan 9, 2026
bd4027b
docs: add comprehensive docstring to _install_from_source method
aybanda Jan 9, 2026
84d8425
Merge branch 'main' into feature/package-build-from-source--101
Anshgrover23 Jan 11, 2026
d3740b4
Merge branch 'main' into feature/package-build-from-source--101
Anshgrover23 Jan 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/codeql.yml
Copy link
Collaborator

Choose a reason for hiding this comment

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

No need to change this file.

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
languages: python
queries: +security-extended,security-and-quality

- name: Autobuild
uses: github/codeql-action/autobuild@v4

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
Expand Down
19 changes: 9 additions & 10 deletions .github/workflows/spam-protection.yml
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why this one changed ?

Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,18 @@ jobs:
labels: ['needs-triage']
});

const commentBody = '🔍 **Auto-Review Notice**\n\n' +
'This PR was flagged for: ' + flags.join(', ') + '\n\n' +
'Please ensure:\n' +
'- [ ] PR description explains the changes\n' +
'- [ ] CLA is signed\n' +
'- [ ] Changes are tested\n\n' +
'A maintainer will review shortly.';

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: '🔍 **Auto-Review Notice**

This PR was flagged for: ' + flags.join(', ') + '

Please ensure:
- [ ] PR description explains the changes
- [ ] CLA is signed
- [ ] Changes are tested

A maintainer will review shortly.'
body: commentBody
Comment on lines +65 to +77
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

Unrelated change in a feature PR.

This refactoring of the spam protection workflow appears unrelated to the PR's stated objectives (adding package build from source functionality). While the code change itself is harmless and slightly improves readability, including unrelated changes makes PRs harder to review and understand.

Consider keeping PRs focused on a single concern. This workflow refactor could either be:

  • Removed from this PR, or
  • Submitted separately if the refactoring is desired
Optional: Consider template literals for cleaner string construction

If you do keep this change, you could use ES6 template literals for better readability:

-              const commentBody = '🔍 **Auto-Review Notice**\n\n' +
-                'This PR was flagged for: ' + flags.join(', ') + '\n\n' +
-                'Please ensure:\n' +
-                '- [ ] PR description explains the changes\n' +
-                '- [ ] CLA is signed\n' +
-                '- [ ] Changes are tested\n\n' +
-                'A maintainer will review shortly.';
+              const commentBody = `🔍 **Auto-Review Notice**
+
+This PR was flagged for: ${flags.join(', ')}
+
+Please ensure:
+- [ ] PR description explains the changes
+- [ ] CLA is signed
+- [ ] Changes are tested
+
+A maintainer will review shortly.`;
📝 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
const commentBody = '🔍 **Auto-Review Notice**\n\n' +
'This PR was flagged for: ' + flags.join(', ') + '\n\n' +
'Please ensure:\n' +
'- [ ] PR description explains the changes\n' +
'- [ ] CLA is signed\n' +
'- [ ] Changes are tested\n\n' +
'A maintainer will review shortly.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: '🔍 **Auto-Review Notice**
This PR was flagged for: ' + flags.join(', ') + '
Please ensure:
- [ ] PR description explains the changes
- [ ] CLA is signed
- [ ] Changes are tested
A maintainer will review shortly.'
body: commentBody
const commentBody = `🔍 **Auto-Review Notice**
This PR was flagged for: ${flags.join(', ')}
Please ensure:
- [ ] PR description explains the changes
- [ ] CLA is signed
- [ ] Changes are tested
A maintainer will review shortly.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: commentBody
🤖 Prompt for AI Agents
In @.github/workflows/spam-protection.yml around lines 65 - 77, You introduced
an unrelated refactor to the spam-protection workflow by changing how
commentBody is built before calling github.rest.issues.createComment (variables:
commentBody, flags, pr.number); either remove this change from the current PR so
the feature PR stays focused, or extract the workflow change into its own
separate PR; if you decide to keep it here, simplify the string assembly by
using a template literal that interpolates flags and preserves the checklist and
context, and ensure the call to github.rest.issues.createComment still uses
issue_number: pr.number and the same body variable.

});
}
232 changes: 228 additions & 4 deletions cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

logger = logging.getLogger(__name__)

from cortex.api_key_detector import auto_detect_api_key, setup_api_key
from cortex.ask import AskHandler
from cortex.branding import VERSION, console, cx_header, cx_print, show_banner
Expand Down Expand Up @@ -224,8 +226,9 @@ def notify(self, args):

elif args.notify_action == "enable":
mgr.config["enabled"] = True
# Addressing CodeRabbit feedback: Ideally should use a public method instead of private _save_config,
# but keeping as is for a simple fix (or adding a save method to NotificationManager would be best).
# Addressing CodeRabbit feedback: Ideally should use a public method
# instead of private _save_config, but keeping as is for a simple fix
# (or adding a save method to NotificationManager would be best).
mgr._save_config()
self._print_success("Notifications enabled")
return 0
Expand Down Expand Up @@ -638,6 +641,9 @@ def install(
execute: bool = False,
dry_run: bool = False,
parallel: bool = False,
from_source: bool = False,
source_url: str | None = None,
version: str | None = None,
):
# Validate input first
is_valid, error = validate_install_request(software)
Expand Down Expand Up @@ -672,6 +678,10 @@ def install(
start_time = datetime.now()

try:
# Handle --from-source flag
if from_source:
return self._install_from_source(software, execute, dry_run, source_url, version)

self._print_status("🧠", "Understanding request...")

interpreter = CommandInterpreter(api_key=api_key, provider=provider)
Expand Down Expand Up @@ -960,7 +970,8 @@ def history(self, limit: int = 20, status: str | None = None, show_id: str | Non
packages += f" +{len(r.packages) - 2}"

print(
f"{r.id:<18} {date:<20} {r.operation_type.value:<12} {packages:<30} {r.status.value:<15}"
f"{r.id:<18} {date:<20} {r.operation_type.value:<12} "
f"{packages:<30} {r.status.value:<15}"
)

return 0
Expand Down Expand Up @@ -1281,7 +1292,8 @@ def _env_template(self, env_mgr: EnvironmentManager, args: argparse.Namespace) -
return self._env_template_apply(env_mgr, args)
else:
self._print_error(
"Please specify: template list, template show <name>, or template apply <name> <app>"
"Please specify: template list, template show <name>, "
"or template apply <name> <app>"
)
return 1

Expand Down Expand Up @@ -2001,6 +2013,200 @@ def progress_callback(current: int, total: int, step: InstallationStep) -> None:
console.print(f"Error: {result.error_message}", style="red")
return 1

def _install_from_source(
self,
package_name: str,
execute: bool,
dry_run: bool,
source_url: str | None,
version: str | None,
) -> int:
Comment on lines +2016 to +2023
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 9, 2026

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add comprehensive docstring for this method.

The _install_from_source() method lacks a docstring. Per coding guidelines, docstrings are required for all public APIs, and this method is a significant entry point for source-build functionality.

📝 Suggested docstring
     def _install_from_source(
         self,
         package_name: str,
         execute: bool,
         dry_run: bool,
         source_url: str | None,
         version: str | None,
     ) -> int:
+        """Build and install a package from source.
+
+        Handles the complete source build workflow including dependency detection,
+        build system detection, compilation, and installation. Integrates with
+        InstallationHistory for audit logging.
+
+        Args:
+            package_name: Name of the package to build (supports package@version syntax)
+            execute: Execute installation commands after building
+            dry_run: Show commands without executing
+            source_url: Optional URL to source code (tarball, GitHub, etc.)
+            version: Optional version to build (can also be specified via package@version)
+
+        Returns:
+            int: Exit code (0 for success, 1 for failure)
+        """
📝 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 _install_from_source(
self,
package_name: str,
execute: bool,
dry_run: bool,
source_url: str | None,
version: str | None,
) -> int:
def _install_from_source(
self,
package_name: str,
execute: bool,
dry_run: bool,
source_url: str | None,
version: str | None,
) -> int:
"""Build and install a package from source.
Handles the complete source build workflow including dependency detection,
build system detection, compilation, and installation. Integrates with
InstallationHistory for audit logging.
Args:
package_name: Name of the package to build (supports package@version syntax)
execute: Execute installation commands after building
dry_run: Show commands without executing
source_url: Optional URL to source code (tarball, GitHub, etc.)
version: Optional version to build (can also be specified via package@version)
Returns:
int: Exit code (0 for success, 1 for failure)
"""
🤖 Prompt for AI Agents
In @cortex/cli.py around lines 2016 - 2023, Add a comprehensive docstring to the
_install_from_source method: describe its purpose (installing a package from a
source URL), document all parameters (package_name: str, execute: bool, dry_run:
bool, source_url: str | None, version: str | None), explain return value (int
status/exit code), note side effects (invokes build/install, may execute
commands), and list raised exceptions or error cases and any special behavior
for dry_run vs execute; place the docstring immediately below the def
_install_from_source(...) signature in cortex/cli.py using the project’s
docstring style.

✅ Addressed in commit bd4027b

Copy link
Collaborator

Choose a reason for hiding this comment

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

@aybanda Address this one.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

"""Install a package from a source URL by building and optionally installing it.

This method handles the complete workflow for installing packages from source code:
parsing version information, building the package, and optionally executing
installation commands. It supports dry-run mode for previewing operations and
records all activities in the installation history for audit purposes.

Args:
package_name: Name of the package to install. If version is specified
using "@" syntax (e.g., "python@3.12"), it will be parsed automatically
if version parameter is None.
execute: If True, executes the installation commands after building.
If False, only builds the package and displays commands without executing.
dry_run: If True, performs a dry run showing what commands would be executed
without actually building or installing. Takes precedence over execute.
source_url: Optional URL to the source code repository or tarball.
If None, the SourceBuilder will attempt to locate the source automatically.
version: Optional version string to build. If None and package_name contains
"@", the version will be extracted from package_name.

Returns:
int: Exit status code. Returns 0 on success (build/install completed or
dry-run completed), 1 on failure (build failed or installation failed).

Side Effects:
- Invokes SourceBuilder.build_from_source() to build the package
- May execute installation commands via InstallationCoordinator if execute=True
- Records installation start, progress, and completion in InstallationHistory
- Prints status messages and progress to console
- May use cached builds if available

Raises:
No exceptions are raised directly, but underlying operations may fail:
- SourceBuilder.build_from_source() failures are caught and returned as status 1
- InstallationCoordinator.execute() failures are caught and returned as status 1
- InstallationHistory exceptions are caught and logged as warnings

Special Behavior:
- dry_run=True: Shows build/install commands without executing any operations.
Returns 0 after displaying commands. Installation history is still recorded.
- execute=False, dry_run=False: Builds the package and displays install commands
but does not execute them. Returns 0. User is prompted to run with --execute.
- execute=True, dry_run=False: Builds the package and executes all installation
commands. Returns 0 on success, 1 on failure.
- Version parsing: If package_name contains "@" (e.g., "python@3.12") and version
is None, the version is automatically extracted and package_name is updated.
- Caching: Uses cached builds when available, printing a notification if cache
is used.
"""
from cortex.source_builder import SourceBuilder

# Initialize history for audit logging (same as install() method)
history = InstallationHistory()
install_id = None
start_time = datetime.now()

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 and parts[1] else None

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

# Prepare commands list for history recording
# Include source URL in the commands list to track it
commands = []
if source_url:
commands.append(f"Source URL: {source_url}")
commands.append(f"Build from source: {package_name}")
if version:
commands.append(f"Version: {version}")

Comment on lines +2093 to +2100
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

Fix commands list to contain only executable commands.

The commands list includes metadata strings like "Source URL: ...", "Build from source: ...", and "Version: ...". These are not executable shell commands and will cause issues:

  • InstallationHistory._extract_packages_from_commands() expects actual commands
  • Rollback functionality may fail or produce incorrect results
  • History display will show non-commands mixed with real commands

Store metadata separately or in a structured format rather than mixing it with executable commands.

Suggested fix
     # Prepare commands list for history recording
-    # Include source URL in the commands list to track it
     commands = []
-    if source_url:
-        commands.append(f"Source URL: {source_url}")
-    commands.append(f"Build from source: {package_name}")
-    if version:
-        commands.append(f"Version: {version}")
+    # Commands will be populated after build with actual install commands
+    # Metadata (source_url, version) could be stored in a separate field if InstallationHistory supports it

Then after line 2138:

-    # Add install commands to the commands list for history
-    commands.extend(result.install_commands)
+    # Use actual install commands from build result
+    commands = result.install_commands

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

# Record installation start
if execute or dry_run:
try:
install_id = history.record_installation(
InstallationType.INSTALL,
[package_name],
commands,
start_time,
)
except Exception as e:
logger.warning(f"Failed to record installation start: {e}")
Comment on lines +2110 to +2111
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

Consider surfacing history recording failures to the user.

History recording failures are caught with broad Exception handling and only logged as warnings (also at lines 2148-2149, 2161-2162, 2193, 2206-2207). Users won't see these failures in CLI output, leading to silent audit logging failures.

While failing the entire operation on history errors may be too strict, consider:

  • Displaying a warning to the user via cx_print(..., "warning")
  • Tracking whether history logging succeeded and mentioning it in the final success/failure message

Based on learnings, audit logging to ~/.cortex/history.db is a required feature for all package operations.


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}")
# Record failed installation
if install_id:
try:
history.update_installation(
install_id,
InstallationStatus.FAILED,
error_message=result.error_message or "Build failed",
)
except Exception as e:
logger.warning(f"Failed to update installation record: {e}")
return 1

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

# Add install commands to the commands list for history
commands.extend(result.install_commands)

if dry_run:
cx_print("\nBuild commands (dry run):", "info")
for cmd in result.install_commands:
console.print(f" • {cmd}")
# Record successful dry run
if install_id:
try:
history.update_installation(install_id, InstallationStatus.SUCCESS)
except Exception as e:
logger.warning(f"Failed to update installation record: {e}")
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")
# Record successful build (but not installed)
if install_id:
try:
history.update_installation(install_id, InstallationStatus.SUCCESS)
except Exception as e:
logger.warning(f"Failed to update installation record: {e}")
return 0

# Execute install commands
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!")
# Record successful installation
if install_id:
try:
history.update_installation(install_id, InstallationStatus.SUCCESS)
console.print(f"\n📝 Installation recorded (ID: {install_id})")
console.print(f" To rollback: cortex rollback {install_id}")
except Exception as e:
logger.warning(f"Failed to update installation record: {e}")
return 0
else:
self._print_error("Installation failed")
error_msg = install_result.error_message or "Installation failed"
if install_result.error_message:
console.print(f"Error: {error_msg}", style="red")
# Record failed installation
if install_id:
try:
history.update_installation(
install_id, InstallationStatus.FAILED, error_message=error_msg
)
except Exception as e:
logger.warning(f"Failed to update installation record: {e}")
return 1

# --------------------------


Expand Down Expand Up @@ -2144,6 +2350,21 @@ def main():
action="store_true",
help="Enable parallel execution for multi-step installs",
)
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(
"--pkg-version",
type=str,
help="Version to build (for --from-source)",
)

# Import command - import dependencies from package manager files
import_parser = subparsers.add_parser(
Expand Down Expand Up @@ -2510,6 +2731,9 @@ def main():
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, "pkg_version", None),
)
elif args.command == "import":
return cli.import_deps(args)
Expand Down
Loading
Loading