Soy un gran fan y usuario de pipx, pero por alguna razón, no funciona tan
bien en macOS como en Linux.
La cosa es que uso conda (instalacion manual) y pipx (brew) muchas veces
cuando tengo un entorno virual activado pipx se hace un lio y usa el entorno
virtual incorrecto haciendo que el comando falle.
Uno de los cambios que hice fue instalar pipx en un entorno conda en lugar de
con brew y crear un alias para el binario, así ya no hay inconvenientes de
entornos y variables. En general, funciona mejor; me permite controlar la
versión de Python que usa pipx fácilmente (La variable de entorno
PIPX_DEFAULT_PYTHON también me causa problemas en MacOS).
Usando brew instala la última versión de Python, la cual algunos CLIs no
soportan (como duckdb).
Sin embargo, la situación no se limita a esto. Recientemente, me enfrenté al
siguiente desafío:
1
2
3
4
5
6
7
8
9
10
11
12
$ pipx install visidata
installed package visidata 3.0.2, installed using Python 3.11.5
These apps are now globally available
- vd
- vd2to3.vdx
- visidata
These manual pages are now globally available
- man1/vd.1
- man1/visidata.1
done! ✨ 🌟 ✨
$ vd
zsh: /Users/mgreco/.local/bin/vd: bad interpreter: /Users/mgreco/Library/Application: no such file or directory
En este punto ya me he dado por vencido y no quiero seguir creando soluciones
provisionales para esto, siendo que debería funcionar de inmediato (supongo que
alguna implementación inusual de macOS, impide que funcione correctamente).
Se me ocurrio replica lo que hago cuando quiero instalar un dependencia que
pipx en MacOS falla y que consiste en usar conda para gestionar los
entornos y pip para instalar dependencias. Es replicar la funcionalidad de
pipx install, pero usando conda. Esto tiene alguna ventaja extra como que te
permite controlar la versión de Python que deseas usar para cada entorno.
Lo primero que hice fue definir cómo quiero usarlo, que es fácil si ya conoces
pipx:
1
2
3
cx install black
cx install black -p 3.11
cx install git+https://github.com/psf/black
Nota: cx es como he llamado al comando conda + pipx.
El proceso para crear un entorno sería algo como lo siguiente:
Crear un entorno utilizando la versión deseada de Python.
Instalar el paquete requerido en el entorno creado.
Realizar la creación de un enlace simbólico a la Interfaz de Línea de
Comandos (CLI) en ~/.local/bin.
importsubprocessimportargparseparser=argparse.ArgumentParser()parser.add_argument("dep",help="can be a URL or a package name")parser.add_argument("-p","--pyver",help="python version to use",default="3.11")args=parser.parse_args()dep=args.deppyver=args.pyver# extract name from depname=dep.split('/')[-1].split('.')[0]conda_cmd=f"conda create -p ~/.cx/venvs/{name} python={pyver} -y"pip_cmd=f"~/.cx/venvs/{name}/bin/pip install -q {dep} "subprocess.call(conda_cmd,shell=True)subprocess.call(pip_cmd,shell=True)
Esta solucion ya hace lo que quiero, solo falta incluir el symlink y alguna
otras mejoras:
importsubprocessimportargparseparser=argparse.ArgumentParser()parser.add_argument("dep",help="can be a URL or a package name")parser.add_argument("-p","--pyver",help="python version to use",default="3.11")args=parser.parse_args()dep=args.deppyver=args.pyver# extract name from depname=dep.split('/')[-1].split('.')[0]print(name)conda_cmd=f"conda create -p ~/.cx/venvs/{name} python={pyver} -y -q > /dev/null"pip_cmd=f"~/.cx/venvs/{name}/bin/pip install -q {dep} "frompathlibimportPathsubprocess.call(conda_cmd,shell=True)pre_binaries=set(Path(f"~/.cx/venvs/{name}/bin/").glob('*'))subprocess.call(pip_cmd,shell=True)post_binaries=set(Path(f"~/.cx/venvs/{name}/bin/").glob('*'))new_binaries=post_binaries-pre_binariesprint(new_binaries)
En esta iteracion he incluido una forma de determinar qué binarios incluir en
~/.local/bin es bastante facil y funciona bien para lo que necesito pero
tiene margen para mejorar, por ejemplo, seguramente este incluyendo binarios de
las dependencias de los proyectos que estoy instalando.
El problema con esta solución es que la salida de new_binaries es un conjunto
vacío.
Sucede que MacOS devuelve False en
Path(f"~/.cx/venvs/{name}/bin/").exists(), pero si haces
Path(f"~/.cx/venvs/{name}/bin/").expanduser().exists(), devuelve True.
Por lo tanto, una vez resuelto el problema, ya tengo black y blackd en
new_binaries.
importsubprocessimportargparsefrompathlibimportPathparser=argparse.ArgumentParser()parser.add_argument("cmd",help="can be a URL or a package name",default="install",choices=["install","inject","run"],)parser.add_argument("dep",help="can be a URL or a package name")parser.add_argument("-p","--pyver",help="python version to use",default="3.11")parser.add_argument("-f","--force",help="Force",action="store_true")parser.add_argument("-v","--verbose",help="Verbose",action="store_true")args=parser.parse_args()dep=args.deppyver=args.pyverforce=args.forcecmd=args.cmdverbose=args.verbose# extract vname from depvname=dep.split("/")[-1].split(".")[0]# define variablesVENV=Path(f"~/.cx/venvs/{vname}").expanduser()BIN=Path(f"~/.cx/venvs/{vname}/bin").expanduser()PIP=Path(f"{BIN}/pip").expanduser()ifverbose:print(f"{VENV= }")print(f"{BIN= }")print(f"{PIP= }")ifcmd!="install":raiseNotImplementedError# create venvvenv_cmd=f"conda create -p {VENV} python={pyver} -y -q > /dev/null"ifverbose:print(f"{venv_cmd= }")subprocess.call(venv_cmd,shell=True)pre_binaries=set(BIN.glob("*"))# install deppip_cmd=f"{PIP} install -q {dep}"ifverbose:print(f"{pip_cmd= }")subprocess.call(pip_cmd,shell=True)post_binaries=set(BIN.glob("*"))# find new binariesnew_binaries=post_binaries-pre_binaries# create symlink in ~/.local/binf="-f"ifforceelse""forpinnew_binaries:link_cmd=f"ln {f} -s {p} ~/.local/bin"ifverbose:print(f"{link_cmd= }")subprocess.call(link_cmd,shell=True)
Esta es la solución final.
Tiene varias mejoras:
Incluye un parametro force para forzar el symlink
Crea un alias para el binario
Incluye un parametro verbose para mostrar el comando ejecutado
Lo he juntado todo en un proyecto de GitHub, podria ser usado con pipx run
pero en mi caso tengo el script en mi ~/.local/bin/cx.