lunes, 12 de noviembre de 2012

Integrando CMake con Eclipse CDT

Acostumbro a usar Eclipse para desarrollar en C++. Admito que se bebe la memoria (sobretodo cuando indexa el código), pero la funcionalidad que me da para navegar, buscar, autocompletar y depurar es impagable.

Ahora, la compilación deja un poco que desear. Para empezar, porque compilar un proyecto de Eclipse desde la linea de comandos no es tarea fácil. Para seguir, porque a veces se lía un poco con las dependencias y compila menos cosas de las que debiera. No mola nada que tu programa falle porque la mitad de los fuentes están compilados contra una versión de un fichero de cabecera distinta de la otra mitad. Lo digo por experiencia.

Así que la opción era pasarse a otro sistema de compilación, pero manteniendo la integración con Eclipse. Tras varias pruebas, el que más me convencía era CMake. Multiplataforma, código abierto, permite generar todos los ejecutables y librerías que quieras (y no solo uno por proyecto, como hace Eclipse) y gestiona el descubrimiento de las dependencias que te hagan falta. Bueno, y muchas más cosas. Yo, desde que lo uso, soy más feliz.

Sin embargo la integración con Eclipse no es inmediata. El mismo wiki de CMake propone varias opciones. La primera es generar el proyecto de Eclipse CDT con CMake, y la ponen como recomendada. Tiene varias desventajas conocidas. Yo además veo un par de choques de conceptos. Primero, los ficheros generados por CMake deberían ser transitorios, mientras que Eclipse espera que los ficheros de proyecto sean permanentes, como el código fuente. Segundo, CMake generará un fichero de proyecto por cada configuración (Debug, Release, RelWithDebInfo o MinSizeRel), mientras que Eclipse utiliza el mismo proyecto para todas las configuraciones.

La segunda opción, en la que me he basado, propone el camino contrario: usar un proyecto de tipo Standard Makefile que llame a CMake en el momento de compilar. En este tipo de proyecto, Eclipse asume que el usuario gestiona sus ficheros Makefile. En el wiki sugieren usar o bien una herramienta externa o bien make targets adicionales para llamar a CMake. Eso hace que la compilación pueda requerir un paso adicional por parte del usuario, así que he ido un poco más alla: utilizar un Makefile maestro que dependa de los Makefiles que genera CMake, y que por tanto le llame cuando sea necesario. Además, las reglas dependerán de la configuración usada por Eclipse y llamarán a CMake de manera consecuente. El Makefile es el siguiente:

CONFIG?=Debug

all: build/$(CONFIG)/Makefile
    $(MAKE) -C build/$(CONFIG)

build/$(CONFIG)/Makefile: build/$(CONFIG)
    cmake -E chdir build/$(CONFIG) cmake -G "Unix Makefiles" ../../ -DCMAKE_BUILD_TYPE:STRING=$(CONFIG)

build/$(CONFIG):
    mkdir -p build/$(CONFIG)

.PHONY: clean

clean:
    rm -fr build/$(CONFIG)
   
Comienza estableciendo la variable CONFIG, en caso de que no exista, a la configuración Debug. Luego vienen las tres reglas principales:
  1. Una regla que compila el código cuando el fichero build/$(CONFIG)/Makefile se haya generado.
  2. Una regla que genera ese fichero llamando a CMake desde el directorio build/$(CONFIG), si existe.
  3. Y una regla que crea ese directorio.
Finalmente está la regla que limpia la compilación anterior.

Fijaos que al llamar a CMake, se establece la variable CMAKE_BUILD_TYPE al mismo valor que CONFIG, para que compile el proyecto con los parámetros adecuados. Las alternativas, como he dicho antes, son Debug, Release, RelWithDebInfo y MinSizeRel. Así que hay hacer que Eclipse llame a make con el valor apropiado de la variable CONFIG. Es tan sencillo como ir a las propiedades del proyecto -> C/C++ Build -> Environment, y añadir una variable de entorno llamada CONFIG con el valor que queramos. Si le damos el valor ${ConfigName}, haremos que el nombre de la configuración de Eclipse coincida con la de CMake, y la jugada ya es redonda.

sábado, 10 de noviembre de 2012

Dual-boot y virtualización

Vaya, hacía ya tiempo que no escribía por aquí... pero nunca es tarde.

Llevo unos días lanzando unas simulaciones que corren en Linux, y que requieren tanto procesador como memoria abundantes. Y como recientemente me compré un equipo jugón en casa, había pensado en lanzar alguna ahí. Problema: equipo jugón == Windows (hasta que salga Steam para Linux...). Hasta ahora tenía una Debian virtualizada, que me iba muy bien para tareas de edición fotográfica, y podría haberla usado. Pero claro, así no puedo disponer totalmente de los cuatro núcleos y los 8 gigas de ram, así que mejor si Linux se ejecuta como anfitrión.

Total, que la idea era instalar Linux como arranque dual, pero poder seguir manteniendo la virtualización para ejecutarlo cómodamente desde Windows. ¿¿¿Es eso posible??? Pues sí, amiguitos. En pocas palabras, hay que crear una máquina virtual que utilice un disco físico como unidad virtual, en el que previamente hemos instalado Linux. Sin embargo, hay que tener en cuenta que el hardware que va a ver Linux como anfitrión no será el mismo que vea como virtualizado: discos duros, interfaces de red, tarjetas de sonido, tarjetas gráficas... Veamos el detalle.

 

Instalar Linux

He usado Windows 7, Debian GNU/Linux 7 (testing en este momento) y VMWare Player 9 para montar el tinglado. Tengo un disco SSD de sistema de 128 gigas, y un disco mecánico de datos de 1 tera.

En un principio, Windows ocupaba los dos discos, pero encoger las particiones para instalar Linux es bastante fácil. Vamos a Mi PC, botón derecho, Administrar, Administrador de discos, botón derecho en las particiones que queremos encoger y Reducir volumen. Podía haber ido a lo fácil y usar sólo una parte del disco de datos para Linux, pero ya que Windows disfruta de un arranque realmente rápido gracias al SSD, Linux no podía ser menos. Así que dejé una partición de 8 gigas en el SSD para la raíz de Linux y una de 50 gigas en el mecánico para el home. De sobra, ya que desde Linux no hay problema en montar y usar las otras particiones NTFS.

Y lo siguiente ya es instalar Debian. Yo siempre tiro de instalación por red. Te bajas una imagen de CD pequeña y luego sólo los paquetes que necesitas. Esta parte debería ser fácil, en principio, pero ayer tenía la negra: el instalador beta que viene con la distribución de pruebas tenía problemas para cargar los módulos del núcleo, así que sin red. Y claro, difícil hacer una instalación por red sin red... Así que usé el instalador de la distribución estable, con idea de actualizar a la de pruebas lo primero de todo. Total, que el instalador estable tenía problemas para conectar a la red también. Había que estar constantemente tirando y levantando de nuevo el interfaz de red para conseguir que descargara los paquetes. Pero bueno, esto no sería divertido si no terminara a las 4 de la mañana, ¿no?

 

Configuración del hardware

Como he dicho antes, hay que configurar Linux teniendo en cuenta que va a ver un hardware distinto dependiendo de si arranca como anfitrión o virtualizado. En general no habrá mayor problema: hoy en día, casi todo se configura al vuelo, con dispositivos extraibles y cosas así. Pero hay ciertos detalles que hay que tener en cuenta. En general, si hay que tomar alguna acción en tiempo de ejecución dependiendo de si el entorno es virtualizado o no, se puede usar un programa como vmware-checkvm (en el caso de VMWare) o imvirt (en cualquier caso). Ambos imprimen por la salida estándar una descripción del gestor de virtualización que se está usando. En caso no estar en un entorno virtualizado, vmware-checkvm devuelve un valor de retorno distinto de cero, e imvirt escribe "Physical" en la salida estándar.

 

Discos duros

He instalado Linux en la partición /dev/sda3, con el espacio de intercambio en /dev/sdb2 y el home en /dev/sdb3. ¿Seguirán llamándose así cuando arranque en virtualizado? Ni de coña. Pero Linux hace tiempo que resolvió este problema: UUIDs. En el fichero /etc/fstab, en lugar de especificar el dispositivo que se debe usar para cada sistema de ficheros, podemos identificarlos por UUID. De este modo es igual en qué dispositivo esté cada sistema de ficheros, siempre se montarán en el lugar que les corresponde.

Para conocer el UUID se usa el comando blkid:

# sudo blkid /dev/sdb3
/dev/sdb3: UUID="8d817db1-25a5-4ef1-a88e-ac47d60a3cf3" TYPE="ext4"

En el fichero fstab deberá aparecer como:

UUID=8d817db1-25a5-4ef1-a88e-ac47d60a3cf /home ext4 defaults 0 1

Debian testing hace esto de forma automática. Incluso utiliza el UUID en la linea de comandos del kernel para identificar el sistema de ficheros raíz. Sin embargo, en Debian estable hay que hacerlo a mano. Tenedlo en cuenta...

 

Interfaces de red

Hay que tener en cuenta que al virtualizar el Linux, va a aparecer como un sistema adicional conectado a la red, así que necesita una dirección IP propia. Con DHCP no hay problema. Tanto si configuráis la máquina virtual para que conecte con NAT como en modo bridged, el servidor de DHCP le dará una dirección sin usar. En mi caso, como estoy en un segmento con direccionamiento estático, tengo que asignarle una dirección que valga para ámbas situaciones. Básicamente hay que asignar una dirección distinta de la que tiene Windows y configurar la máquina virtual en modo bridged; en modo NAT no funciona sin DHCP. Fácil, ¿no?

Si vuestro ordenador está conectado a la red por cable, como es mi caso, Linux verá un interfaz de red eth0 tanto en virtualizado como en anfitrión. El fichero /etc/network/interfaces solo contendrá la configuración de ese interfaz. Si conectáis por wifi, lo más seguro es que en el Linux anfitrión os aparezca el interfaz wlan0 y en virtualizado eth0. Hay que poner la configuración de los dos en el fichero interfaces, y Linux ignorará la de aquel dispositivo que no exista en cada caso.

Una aclaración: yo esperaba que udev generara un nombre distinto para la interfaz de red real y para la virtual, porque tienen direcciones MAC distitnas. No fue así. Más fácil aún.

 

Tarjetas de sonido

Sin problemas. Linux usará la tarjeta que detecte en cada caso, bien la tarjeta física o bien la virtualizada. Posiblemente KDE (en mi caso) o Gnome se quejen de que aparecen y desaparecen dispositivos de audio cada vez que arrancáis Linux en un modo distinto. Simplemente les decís que no los eliminen de la lista y que no pregunten más.

 

Tarjetas gráficas

Aquí está la otra gracia: Es posible que necesites un fichero /etc/X11/xorg.conf según si Linux se ejecuta como anfitrión o virtualizado. Por ejemplo, si quieres usar los drivers propietarios (de NVidia, como es mi caso, o de cualquier otro) o seleccionar una disposición concreta de varias pantallas. En modo virtualizado el driver tiene que ser vmware.

La opción por la que he optado yo es guardar las dos configuraciones en el mismo fichero. Luego, crear un script que haga de wrapper para el servidor X, y que use una configuración distinta en función de lo que devuelva imvirt. La configuración puede ser algo así:

Section "Device"
    Identifier     "Device0"
    Driver         "nvidia"
    VendorName     "NVIDIA Corporation"
EndSection

Section "Device"
    Identifier     "Device1"
    Driver         "vmware"
EndSection

Section "Screen"
    Identifier     "nvs"
    Device         "Device0"
    SubSection     "Display"
        Modes      "nvidia-auto-select"
    EndSubSection
EndSection

Section "Screen"
    Identifier     "vms"
    Device         "Device1"
EndSection


El wrapper, que se llama /usr/bin/Xvirt, tendría la siguiente pinta:

#!/bin/sh
if vmware-checkvm; then
    exec /usr/bin/X -screen vms "$@"
else
    exec /usr/bin/X -screen nvs "$@"
fi

Hay que conseguir que se ejecute en lugar del servidor de X. En el caso de kdm, en el fichero /etc/kde4/kdm/kdmrc hay una linea en la sección de opciones comunes a servidores locales llamada ServerCmd, que establece qué comando hay que ejecutar para iniciar el servidor. Simplemente establecemos ese valor a /usr/bin/Xvirt. Para un caso más general, también se puede renombrar /usr/bin/X por /usr/bin/_X y /usr/bin/Xvirt por /usr/bin/X.

 

Crear la máquina virtual

Una vez instalado Debian y visto que el ordenador arranca en modo dual (uso Grub 2, detalle importante más adelante), procedo a la creación de la máquina virtual. La clave está en seleccionar los discos duros que contienen el sistema Linux como discos virtuales en lugar de crear unos nuevos. En concreto, VMWare nos da la opción de hacer visibles sólo algunas de las particiones. Tener acceso a particiones que están montadas en el anfitrión es muy mala idea, podemos acabar con datos corruptos. Así, las particiones ocultas devuelven bloques de ceros al leer e ignoran las escrituras.

 

Arrancando

Total, que he añadido los dos discos físicos a la máquina virtual, dejando visibles sólo las particiones de Linux. Configuro la red en modo bridged, como he dicho antes, y arranco. En la bios de la máquina virtual selecciono el primer disco para arrancar, el que tiene Grub. ¿Y qué ocurre? Nada, Grub no arranca.

Se veía venir, mis discos son SATA y VMWare añade los discos físicos como si fueran IDE, así que algún problema iban a dar. La solución no es complicada: crear un pequeño disco virtual que contenga el sector de arranque con Grub, pero utilizando los mismos ficheros que el sistema instalado. El disco puede ser todo lo pequeño que queráis, 100 megas le he puesto yo, pero puede ser menos. Eso sí, tiene que caber una partición y un sistema de ficheros. Si no, Grub se quejará. Arrancamos la máquina virtual con el CD de instalación de Debian en modo recuperación. Creamos la partición en el disco virtual y la formateamos, como ext2, por ejemplo. Luego ejecutamos un shell en el entorno del sistema instalado. Nos aseguramos que en /boot/grub están los ficheros de Grub originales, y miramos qué dispositivo se corresponde con el nuevo disco virtual; supongamos que es /dev/sdc, pues ejecutamos:

# grub-install /dev/sdc

Este comando copiará el gestor de arranque en el nuevo disco virtual y recordará en qué disco están los ficheros con el menú y las siguientes etapas de arranque. Luego reiniciamos y en la bios de la máquian virtual seleccionamos el nuevo disco virtual como dispositivo de arranque. ¿Aparece el menú de Grub? ¡Perfecto!

Fijáos que el menú es el mismo que sale al arrancar el ordenador. Es decir, podéis seleccionar tanto el Linux como el Windows que se está ejecutando en ese momento como anfitrión. Y si intentáis virtualizar el mismo sitema anfitrión... Inception!! Noooooo, es broma, no pasa nada. Recordad que habéis ocultado todas las particiones que no pertenecían a Linux, así que no hay peligro. Porque las habéis ocultado, ¿¿¿VERDAD??? Si no, id preparandoos para reinstalar todo...

 

Compartición de archivos con el Windows anfitrión

Queda un último tema por aclarar: cómo acceder a los ficheros del Windows anfitrión. Cuando el Linux se ejecuta como anfitrión, se pueden montar las particiones NTFS; cuando se ejecuta virtualizado, no se puede. Entonces hay que usar las carpetas compartidas. VMWare tiene su propio sistema de carpetas compartidas, pero a mi me ha dado muchos problemas en el pasado, con ficheros que se corrompían y baja tasa de transferencia. En cambio, con las carpetas compartidas de Windows y un cliente Samba en Linux, funciona como la seda.

La idea es que, de un modo u otro, se monte cada partición siempre en el mismo sitio. Yo he elegido /mnt/c y /mnt/d para montar las unidades C: y D: que me aparecen en Windows. Montar la partición sólo funciona en modo anfitrión, montar el la carpeta compartida sólo funciona en modo virtualizado, así que voy a hacer una pequeña chapucilla: Añadir dos lineas al fichero /etc/fstab por cada punto de montaje, uno montando la partición y otro montando la carpeta compartida. Sólo una de las dos lineas funcionará.

Por ejemplo, para montar la partición de datos, D:, añado las siguientes dos lineas al fichero /etc/fstab:

UUID=8C949FE4949FCF58 /mnt/d ntfs defaults,uid=javi,gid=javi,umaks=066 0 0
 

//192.168.0.2/d /mnt/d cifs credentials=/etc/win.creds,uid=javi,gid=javi,rw,file_mode=0600,dir_mode=0700 0 0

Fijaos que hay que establecer quién será el propietario de los ficheros y los permisos por defecto, ya que ni ntfs ni cifs son sistemas de ficheros Posix. El fichero /etc/win.creds contiene el usuario y la contraseña con las que he compartido la unidad d en Windows.

Y fin.