Compilar y ejecutar Rust en Linux con rustc y Cargo

Para comprobar un programa pequeño de Rust podemos compilar un archivo directamente. Cuando el proyecto empieza a tener varios archivos, dependencias y pruebas, Cargo nos ayuda a organizar ese trabajo.

Vamos a hacer ambos recorridos: primero un archivo que muestre un mensaje y después un proyecto con una función y dos tests. Así veremos dónde queda el ejecutable y qué diferencia hay entre comprobar código, compilarlo y ejecutarlo.

Si todavía no tenemos las herramientas, empezamos por instalar Rust y Cargo en Linux. Aquí no vamos a repetir la instalación ni a cambiar el conjunto de herramientas de otro proyecto.

Logotipo del lenguaje de programación Rust

Escribir y compilar un archivo

Creamos una carpeta nueva, entramos en ella y abrimos hola_mundo.rs con nuestro editor. El archivo contiene:

fn main() {
    println!("Hola mundo desde ElSoftwareLibre!");
}

main es el punto de entrada de este programa. La línea interior imprime el mensaje. Guardamos el código antes de llamar al compilador.

Desde la carpeta donde está el archivo:

rustc hola_mundo.rs -o hola_mundo
./hola_mundo

La opción -o indica el nombre de salida. Así no dependemos de recordar qué nombre elegirá el compilador. En este caso ejecutamos ./hola_mundo, no ./main.

Si compilamos sin -o, el nombre de salida se obtiene normalmente del archivo de entrada. Cambiar el nombre del .rs y seguir ejecutando otro binario puede hacernos creer que el código no se ha actualizado.

Compilación y ejecución de hola_mundo.rs desde una terminal de Linux

Un archivo de Rust compilado y ejecutado desde su carpeta.

La introducción oficial a rustc explica ese primer recorrido. Si el compilador muestra un error, lo leemos antes de intentar abrir un ejecutable que quizá no llegó a generarse.

Crear un proyecto con Cargo

Para continuar con un proyecto nuevo:

cargo new --vcs none mi_programa
cd mi_programa

--vcs none evita crear un repositorio Git para este ejemplo. Si la carpeta ya existe, revisamos su contenido en lugar de borrarla para que la orden vuelva a funcionar.

Cargo prepara Cargo.toml y un archivo src/main.rs. El primero describe el paquete y sus dependencias. El segundo contiene el código de entrada del programa.

La guía de Cargo desarrolla esa estructura. No necesitamos añadir bibliotecas externas para comprender la compilación básica.

Sustituimos el contenido de src/main.rs del proyecto de prueba por:

fn total(unidades: u32, precio_centimos: u32) -> u32 {
    unidades * precio_centimos
}

fn main() {
    println!("Total: {} céntimos", total(3, 250));
}

Es una función pequeña para ver el flujo, no un sistema de facturación. Utiliza cantidades limitadas de ejemplo y no resuelve descuentos, impuestos ni desbordamientos para entradas arbitrarias.

Comprobar sin generar el ejecutable final

Desde la carpeta que contiene Cargo.toml:

cargo check

Esta orden realiza las comprobaciones de compilación que describe la documentación de cargo check, sin completar la generación del ejecutable como hace una compilación normal.

Es útil mientras cambiamos código, pero que termine correctamente no significa que hayamos ejecutado el programa ni sus pruebas. Si buscamos un fallo de comportamiento, necesitamos otra comprobación.

Si Cargo no encuentra Cargo.toml, comprobamos la carpeta desde la que hemos lanzado la orden. Ejecutarlo en src puede funcionar por la búsqueda del manifiesto en directorios superiores, pero para empezar prefiero trabajar desde la raíz del proyecto.

Compilar y ejecutar

Podemos separar los dos pasos:

cargo build
./target/debug/mi_programa

También podemos pedir que Cargo compile lo necesario y ejecute el programa:

cargo run

Con nuestro ejemplo veremos Total: 750 céntimos. El nombre del ejecutable corresponde al paquete que hemos creado. Si cambiamos el nombre del paquete o definimos otros binarios, debemos revisar cuál queremos ejecutar.

La documentación de cargo build explica la selección de paquetes y salidas. No utilizaría la mera existencia de un binario como prueba de que la última compilación tuvo éxito: puede quedar una salida anterior.

Cuando necesitamos pasar argumentos al programa, los separamos de las opciones de Cargo con --. Eso sólo será útil si nuestro programa está preparado para leerlos. El ejemplo anterior no utiliza argumentos de entrada.

Añadir pruebas al comportamiento

Al final de src/main.rs, después del código anterior, añadimos:

#[cfg(test)]
mod tests {
    use super::total;

    #[test]
    fn calcula_tres_unidades() {
        assert_eq!(total(3, 250), 750);
    }

    #[test]
    fn ninguna_unidad_no_cuesta() {
        assert_eq!(total(0, 250), 0);
    }
}

Ahora ejecutamos:

cargo test

Estas pruebas comprueban los dos casos que hemos escrito. No demuestran que cualquier cantidad sea válida ni que el programa cubra todos los requisitos de una aplicación real.

La documentación de cargo test describe cómo selecciona y ejecuta tests. Si el resumen indica cero pruebas, no lo interpretamos como una validación de un comportamiento que no se ha comprobado.

En el ejemplo, introducir una expectativa incorrecta debe hacer fallar el test. Si cambiamos el resultado esperado para que coincida con cualquier salida, habremos quitado el aviso sin resolver el problema.

Preparar una compilación de entrega

Cuando queremos generar la salida con el perfil de entrega:

cargo build --release
./target/release/mi_programa

La ruta ya no es target/debug. release utiliza otro perfil de compilación, con sus ajustes correspondientes. No podemos demostrar una mejora de rendimiento simplemente por cambiar el nombre de la carpeta.

Antes de compartir ese ejecutable, probamos los casos que necesita el programa. En este ejemplo se comprobó tanto la ejecución normal como la salida de entrega, además de los dos tests.

También conservamos el código y la configuración con que se ha construido. Un binario aislado no explica qué dependencias, versión y opciones se utilizaron.

Dependencias y trabajo sin conexión

Si añadimos dependencias, Cargo puede necesitar descargarlas. No deberíamos aceptar una biblioteca sólo porque su nombre encaje con la tarea. Revisamos su origen, mantenimiento y licencia.

Para el proyecto anterior, que utiliza la biblioteca estándar y no añade dependencias externas, podemos realizar las operaciones con --offline:

cargo check --offline
cargo test --offline

En un proyecto más grande esa opción requiere que lo necesario ya esté disponible localmente. No descarga mágicamente lo que falta ni garantiza que cualquier repositorio funcione sin red.

No todos los ejecutables sirven para cualquier Linux

Un binario se construye para una arquitectura y un entorno determinados. Que lo hayamos compilado en Linux no garantiza que funcione en otro equipo con arquitectura o bibliotecas diferentes.

Podemos consultar información del compilador con rustc -vV y revisar el objetivo de compilación del proyecto. Para distribuir una aplicación, hay que comprobarla en los sistemas previstos y decidir cómo se entregarán sus dependencias.

No empezaría cambiando el objetivo de compilación si lo único que queremos es ejecutar el primer ejemplo en nuestro ordenador. Primero verificamos el flujo normal y después resolvemos la distribución como un paso distinto.

Con esos pasos sabemos qué ha comprobado cada orden y qué archivo estamos ejecutando. Esa diferencia evita varios errores pequeños que, en un proyecto mayor, suelen costar bastante más de encontrar.

Sergio Almela

Senior Software Engineer con más de diez años de experiencia. Escribo sobre Linux, software libre, backend, arquitectura, rendimiento, testing e IA aplicada al desarrollo.

Ver todas las entradas de Sergio Almela →

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *