Skip to content

Add package conflict resolver#72

Open
dnesdan wants to merge 5 commits into
cxlinux-ai:mainfrom
dnesdan:codex/package-conflict-resolver-45
Open

Add package conflict resolver#72
dnesdan wants to merge 5 commits into
cxlinux-ai:mainfrom
dnesdan:codex/package-conflict-resolver-45

Conversation

@dnesdan
Copy link
Copy Markdown

@dnesdan dnesdan commented May 20, 2026

Adds a small conflict resolver for planned package installs.

What changed:

  • new apt/scripts/resolve-conflicts.py helper
  • detects APT Conflicts/Breaks and optional CX json rules
  • prints simple options and can save a selected package preference
  • added --interactive prompt for choosing the preferred package
  • cx wrapper exposes cx resolve-conflicts ...
  • cx install with multiple packages runs the check before handing off
  • docs and a focused shell test

Checks run:

  • python3 -m py_compile apt/scripts/resolve-conflicts.py
  • bash -n tests/resolve-conflicts-test.sh
  • make test-conflicts
  • git diff --check
  • git diff --cached --check

Closes #45

Summary by CodeRabbit

  • New Features

    • Added package conflict detection and resolution with interactive conflict selection and persistent preference storage.
    • Automatic preflight conflict checks integrated into package installation workflow.
    • New resolve-conflicts command available in the CLI.
  • Documentation

    • Added comprehensive documentation covering conflict detection, resolution options, and practical usage examples.

Review Change Stack

@dnesdan dnesdan requested a review from a team as a code owner May 20, 2026 11:20
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Warning

Rate limit exceeded

@dnesdan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c35d91dd-4db5-4551-bca8-6bd6c0cbf690

📥 Commits

Reviewing files that changed from the base of the PR and between a00dd52 and 4a3c4b0.

📒 Files selected for processing (3)
  • apt/scripts/resolve-conflicts.py
  • packages/cx-core/debian/rules
  • tests/resolve-conflicts-test.sh
📝 Walkthrough

Walkthrough

Adds a standalone resolver CLI (apt/scripts/resolve-conflicts.py) that detects APT Conflicts/Breaks and optional JSON rules, persists user “keep” choices, integrates into the cx wrapper and Debian package, includes docs, and adds a Makefile test target plus end-to-end tests.

Changes

Package Conflict Resolver

Layer / File(s) Summary
Core parsing and conflict detection
apt/scripts/resolve-conflicts.py (lines 1–102)
Defines Conflict dataclass and deterministic key, parses APT relation fields, implements read_apt_fields() and detect_apt_conflicts() to build de-duplicated Conflict records.
Rule loading and preference storage
apt/scripts/resolve-conflicts.py (lines 104–173)
Loads optional JSON rule conflicts filtered to the requested package set (load_rule_conflicts()), and implements load_preferences(), save_preference(), save_preferences() for persistent choice storage.
CLI orchestration, output, and interactive prompts
apt/scripts/resolve-conflicts.py (lines 175–302)
Formats human/JSON output (conflict_to_dict(), print_human()), supports interactive prompt saving (prompt_for_choices()), resolves default preferences path, parses CLI args, and orchestrates main() with exit codes 0/1/2 and main entrypoint.
End-to-end and robustness tests
tests/resolve-conflicts-test.sh (lines 1–99)
Integration tests covering conflict detection for docker.io vs podman, --choice persistence, --json output, interactive prompt, malformed/missing rules handling, and a non-conflict check.
Packaging, wrapper dispatch, README, Makefile
packages/cx-core/debian/rules, apt/README.md, Makefile
Installs resolve-conflicts.py to /usr/share/cx/, updates /usr/bin/cx wrapper to dispatch cx resolve-conflicts and certain cx install invocations to the resolver, documents usage and rules in apt/README.md, and adds make test-conflicts target to run tests.

Sequence Diagram

sequenceDiagram
  participant User
  participant Resolver as resolve-conflicts.py
  participant APT as apt-cache
  participant RulesFile as Rules JSON
  participant PrefsFile as Preferences JSON

  User->>Resolver: resolve-conflicts docker.io podman --rules rules.json
  Resolver->>APT: apt-cache show docker.io
  APT-->>Resolver: Conflicts/Breaks fields
  Resolver->>APT: apt-cache show podman
  APT-->>Resolver: Conflicts/Breaks fields
  Resolver->>RulesFile: load rules.json (optional)
  RulesFile-->>Resolver: rule conflicts
  Resolver->>PrefsFile: load saved preferences
  PrefsFile-->>Resolver: previous choices (if any)
  Resolver-->>User: display conflicts and options (exit 1)
  User->>Resolver: --choice podman
  Resolver->>PrefsFile: save docker.io|podman -> podman
  Resolver-->>User: preference saved (exit 0)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #45: Package Conflict Resolution UI — This PR implements interactive conflict detection, option selection, and preference persistence aligning with the issue's objectives.
  • cortexlinux/cortex#42 — Adds a resolver CLI, JSON/human output, and wrapper integration consistent with the described feature.

Poem

🐰 I nibbled docs and parsed the cache,
Found tangled packages in a dash,
Pick a keeper, I saved the choice,
A quiet hop, a confident voice,
Preferences stored — now conflicts pass.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a package conflict resolver as indicated by the Makefile, Python script, documentation, and test additions.
Linked Issues check ✅ Passed All primary requirements from issue #45 are met: conflict detection from APT metadata [#45], human-readable output format [#45], multiple resolution options [#45], interactive selection capability [#45], preference persistence [#45], package manager wrapper integration [#45], shell tests included [#45], and documentation with examples [#45].
Out of Scope Changes check ✅ Passed All changes directly support conflict resolver functionality and its integration. The Makefile updates, README documentation, Python implementation, Debian packaging, and test coverage all align with the stated PR objectives for implementing the resolver.
Docstring Coverage ✅ Passed Docstring coverage is 93.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a package conflict resolver tool to detect and explain package conflicts before installation, integrating it into the cx CLI wrapper and providing comprehensive documentation and tests. Review feedback suggests optimizing the detection logic by avoiding redundant apt-cache calls, improving the robustness of JSON rule loading with better error handling, and enhancing performance by reducing file I/O operations when saving user preferences.

Comment thread apt/scripts/resolve-conflicts.py Outdated
requested = set(packages)
conflicts: dict[str, Conflict] = {}

for package in packages:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The loop iterates over the packages list, which may contain duplicates if the user provides the same package multiple times in the command line. This results in redundant apt-cache calls. Iterating over the requested set instead avoids this unnecessary overhead.

Suggested change
for package in packages:
for package in requested:

Comment thread apt/scripts/resolve-conflicts.py Outdated
Comment on lines +104 to +105
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The load_rule_conflicts function lacks error handling for JSON parsing and does not verify that the loaded data is a dictionary. If the rules file is malformed or contains an unexpected structure (e.g., a list at the top level), the script will crash with a JSONDecodeError or AttributeError. Consider adding a try-except block and a type check, similar to the implementation in load_preferences.

    try:
        with path.open("r", encoding="utf-8") as handle:
            data = json.load(handle)
    except (OSError, json.JSONDecodeError):
        return []

    if not isinstance(data, dict):
        return []

Comment on lines +222 to +224
for conflict in matching:
save_preference(preferences_path, conflict.key, args.choice)
preferences[conflict.key] = args.choice
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Calling save_preference inside a loop causes multiple redundant file read/write operations if the user's choice matches multiple conflicts. It is more efficient to update the preferences dictionary in memory and perform a single write operation after the loop.

        for conflict in matching:
            preferences[conflict.key] = args.choice

        # Save all updated preferences at once
        preferences_path.parent.mkdir(parents=True, exist_ok=True)
        with preferences_path.open("w", encoding="utf-8") as handle:
            json.dump(preferences, handle, indent=2, sort_keys=True)
            handle.write("\n")

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/resolve-conflicts-test.sh (1)

35-38: ⚡ Quick win

Invoke the resolver via python3 for test portability.

These calls currently depend on the script executable bit being preserved. Using python3 avoids false negatives in CI/local environments.

Suggested patch
-OUTPUT="$("$ROOT_DIR/apt/scripts/resolve-conflicts.py" \
+OUTPUT="$(python3 "$ROOT_DIR/apt/scripts/resolve-conflicts.py" \
     --rules "$RULES_FILE" \
     --preferences "$PREFS_FILE" \
     docker.io podman 2>&1)"
@@
-"$ROOT_DIR/apt/scripts/resolve-conflicts.py" \
+python3 "$ROOT_DIR/apt/scripts/resolve-conflicts.py" \
     --rules "$RULES_FILE" \
     --preferences "$PREFS_FILE" \
     --choice podman \
     docker.io podman >/dev/null || true
@@
-JSON_OUTPUT="$("$ROOT_DIR/apt/scripts/resolve-conflicts.py" \
+JSON_OUTPUT="$(python3 "$ROOT_DIR/apt/scripts/resolve-conflicts.py" \
     --rules "$RULES_FILE" \
     --preferences "$PREFS_FILE" \
     --json \
     docker.io podman || true)"
@@
-NO_CONFLICT_OUTPUT="$("$ROOT_DIR/apt/scripts/resolve-conflicts.py" \
+NO_CONFLICT_OUTPUT="$(python3 "$ROOT_DIR/apt/scripts/resolve-conflicts.py" \
     --rules "$RULES_FILE" \
     --preferences "$PREFS_FILE" \
     nginx curl)"

Also applies to: 53-57, 62-66, 69-72

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/resolve-conflicts-test.sh` around lines 35 - 38, Change the test
invocations that call the resolver script directly to run it with python3 to
avoid relying on the executable bit; specifically update the calls that execute
"resolve-conflicts.py" (the occurrences that currently run
"$ROOT_DIR/apt/scripts/resolve-conflicts.py" with various arguments such as
--rules "$RULES_FILE" --preferences "$PREFS_FILE" and the package names) so they
are prefixed with "python3" (i.e. run "python3
$ROOT_DIR/apt/scripts/resolve-conflicts.py ...") — apply the same change for the
other occurrences noted (around the blocks at 53-57, 62-66, and 69-72) so tests
invoke the script via python3 for portability.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apt/scripts/resolve-conflicts.py`:
- Line 211: The call to load_rule_conflicts uses Path(args.rules) without
expanding ~, so pass an expanded path: replace Path(args.rules) with
Path(args.rules).expanduser() (i.e., call
load_rule_conflicts(Path(args.rules).expanduser(), args.packages)); also scan
for other uses of args.rules in this file and apply .expanduser() there to match
how preferences are handled.
- Around line 22-43: The parser currently uses APT_RELATION_RE.search(...)
inside parse_relation_names which only returns the first token per
comma-separated clause and misses alternatives like the second token in "pkg-a |
pkg-b"; change parse_relation_names to iterate all matches for each part (e.g.,
use APT_RELATION_RE.findall(...) or APT_RELATION_RE.finditer(...)) and add every
match to the names set so all package names in a relation expression are
captured; keep using the existing APT_RELATION_RE and the same part.strip()
logic, just collect all matches instead of taking only the first.
- Around line 104-106: When loading the optional rules file (the with
path.open(...) block that calls json.load), catch JSON parsing errors so a
malformed --rules file doesn't crash the script; wrap json.load(path) in a
try/except that handles json.JSONDecodeError (and optionally
ValueError/OSError), log a warning mentioning the --rules file path and error,
and set data to an empty/default rules structure so processing continues instead
of raising.

---

Nitpick comments:
In `@tests/resolve-conflicts-test.sh`:
- Around line 35-38: Change the test invocations that call the resolver script
directly to run it with python3 to avoid relying on the executable bit;
specifically update the calls that execute "resolve-conflicts.py" (the
occurrences that currently run "$ROOT_DIR/apt/scripts/resolve-conflicts.py" with
various arguments such as --rules "$RULES_FILE" --preferences "$PREFS_FILE" and
the package names) so they are prefixed with "python3" (i.e. run "python3
$ROOT_DIR/apt/scripts/resolve-conflicts.py ...") — apply the same change for the
other occurrences noted (around the blocks at 53-57, 62-66, and 69-72) so tests
invoke the script via python3 for portability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e1073de-c9ba-463f-8ef6-c9fb86f7560f

📥 Commits

Reviewing files that changed from the base of the PR and between cfd0627 and e7e0bcc.

📒 Files selected for processing (5)
  • Makefile
  • apt/README.md
  • apt/scripts/resolve-conflicts.py
  • packages/cx-core/debian/rules
  • tests/resolve-conflicts-test.sh

Comment thread apt/scripts/resolve-conflicts.py
Comment thread apt/scripts/resolve-conflicts.py Outdated
Comment thread apt/scripts/resolve-conflicts.py Outdated
@dnesdan dnesdan force-pushed the codex/package-conflict-resolver-45 branch from e7e0bcc to ea9d302 Compare May 20, 2026 11:26
@dnesdan
Copy link
Copy Markdown
Author

dnesdan commented May 20, 2026

Updated this with the review fixes:\n\n- collect all package names in relation expressions like pkg-a | pkg-b\n- expand ~ in the rules path\n- malformed rules json now warns and keeps going\n- tests call the helper through python3\n- added coverage for malformed rules and relation parsing\n\nRe-ran:\n- python3 -m py_compile apt/scripts/resolve-conflicts.py\n- bash -n tests/resolve-conflicts-test.sh\n- make test-conflicts\n- git diff --check

Copy link
Copy Markdown

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apt/scripts/resolve-conflicts.py`:
- Around line 51-53: The subprocess invocation uses "apt-cache" by name which is
a PATH-based execution risk; update the subprocess.run call (the line assigning
result in resolve-conflicts.py) to call the absolute binary (e.g.,
"/usr/bin/apt-cache") or resolve the path via shutil.which and fail if not
found, then use that absolute path in the argv list passed to subprocess.run to
eliminate PATH hijack risk.
- Around line 156-161: The save_preferences function currently writes directly
to the final path which can produce a truncated/corrupt JSON if interrupted;
change save_preferences to write atomically by creating a temporary file in the
same directory (use a unique temp filename based on the target Path), write JSON
to that temp file, flush and os.fsync the file descriptor to ensure data is on
disk, then os.replace (atomic rename) the temp file to the original path; ensure
the parent directory is created beforehand and clean up the temp file on errors.
- Around line 118-124: The code assumes `data` is a dict with a list under
"conflicts" and that each conflict is a dict, which causes AttributeError on
malformed JSON; update the logic in resolve-conflicts.py (the loop starting with
data.get("conflicts", [])) to first validate types: check that `data` is an
instance of dict, that `conflicts = data.get("conflicts")` is a list (or treat
non-list as absent), and for each item verify `isinstance(item, dict)` before
calling item.get or iterating its "packages"/"options"; if validation fails,
skip that entry (or treat rules as not provided) so --rules remains optional and
processing continues without raising.

In `@packages/cx-core/debian/rules`:
- Around line 38-40: The current install pre-check uses the raw argv length
check 'if [ "$$1" = "install" ] && [ "$$#" -gt 2 ]' which counts flags and
causes resolve-conflicts.py to be invoked when flags (like --yes) are present;
change the gate to count package operands only (non-flag args) before calling
python3 /usr/share/cx/resolve-conflicts.py. Implement a small loop in the cx
wrapper script that iterates over "$@" and increments a counter for args not
starting with '-' (or use getopt to parse), then use that counter (e.g.,
operands_count -gt 1) in the conditional instead of "$#"; keep the call to
resolve-conflicts.py and its argument expansion "$${@:2}" unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bb551b9c-3e81-4da6-a8f0-2f2acc97e443

📥 Commits

Reviewing files that changed from the base of the PR and between e7e0bcc and a00dd52.

📒 Files selected for processing (5)
  • Makefile
  • apt/README.md
  • apt/scripts/resolve-conflicts.py
  • packages/cx-core/debian/rules
  • tests/resolve-conflicts-test.sh
✅ Files skipped from review due to trivial changes (1)
  • apt/README.md

Comment thread apt/scripts/resolve-conflicts.py
Comment thread apt/scripts/resolve-conflicts.py Outdated
Comment thread apt/scripts/resolve-conflicts.py Outdated
Comment thread packages/cx-core/debian/rules Outdated
@dnesdan
Copy link
Copy Markdown
Author

dnesdan commented May 20, 2026

Pushed a small follow-up in 4a3c4b0 for the review notes:

  • use /usr/bin/apt-cache instead of PATH lookup
  • write saved preferences through a temp file + fsync + replace
  • skip malformed rule entries safely
  • count install package operands before running the resolver

Local checks passing: python compile, bash syntax, git diff --check, and make test-conflicts.

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 Conflict Resolution UI

1 participant