From 2d9bb1ed46eea734313fa8bfdb92de0bf342fe45 Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Wed, 25 Oct 2023 20:07:26 +0100 Subject: [PATCH] Add `--select-candidates` flag (#90) * 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). --- README.md | 8 ++++--- flake.lock | 8 +++---- flake.nix | 2 +- nix_alien/_impl.py | 38 +++++++++++++++++++++++++++---- nix_alien/fhs_env.py | 4 ++++ nix_alien/libs.py | 53 +++++++++++++++++++++++++++++++------------ nix_alien/nix_ld.py | 4 ++++ tests/test_libs.py | 54 +++++++++++++++++++++++++++++++++++++++++--- 8 files changed, 142 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index e710cf0..a3f4005 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 diff --git a/flake.lock b/flake.lock index f457e9b..993cd46 100644 --- a/flake.lock +++ b/flake.lock @@ -70,16 +70,16 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1697009197, - "narHash": "sha256-viVRhBTFT8fPJTb1N3brQIpFZnttmwo3JVKNuWRVc3s=", + "lastModified": 1697723726, + "narHash": "sha256-SaTWPkI8a5xSHX/rrKzUe+/uVNy6zCGMXgoeMb7T9rg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54", + "rev": "7c9cc5a6e5d38010801741ac830a3f8fd667a7a0", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 6475006..fc2c29c 100644 --- a/flake.nix +++ b/flake.nix @@ -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"; }; diff --git a/nix_alien/_impl.py b/nix_alien/_impl.py index 584e302..f560aae 100644 --- a/nix_alien/_impl.py +++ b/nix_alien/_impl.py @@ -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), @@ -46,6 +47,7 @@ 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) @@ -53,7 +55,12 @@ def create( 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) @@ -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, @@ -106,6 +114,7 @@ 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) @@ -113,7 +122,12 @@ def create_flake( 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) @@ -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", @@ -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: @@ -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, ) diff --git a/nix_alien/fhs_env.py b/nix_alien/fhs_env.py index 1a75625..f6748d3 100644 --- a/nix_alien/fhs_env.py +++ b/nix_alien/fhs_env.py @@ -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, @@ -42,6 +43,7 @@ def create_fhs_env( silent=silent, additional_libs=additional_libs, additional_packages=additional_packages, + select_candidates=select_candidates, ) @@ -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, @@ -78,6 +81,7 @@ def create_fhs_env_flake( silent=silent, additional_libs=additional_libs, additional_packages=additional_packages, + select_candidates=select_candidates, ) diff --git a/nix_alien/libs.py b/nix_alien/libs.py index 77ec295..c6695b9 100644 --- a/nix_alien/libs.py +++ b/nix_alien/libs.py @@ -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() @@ -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}", @@ -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", @@ -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: diff --git a/nix_alien/nix_ld.py b/nix_alien/nix_ld.py index 6f1ca73..0b3caf2 100644 --- a/nix_alien/nix_ld.py +++ b/nix_alien/nix_ld.py @@ -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, @@ -42,6 +43,7 @@ def create_nix_ld( silent=silent, additional_libs=additional_libs, additional_packages=additional_packages, + select_candidates=select_candidates, ) @@ -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, @@ -78,6 +81,7 @@ def create_nix_ld_flake( silent=silent, additional_libs=additional_libs, additional_packages=additional_packages, + select_candidates=select_candidates, ) diff --git a/tests/test_libs.py b/tests/test_libs.py index 3e50cf6..c46b841 100644 --- a/tests/test_libs.py +++ b/tests/test_libs.py @@ -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"]) == { @@ -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({}) == [] @@ -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) == {