Skip to content

Commit

Permalink
Add --select-candidates flag (#90)
Browse files Browse the repository at this point in the history
* Add --select-candidate flag to nix-alien-libs

* Add --select-candidates flag to nix-alien/nix-alien-ld

* Update README.md

* Optimise loops to use generators

* Use nixos-unstable

This is what nix-index-database uses (not that it matters too much,
because we will probably lock different commits).
  • Loading branch information
thiagokokada authored Oct 25, 2023
1 parent 80ee1d1 commit 2d9bb1e
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 29 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ There are also some other options, check them using `--help` flag on each
program. Example for `nix-alien`:

```console
usage: nix-alien [-h] [--version] [-l LIBRARY] [-p PACKAGE] [-r] [-d PATH] [-P] [-E] [-s] [-f] program ...
usage: nix-alien [-h] [--version] [-l LIBRARY] [-p PACKAGE] [-c CANDIDATE] [-r] [-d PATH] [-P] [-E] [-s] [-f] program ...

positional arguments:
program Program to run
Expand All @@ -108,6 +108,8 @@ options:
Additional library to search. May be passed multiple times
-p PACKAGE, --additional-packages PACKAGE
Additional package to add. May be passed multiple times
-c CANDIDATE, --select-candidates CANDIDATE
Library candidates that will be auto-selected if found. Useful for automation. May be passed multiple times
-r, --recreate Recreate 'default.nix' file if exists
-d PATH, --destination PATH
Path where 'default.nix' file will be created
Expand All @@ -120,8 +122,8 @@ options:

### Usage without installing

You can run the scripts from this repo directly without clonning or
installing them, assuming you're using [a resonable up-to-date nix and enabled
You can run the scripts from this repo directly without clonning or installing
them, assuming you're using [a resonable up-to-date nix and enabled
experimental Flakes support](https://nixos.wiki/wiki/Flakes#Enable_flakes).

```console
Expand Down
8 changes: 4 additions & 4 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
flake = false;
};
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nix-index-database.url = "github:Mic92/nix-index-database";
};

Expand Down
38 changes: 34 additions & 4 deletions nix_alien/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ def create_template_drv(
silent: bool = False,
additional_libs: Iterable[str] = (),
additional_packages: Iterable[str] = (),
select_candidates: Iterable[str] = (),
_indent: int = 4,
) -> str:
path = Path(program).expanduser()
packages = find_libs(path, silent, additional_libs)
packages = find_libs(path, silent, additional_libs, select_candidates)

return read_template(template).safe_substitute(
__name__=REMOVE_WHITESPACES.sub("_", path.name),
Expand All @@ -46,14 +47,20 @@ def create(
silent: bool = False,
additional_libs: Iterable[str] = (),
additional_packages: Iterable[str] = (),
select_candidates: Iterable[str] = (),
) -> None:
if recreate:
destination.unlink(missing_ok=True)

if not destination.exists():
destination.parent.mkdir(parents=True, exist_ok=True)
fhs_shell = create_template_drv(
template, program, silent, additional_libs, additional_packages
template,
program,
silent,
additional_libs,
additional_packages,
select_candidates,
)
with open(destination, "w", encoding="locale") as file:
file.write(fhs_shell)
Expand Down Expand Up @@ -82,10 +89,11 @@ def create_template_drv_flake(
silent: bool = False,
additional_libs: Iterable[str] = (),
additional_packages: Iterable[str] = (),
select_candidates: Iterable[str] = (),
_indent: int = 12,
) -> str:
path = Path(program).expanduser()
libs = find_libs(path, silent, additional_libs)
libs = find_libs(path, silent, additional_libs, select_candidates)

return read_template(template).safe_substitute(
__name__=path.name,
Expand All @@ -106,14 +114,20 @@ def create_flake(
silent: bool = False,
additional_libs: Iterable[str] = (),
additional_packages: Iterable[str] = (),
select_candidates: Iterable[str] = (),
) -> None:
if recreate:
destination.unlink(missing_ok=True)

if not destination.exists():
destination.parent.mkdir(parents=True, exist_ok=True)
fhs_shell = create_template_drv_flake(
template, program, silent, additional_libs, additional_packages
template,
program,
silent,
additional_libs,
additional_packages,
select_candidates,
)
with open(destination, "w", encoding="locale") as file:
file.write(fhs_shell)
Expand Down Expand Up @@ -163,6 +177,20 @@ def main(
action="append",
default=[],
)
parser.add_argument(
"-c",
"--select-candidates",
metavar="CANDIDATE",
help=" ".join(
[
"Library candidates that will be auto-selected if found.",
"Useful for automation.",
"May be passed multiple times",
]
),
action="append",
default=[],
)
parser.add_argument(
"-r",
"--recreate",
Expand Down Expand Up @@ -231,6 +259,7 @@ def main(
recreate=parsed_args.recreate,
additional_libs=parsed_args.additional_libs,
additional_packages=parsed_args.additional_packages,
select_candidates=parsed_args.select_candidates,
silent=parsed_args.silent,
)
else:
Expand All @@ -241,5 +270,6 @@ def main(
recreate=parsed_args.recreate,
additional_libs=parsed_args.additional_libs,
additional_packages=parsed_args.additional_packages,
select_candidates=parsed_args.select_candidates,
silent=parsed_args.silent,
)
4 changes: 4 additions & 0 deletions nix_alien/fhs_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def create_fhs_env(
silent: bool = False,
additional_libs: Iterable[str] = (),
additional_packages: Iterable[str] = (),
select_candidates: Iterable[str] = (),
) -> None:
return _impl.create(
template=TEMPLATE,
Expand All @@ -42,6 +43,7 @@ def create_fhs_env(
silent=silent,
additional_libs=additional_libs,
additional_packages=additional_packages,
select_candidates=select_candidates,
)


Expand All @@ -68,6 +70,7 @@ def create_fhs_env_flake(
silent: bool = False,
additional_libs: Iterable[str] = (),
additional_packages: Iterable[str] = (),
select_candidates: Iterable[str] = (),
) -> None:
return _impl.create_flake(
template=FLAKE_TEMPLATE,
Expand All @@ -78,6 +81,7 @@ def create_fhs_env_flake(
silent=silent,
additional_libs=additional_libs,
additional_packages=additional_packages,
select_candidates=select_candidates,
)


Expand Down
53 changes: 39 additions & 14 deletions nix_alien/libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def find_libs(
path: Union[Path, str],
silent: bool = False,
additional_libs: Iterable[str] = (),
select_candidates: Iterable[str] = (),
) -> dict[str, Optional[str]]:
_print = get_print(silent)
path = Path(path).expanduser()
Expand All @@ -55,25 +56,34 @@ def find_libs(
continue

candidates = find_lib_candidates(dep.soname)
selected_candidate = None

if len(candidates) == 0:
_print(f"No candidate found for '{dep.soname}'", file=sys.stderr)
selected_candidate = None
elif len(candidates) == 1:
selected_candidate = candidates[0]
else:
intersection = set(resolved_deps.values()).intersection(candidates)
if intersection:
# Can be any candidate really, lets pick the first one
selected_candidate = intersection.pop()
else:
fzf_options = join(
[
"--cycle",
"--prompt",
f"Select candidate for '{dep.soname}'> ",
]
)
selected_candidate = fzf.prompt(candidates, fzf_options)[0]
# Prioritise user selected candidates
maybe_selected_candidates = (
c for c in select_candidates if c in candidates
)
selected_candidate = next(maybe_selected_candidates, None)

# Try to find an dependency that is already solved
if not selected_candidate:
intersection = (d for d in resolved_deps.values() if d in candidates)
selected_candidate = next(intersection, None)

# Show FZF to allow user to select the best dependency
if not selected_candidate:
fzf_options = join(
[
"--cycle",
"--prompt",
f"Select candidate for '{dep.soname}'> ",
]
)
selected_candidate = fzf.prompt(candidates, fzf_options)[0]

_print(
f"Selected candidate for '{dep.soname}': {selected_candidate}",
Expand Down Expand Up @@ -105,6 +115,20 @@ def main(args=None):
action="append",
default=[],
)
parser.add_argument(
"-c",
"--select-candidates",
metavar="CANDIDATE",
help=" ".join(
[
"Library candidates that will be auto-selected if found.",
"Useful for automation.",
"May be passed multiple times",
]
),
action="append",
default=[],
)
parser.add_argument("-j", "--json", help="Output as json", action="store_true")
parser.add_argument(
"-s",
Expand All @@ -118,6 +142,7 @@ def main(args=None):
parsed_args.program,
silent=parsed_args.silent,
additional_libs=parsed_args.additional_libs,
select_candidates=parsed_args.select_candidates,
)

if parsed_args.json:
Expand Down
4 changes: 4 additions & 0 deletions nix_alien/nix_ld.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def create_nix_ld(
silent: bool = False,
additional_libs: Iterable[str] = (),
additional_packages: Iterable[str] = (),
select_candidates: Iterable[str] = (),
) -> None:
return _impl.create(
template=TEMPLATE,
Expand All @@ -42,6 +43,7 @@ def create_nix_ld(
silent=silent,
additional_libs=additional_libs,
additional_packages=additional_packages,
select_candidates=select_candidates,
)


Expand All @@ -68,6 +70,7 @@ def create_nix_ld_flake(
silent: bool = False,
additional_libs: Iterable[str] = (),
additional_packages: Iterable[str] = (),
select_candidates: Iterable[str] = (),
) -> None:
return _impl.create_flake(
template=FLAKE_TEMPLATE,
Expand All @@ -78,6 +81,7 @@ def create_nix_ld_flake(
silent=silent,
additional_libs=additional_libs,
additional_packages=additional_packages,
select_candidates=select_candidates,
)


Expand Down
54 changes: 51 additions & 3 deletions tests/test_libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ def test_find_libs_when_multiple_candidates_found(
DependencyMock(soname="libbar.so", path="/lib/libbar.so", found=False),
DependencyMock(soname="libbar.so", path="/lib/libbar.so", found=False),
]
mock_subprocess.run.return_value = CompletedProcessMock(stdout="foo.out\nbar.out")
mock_subprocess.run.return_value = CompletedProcessMock(
stdout="\n".join(["foo.out", "bar.out"])
)
# On the second time, this will take the candidate from intersection
mock_fzf.prompt.side_effect = [["foo.out"]]
assert libs.find_libs("xyz", silent=True, additional_libs=["libquux.so"]) == {
Expand All @@ -122,6 +124,38 @@ def test_find_libs_when_multiple_candidates_found(
assert err == ""


@patch("nix_alien.libs.list_dependencies", autospec=True)
@patch("nix_alien.libs.subprocess", autospec=True)
def test_find_libs_when_select_candidates_is_used(
mock_subprocess,
mock_list_dependencies,
capsys,
):
mock_list_dependencies.return_value = [
DependencyMock(soname="libfoo.so", path="/lib/libfoo.so", found=False),
DependencyMock(soname="libbar.so", path="/lib/libbar.so", found=False),
DependencyMock(soname="libquux.so", path="/lib/libbar.so", found=False),
]
mock_subprocess.run.return_value = CompletedProcessMock(
stdout="\n".join(["foo.out", "bar.out"])
)
assert libs.find_libs("xyz", select_candidates=["foo.out"]) == {
"libfoo.so": "foo.out",
"libbar.so": "foo.out",
"libquux.so": "foo.out",
}

_, err = capsys.readouterr()
assert (
err
== """\
[nix-alien] Selected candidate for 'libfoo.so': foo.out
[nix-alien] Selected candidate for 'libbar.so': foo.out
[nix-alien] Selected candidate for 'libquux.so': foo.out
"""
)


def test_get_unique_packages():
assert libs.get_unique_packages({}) == []

Expand Down Expand Up @@ -170,8 +204,22 @@ def test_main_with_args(mock_subprocess, mock_list_dependencies, capsys):
DependencyMock(soname="libfoo.so", path="/lib/libfoo.so", found=False),
DependencyMock(soname="libbar.so", path="/lib/libbar.so", found=False),
]
mock_subprocess.run.return_value = CompletedProcessMock(stdout="foo.out")
libs.main(["xyz", "--json", "--silent", "-l", "libqux.so", "-l", "libquux.so"])
mock_subprocess.run.return_value = CompletedProcessMock(
stdout="\n".join(["foo.out", "bar.out"])
)
libs.main(
[
"xyz",
"--json",
"--silent",
"-l",
"libqux.so",
"-l",
"libquux.so",
"-c",
"foo.out",
]
)

out, err = capsys.readouterr()
assert json.loads(out) == {
Expand Down

0 comments on commit 2d9bb1e

Please sign in to comment.