Fuente: handbook/ en el repositorio. Cada bloque de código compila: el CI corre ray check sobre cada uno.
Distribuir
Los capítulos anteriores construyen las apps. Este cubre cómo llegan a sus usuarios: empaquetar para cada sistema, firmar, publicar en las tiendas móviles y, en escritorio, actualizarse solas de forma segura. El ejemplo es la Notes del capítulo multiplataforma, que trae el código de actualización.
| Destino | Qué se entrega | Cómo |
|---|---|---|
| macOS | Notes.app firmada y notarizada |
ray bundle con [app] sign y [app] notary |
| Windows | Notes.exe firmado |
ray bundle con [app] sign |
| Linux | el binario y su .desktop |
ray bundle |
| iPhone | la app en App Store | ray bundle --ios, y Xcode para archivar y subir |
| Android | la app en Google Play | ray bundle --android y gradle bundleRelease |
| Servidor | un binario | ray build --native --release |
1. Empaquetar el escritorio
ray bundle compila el binario nativo en modo release, con los recursos embebidos, y lo deja en el
formato del sistema en el que se ejecuta. La identidad y el icono salen del ray.toml:
[app]
name = "Notes"
id = "dev.raylang.notes"
icon = "icon.png" # opcional: el icono de la app en cada sistema
Como cada sistema se empaqueta en su propia máquina, lo habitual es una matriz de CI con un job por sistema operativo.
Todo lo que ray bundle necesita saber de la app está en la sección [app]:
| Clave | Para qué |
|---|---|
name |
el nombre visible de la app |
id |
su identificador, en notación de dominio inverso |
icon |
un PNG; ray bundle genera el formato de cada sistema |
copyright |
el texto del panel «Acerca de» en macOS y de las propiedades del .exe en Windows |
sign |
la identidad de firma (sección 2) |
notary |
el perfil de notarización de macOS (sección 2) |
entitlements |
un archivo de permisos para la firma de macOS |
opens |
lo que la app sabe abrir — ["folder", "text", "image", ".ray"] o un UTI/MIME literal —; el sistema le entrega esos elementos como eventos "open" (soltar sobre el icono, «Abrir con») |
public_key |
la clave pública de las actualizaciones (sección 3); la escribe ray keygen |
Además, [app.plist] añade claves al Info.plist de macOS, y [native] embed lista las carpetas
que van dentro del binario. Una app empaquetada arranca con el directorio actual en /, así que
todo recurso que lea debe ir embebido.
2. Firmar
Sin firma, macOS pide al usuario que apruebe a mano una app descargada, y Windows muestra un aviso de SmartScreen. Linux no exige firma.
macOS necesita un certificado «Developer ID Application» de una cuenta de desarrollador de Apple, y un perfil de notarización creado una vez:
xcrun notarytool store-credentials notes-notary # pide el Apple ID y una contraseña de app
[app]
sign = "Developer ID Application: Tu Nombre (TEAMID)"
notary = "notes-notary"
Con eso, ray bundle firma con hardened runtime, verifica la firma, envía la app a Apple, espera
la aprobación y grapa el ticket a la .app. Si la firma no verifica, sale con el código 74. Las
mismas opciones existen como --sign, --notary y --entitlements, y como las variables
RAY_SIGN_IDENTITY y RAY_NOTARY_PROFILE para CI.
Windows usa signtool: [app] sign es el sujeto del certificado instalado, o la ruta de un
archivo .pfx cuya contraseña va en RAY_SIGN_PFX_PASSWORD.
Para comprobar el resultado en macOS antes de publicarlo:
codesign --verify --deep --strict Notes.app && echo firmada
spctl --assess --type execute Notes.app && echo aceptada por Gatekeeper
En CI, las identidades y las claves llegan como secretos, por variables de entorno:
| Variable | Equivale a | Qué es |
|---|---|---|
RAY_SIGN_IDENTITY |
[app] sign, --sign |
la identidad de firma |
RAY_NOTARY_PROFILE |
[app] notary, --notary |
el perfil de notarización |
RAY_SIGN_PFX_PASSWORD |
la contraseña del .pfx en Windows |
|
RAY_SIGNING_KEY |
--key |
la clave privada de las actualizaciones (sección 3) |
3. Actualizaciones automáticas en escritorio
El publicador deja tres archivos en una URL fija: update.json con la versión y los artefactos,
su firma update.json.sig, y un .zip por plataforma. La app descarga el manifiesto, comprueba la
firma con la clave pública que lleva dentro, y si hay una versión nueva la descarga, verifica su
SHA-256, reemplaza el bundle instalado y se reinicia.
Este es un update.json real, tal como lo escribe ray release:
{
"app": "dev.raylang.notes",
"version": "0.2.0",
"notes": "https://example.com/notes/0.2.0",
"min_version": "",
"artifacts": {
"macos-aarch64": {"url": "Notes-0.2.0-macos-aarch64.zip", "sha256": "2d2f6fd5ae6ce1aa33efb89c746162d9f16e23cefad6443cb023623d63937fe9", "size": 271727}
}
}
Cada plataforma tiene su entrada en artifacts, con la clave <sistema>-<arquitectura>:
macos-aarch64, linux-x86_64, windows-x86_64. La seguridad no depende del servidor que aloja
los archivos, sino de tres comprobaciones:
| Comprobación | Qué impide |
|---|---|
la firma de update.json con la clave de la app |
que alguien publique un manifiesto falso, aunque controle el servidor |
el sha256 y el size de cada artefacto |
que el .zip descargado sea distinto del publicado |
| la clave pública va dentro del binario instalado | que el atacante cambie también la clave con la que se verifica |
Una vez, crea la clave de firma de la app:
ray keygen # la semilla va a ~/.ray/keys/<app-id>.key; la pública, al ray.toml
La clave privada nunca sale de tu máquina o de los secretos de CI (RAY_SIGNING_KEY). La pública
queda en [app] public_key, y ray bundle la mete en el binario.
Guarda una copia de esa clave en un gestor de secretos. Si se pierde, las apps ya instaladas no
pueden aceptar ninguna actualización más, porque solo confían en esa clave. Por eso ray keygen
se niega a sobrescribir una clave existente si no se le pasa --force.
En cada versión, sube version en el ray.toml y publica:
ray release -o dist # el zip de esta plataforma + update.json firmado
ray release -o dist --publish --tag v0.2.0 # además lo sube a GitHub Releases
ray release conserva en update.json los artefactos de otras plataformas de la misma versión,
así que cada job de CI lo ejecuta sobre el mismo dist/. Con GitHub Releases, la URL fija del
manifiesto es https://github.com/<org>/<repo>/releases/latest/download/update.json.
En la app, Notes comprueba al arrancar, en una fibra para no retrasar la ventana:
// Updates are a desktop concern (the stores update phone apps). In a fiber, so the window
// opens at once; a new instance replaces this one when the user accepts.
if (places.is_desktop()) {
let _ = spawn(fn() {
if (updates.offer()) {
exit(0);
}
});
}
/// Offers a newer version, if there is one. Returns true when a new instance was launched and
/// this one must exit.
pub fn offer() -> bool {
let _ = update.cleanup();
let url = manifest_url();
if (url == "") {
return false;
}
let release = match (update.check(url)) {
Result.Ok(Option.Some(r)) => r,
Result.Ok(Option.None) => return false,
Result.Err(e) => {
eprint(e);
return false;
},
};
let question = "You have "
+ update.current()
+ ". Install "
+ release.version
+ " and restart?";
if (!ui.confirm("Notes " + release.version + " is available", question, "Update", "Later")
.unwrap_or(false)) {
return false;
}
let installed = match (update.download(release)) {
Result.Ok(package) => update.apply(package),
Result.Err(e) => Result.Err(e),
};
match (installed) {
Result.Ok(_) => update.relaunch().is_ok(),
Result.Err(e) => {
eprint(e);
false
},
}
}
update.checkfalla si la firma no corresponde a la clave de la app: un manifiesto alterado o de otro publicador se rechaza.update.downloadverifica el tamaño y el SHA-256 antes de devolver el paquete.update.applyreemplaza el bundle de forma atómica;update.relauncharranca la versión nueva y esta termina. Al arrancar,update.cleanupborra los restos de la versión anterior.min_version(ray release --min-version) obliga a pasar por una versión intermedia cuando un salto directo no es seguro.
Ensayar sin publicar nada
Toda la cadena se puede ensayar en tu máquina con un servidor local. Con la app ya empaquetada en su versión actual:
# 1. una versión nueva, publicada en una carpeta y servida en local
sed -i '' 's/^version = "0.1.0"/version = "0.2.0"/' ray.toml
ray release -o dist --base-url http://127.0.0.1:8765/
ray serve dist --port 8765
# 2. la app instalada (0.1.0) apuntando a ese servidor
NOTES_UPDATE_URL=http://127.0.0.1:8765/update.json ./Notes.app/Contents/MacOS/Notes
La app pregunta, se actualiza a 0.2.0 y se reinicia. Si alteras update.json sin volver a firmarlo,
la rechaza con «the manifest signature does not verify».
4. Las tiendas móviles
iPhone. ray bundle --ios genera el proyecto Xcode con tu equipo de desarrollo
([ios] development_team, ver el capítulo móvil). Para publicar, ábrelo en Xcode,
elige Product → Archive y sube el archivo a App Store Connect desde el Organizer. La firma de
distribución la gestiona Xcode con tu cuenta de desarrollador.
Android. El proyecto Gradle trae la firma de release preparada; solo falta el keystore. Créalo una vez en la raíz del proyecto generado:
keytool -genkeypair -v -keystore release.jks -alias app -keyalg RSA -keysize 2048 -validity 10000
y escribe al lado keystore.properties:
storeFile=release.jks
storePassword=…
keyAlias=app
keyPassword=…
gradle bundleRelease # app/build/outputs/bundle/release/app-release.aab, para Google Play
gradle assembleRelease # app/build/outputs/apk/release/app-release.apk, para instalar directo
Los dos archivos sobreviven a regenerar el proyecto con ray bundle --android. No los subas al
control de versiones: con ese keystore se firman todas las versiones futuras de la app.
5. Servidores
Un servicio se entrega como un binario: ray build --native --release. Lleva dentro las plantillas
y los recursos embebidos, y se configura con variables de entorno, como en los capítulos de la
API y del sitio con React.
--target <triple>compila para otra plataforma, por ejemplox86_64-unknown-linux-gnudesde un Mac, si la toolchain de Rust tiene ese target.--without crypto,tls,sqlite,…deja fuera lo que el servicio no usa, para un binario y una imagen de contenedor más pequeños.- Si una función del programa cae fuera de lo que el compilador nativo sabe traducir, el build de
--releasese detiene y la nombra: un binario de producción no lleva funciones que fallan al llamarse. En un build sin--releasesolo avisa;ray build --native --no-stubste da el error también en desarrollo, para enterarte pronto. - Detrás de un proxy como nginx o Caddy, que pone el HTTPS, el binario está listo para producción.
En un contenedor, la imagen solo necesita el binario, compilado para Linux:
FROM debian:stable-slim
COPY notes-api /usr/local/bin/notes-api
ENV HOST=0.0.0.0 PORT=8080
EXPOSE 8080
CMD ["notes-api"]
6. Antes de publicar
| Comprueba | Cómo |
|---|---|
| La versión está subida | version en el ray.toml: es la que comparan las actualizaciones |
| Los tests pasan también en nativo | ray test --native |
| La firma verifica | codesign --verify y spctl --assess en macOS |
| La actualización funciona | el ensayo en local de la sección 3, desde la versión anterior |
| Las claves tienen copia | la clave de actualizaciones y el keystore de Android: sin ellos no hay más versiones |
| Los secretos no están en el repositorio | keystore.properties, el .pfx y ~/.ray/keys/ quedan fuera |
Siguiente paso
Con esto termina el recorrido por los capítulos. Para otros casos de uso, Más ejemplos reúne las apps de la organización ray-language, con su código completo.