Category: Tips

Solucionar problema de Autoridad de Certificación en los binarios .EXE generados con PY2EXE

Últimamente he empezado a utilizar la herramienta py2exe para poder mejorar la distribución de mis desarrollos en Python en sistemas Windows, mediante la generación de un ejecutable .exe autocontenido que evite tener que pedir al usuario que instale Python y las correspondientes librerías.

La herramienta py2exe puede ser instalada de manera sencilla con el comando:

pip install py2exe

Y puedes compilar rápidamente cualquier programa en Python realizando un fichero setup.py simple como este:

from distutils.core import setup
import py2exe

setup(console=['mi_programa.py'])

El cual deberemos ejecutar desde una consola de comandos de la siguiente forma:

python setup.py install

Al ejecutarlo nos resolverá todas las dependencias y dejará la versión autcontenida con el ejecutable mi_programa.exe en la carpeta dist.

Si trabajas con servicios o mínimamente con conexiones seguras haciendo peticiones, seguro que acabas usando la librería requests o puede que la librería con la que trabajes, la use como base para hacer conexiones seguras con SSL. Si intentas usar estas librerías que trabajan con certificados SSL te encontrarás con el siguiente error cuando ejecutas tu binario .exe:

requests.exceptions.SSLError: [Errno 2] No such file or directory

El cual viene dado porque cuando se empaqueta todo, el certificado de la Autoridad de Certificación no se incluye al ser un fichero que no es de Python. A causa de esto, cuando se empaquetan todas la librerías y se llama de manera relativa al certificado de la librería desde nuestro empaquetado con nuestro binario .exe de py2exe, éste no se encuentra porque ninguna parte.

Para solucionarlo es tan sencillo como proporcionar un certificado válido del tipo cacert.pem en la variable de entorno de Python REQUESTS_CA_BUNDLE de nuestro programa. Pero para resolverlo, vamos a hacerlo de manera elegante parcheando dicha variable sólo si es necesario, para poder seguir tirando de los certificados de las propias librería mientras desarrollamos.

Para ello vamos a instalar certifi, una librería que nos facilita una serie de Certificados Raíz que nos van a permitir validar la integridad de certificados, tanto de SSL, como de TSL de los servicios a los que nos conectemos.

pip install py2exe

Ahora vamos a modificar un poco nuestro setup.py:

from distutils.core import setup
import py2exe
import certifi

setup(console=['mi_programa.py'], data_files=[certifi.where()])

Hemos añadido únicamente el import de certifi y en los parámetros de setup hemos añadido el fichero de certificado que nos devuelve certifi gracias al método where. Este fichero se copiará en dist al mismo nivel que nuestro .exe.

Por último añadiremos en mi_programa.py al inicio de nuestro programa el siguiente código:

cacert_path = os.path.join(os.getcwd(), 'cacert.pem')
if os.path.exists(cacert_path):
    os.environ['REQUESTS_CA_BUNDLE'] = cacert_path

El código genera la ruta hasta certificado cacert.pem, usando el directorio de trabajo que será dist. Esa ruta se busca si existe entre las rutas que maneja Python para resolver las librerías y dependencias. Si se ejecuta desde el entorno de desarrollo, encontrará el de la propia librería que lo este usando, sino parcheará añadirá la ruta para que coja certificado que hemos copiado en dist.

De esta forma no sólo se soluciona el problema del certificado de la Autoridad de Certificación, sino que el parche se aplica selectivamente copiando fichero de certificado necesario. Esto es importante porque los certificados pueden cambiar y basta con volver a generar los binarios .exe con py2exe, teniendo las librerías actualizadas y con los certificados en regla, para que el ejecutable creado también los tenga.

Corregir colores ANSI en la consola de Windows 10 tras la actualización Windows Anniversary

La actualización de Windows Aniversario por fin llegó a mi equipo y aunque en general parece ser más robusto y seguro, era imposible no encontrarse con nuevos fallos introducidos a causa de esta enorme actualización. No obstante por la curva de fallos real del software, en cada actualización se introducen nuevos fallos no existentes anteriormente o mal solventados.

curva_fallos_software

Obviamente en esta actualización no iba a ser menos, dado el calado tan ambicioso de la actualización. El fallo que he detectado se corresponde a la interpretación de los colores ANSI de la consola de Windows. Concretamente el problema reside en que no se procesan los códigos ANSI de escape, que antes estaba activado por defecto gracias al flag de la consola ENABLE_VIRTUAL_TERMINAL_PROCESSING.

Esto supone un problema para todas las librerías y códigos de Python que usaban los colores ANSI para cambiar de color la letra y fondo de la consola de manera trasnaprente en todos los sistemas.

Ejemplo de la problemática que se empieza a notar en algunas librerías:

https://github.com/pyreadline/pyreadline/issues/46

Ante este problema se me ocurrieron 2 formas de arreglarlo.

 


Solución 1


cmder-main

La primera pasa por añadir un frontend a la maltrecha consola de comandos de Windows y pensé en Cmder por ser un proyecto que además es portable. De esta forma con un lanzador .BAT/.CMD es posible lanzar sobre Cmder lo que queramos y dejar a un lado la problemática de los colores.

La solución consiste en descargar Cmder en su versión portable y añadir el siguiente .BAT que nos permita lanzar Cmder y ejecutar comandos en su inicio:

@echo off
set CMDER_ROOT=%~dp0
start %CMDER_ROOT%\vendor\conemu-maximus5\ConEmu.exe /icon "%CMDER_ROOT%\cmder.exe" /title Cmder /loadcfgfile "%CMDER_ROOT%\config\ConEmu.xml" /cmd cmd /k "%CMDER_ROOT%\vendor\init.bat cd %CD% && cd .. && %~1"

Este archivo que puedes llamar "cmder.bat" debe estar al mismo nivel de directorio que el ejecutable "Cmder.exe". En este caso además se asume que Cmder está en una carpeta en la raíz del proyecto y la consola se posiciona en el nivel inmediatamente superior al que se encuentra "cmder.bat" y "Cmder.exe".

La forma de usar "cmder.bat" asumiendo que está en la carpeta "cmder" sería la siguiente:

start /B cmder.bat "python main.py" ^&^& timeout 5 ^&^& exit

En este caso se lanza un programa en Python, cuyo archivo de ejecución principal es "main.py". Con el parámetro "/B" se lanza la aplicación sin crear una nueva ventana. Adicionalmente se le indica con "^&^& timeout 5" que la consola espere 5 segundos antes de procesar la salida de la consola gracias a "^&^& exit". El resultado sería algo similar a esto:

cmder-launch

 


Solución 2


La segunda solución pasa por añadir un parche en la librería de Python que usa colores ANSI o en su defecto en el "main" de la aplicación. Las líneas de Python que debes añadir para corregir el problema específico de la interpretación de los colores ANSI de la consola de Windows tras la actualización de Windows Aniversario, es la siguiente:

import os
import platform

if os.name == 'nt' and platform.release() == '10' and platform.version() >= '10.0.14393':
    # Fix ANSI color in Windows 10 version 10.0.14393 (Windows Anniversary Update)
    import ctypes
    kernel32 = ctypes.windll.kernel32
    kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)

Gracias a la librería ctype que proporciona tipos de datos compatibles con C, se pueden hacer llamadas a DLLs o librerías compartidas. Esencialmente lo que se hace es habilitar la flag ENABLE_VIRTUAL_TERMINAL_PROCESSING.

De esta forma los colores de la consola pasan de verse así:

ansi_win_before

A verse así:

ansi_win_after

¡Mucho mejor!, ¿no?

Dejo a continuación la referencia al Fix completo publicado en Gist de GitHub:

ANSIColorFix.py

 

Ambas soluciones resuelven la problemática del color ANSI en la consola de Windows tras la actualización Aniversario del sistema. No obstante la primera solución puede ser menos invasiva al no requerir de cambios en el código, pero la segunda es una solución de enfoque más nativo, ofreciendo una solución transparente a los desarrolladores que hacen uso de las librerías afectadas por este problema.

Añadir fecha y hora al nombre de un archivo en la consola de comandos de Windows

Habitualmente suelo trabajar con la consola de comandos de Windows y con scripts .BAT. Una de las tareas mas habituales es generar outputs en forma de fichero y una buena práctica suele ser añadir al nombre del fichero la fecha y la hora.

Una forma sencilla de mostrar la fecha en la consola de comandos de Windows es poner el siguiente comando:

echo %date%

Y la forma de mostrar la hora es:

echo %time%

Sabiendo esto podemos pasar al siguiente paso para poder añadirlo al nombre de un fichero. Como has podido observar al obtener la fecha, la obtenemos con el signo "/" intercalado entre el día, mes y año. Del mismo modo la hora aparece separada por el signo ":" , para separar las horas, los minutos y los segundos. Además la parte de los segundo viene con "," para marcar los decimales para las décimas de segundo. Los tres símbolos citados, no están permitidos a la hora de incluirlos en un nombre de un fichero en Windows, por lo que procedemos a quitarlos de la siguiente manera:

echo MyFile_%date:~-4,4%-%date:~-7,2%-%date:~-10,2%_%time:~0,2%-%time:~3,2%-%time:~6,2%.txt

Si analizamos el código, lo que se hace es una concatenación de valores que se imprimen como nombre. La fecha se le da la vuelta porque a la hora de ordenar por nombre, a nombres iguales se puede ordenar por años con una simple ordenación por orden alfabético. En el caso de la hora no es necesario darle la vuelta.

Para cada variable date o time se cogen los caracteres que nos interesan de la siguiente forma:

%variable:~startIndex,numChars%

startIndex determina la posición desde la que se empiezan a coger los caracteres dentro de la variable. El índice empieza en 0 y también puede ser negativo el índice, indicando la posición de inicio desde el final de la variable.

numChars determina el número de caracteres que se van a coger desde el startIndex fijado.

cmd-win-append-date-time

De esta forma y extrapolándolo  a otros comando, es posible añadir fecha y hora, sólo fecha o solo hora al nombre de un fichero de un output de un comando.

Crear estructuras de carpetas no trackeables en Git

En ocasiones, por cuestiones de diseño es necesario mantener una estructura de carpetas determinada. Si además trabajas con Git te habrás dado cuenta que Git no añade un directorio vacío (ya sea una carpeta o varias anidadas) al proyecto. Siempre tiene que haber un archivo para que Git haga el tracking y añada la estructura de carpetas.

Si además dicho directorio quieres que se excluya del tracking de archivos, la cosa se pone un pelín más complicada, pero nada que no se pueda configurar con un poco de tino 😉 .

Para resolver el problema, en Git es necesario crear un archivo para que la estructura de carpetas se añada al tracking del proyecto. Por ello es necesario generar un fichero "dummy", un fichero vacío en el nivel más bajo del directorio, que sirva de baliza para que Git nos incluya la ruta completa de carpetas que nos permita llegar al archivo. Con esto ya tenemos solucionado el tema del directorio, poniendo ficheros "dummy" en los niveles más bajos del directorio.

Ahora es necesario realizar una acción que nos excluya todos los ficheros del directorio en cuestión, menos los ficheros "dummy" que nos hacen de baliza. Para ello en la raíz del proyecto, definiremos un fichero ".gitignore"  que sirve para manejar exclusiones del tracking de ficheros para el proyecto. Lo ficheros o grupos de ficheros definidos en las reglas de ".gitignore" se comportan como ficheros que no existen para Git, y evita proponerlos para incluirlos en un eventual commit.

Creando un caso simple que sirva de ejemplo, excluiremos la carpeta "test" de la ruta "config/test/" de un proyecto.

git_exclusion_directorio

La configuración del fichero ".gitignore" sería la siguiente:

config/test/*
!config/test/dummy.txt

El fichero consta de dos líneas. Una primera línea que excluye todo el directorio del tracking y una segunda línea que excluye el fichero "dummy" de la exclusión de la primera línea. Es importante el orden de las líneas y no son conmutables, ya que el fichero es interpretado de manera secuencial desde la primera línea hasta la última.

Con estas dos sencillas líneas podemos crear estructuras de ficheros, dentro del proyecto, para las cuales no queremos hacer seguimiento de cambios en sus ficheros. Esto es útil para generar estructuras donde alojar ficheros con los que probar el código del proyecto, pero los cuales no queremos añadir al proyecto porque no nos interese (por la razón que sea). También pueden ser útiles estas estructuras estáticas, en los casos en los que el usuario final, introducirá algún tipo de fichero que consuma nuestro proyecto.


Una mejor opción 🧬 [28/02/2021]

Un iteración más elegante es usar varios ficheros ".gitignore". Sobre todo en el caso de que queramos excluir un directorio y todos sus posibles subdirectorios.

⚠️ El uso de ficheros ".gitignore" de manera indiscriminada y descontrolada puede ser constraproducente si no existe una organización o una estrategia previa en su uso en el repositorio del proyecto. Git busca recursivamente y aplica todos los ficheros ".gitignore" que se encuentre dentro del repositorio.

La estrategia sería definir un fichero ".gitignore" dentro de la carpeta (y subcarpetas) que queramos que sean ignoradas, añadiendo como excepción el propio fichero para que Git nos permita añadirlo. El contenido del fichero sería el siguiente:

# Este directorio y subdirectorios son ignorados
*
# Indicamos que .gitignore es la única excepción
!.gitignore

El fichero consta de dós línea, la primera excluye absolutamente todo y la segunda añade como excepción el propio fichero para que pueda ser incluido en el árbol de versionado Git y no sea también excluido. De esta forma tenemos una forma sencilla en la que añadiendo ese fichero ".gitignore" a cualquier directorio, estaremos ignorando el contenido de dicho directo y subdirectorios.