Anki/qt/launcher/src/bin/build_win.rs
Damien Elmes de7de82f76 Refactor launcher + various tweaks
- Launcher can now be accessed via Tools>Upgrade/Downgrade
- Anki closes automatically on update
- When launcher not available, show update link like in the past
- It appears that access to the modern console host requires an app
to be built with the windows console subsystem, so we introduce an
extra anki-console.exe binary to relaunch ourselves with. Solves
https://forums.ankiweb.net/t/new-online-installer-launcher/62745/50
- Windows now requires you to close the terminal like on a Mac,
as I couldn't figure out how to have it automatically close. Suggestions
welcome!
- Reduce the amount of duplicate/near-duplicate code in the various
platform files, and improve readability
- Add a helper to install the current code into the launcher env
- Fix cargo test failing to build on ARM64 Windows
2025-06-27 16:10:12 +07:00

302 lines
8.5 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::env;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use anki_io::copy_file;
use anki_io::create_dir_all;
use anki_io::remove_dir_all;
use anki_io::write_file;
use anki_process::CommandExt;
use anyhow::Result;
const OUTPUT_DIR: &str = "../../../out/launcher";
const LAUNCHER_EXE_DIR: &str = "../../../out/launcher_exe";
const NSIS_DIR: &str = "../../../out/nsis";
const CARGO_TARGET_DIR: &str = "../../../out/rust";
const NSIS_PATH: &str = "C:\\Program Files (x86)\\NSIS\\makensis.exe";
fn main() -> Result<()> {
println!("Building Windows launcher...");
let output_dir = PathBuf::from(OUTPUT_DIR);
let launcher_exe_dir = PathBuf::from(LAUNCHER_EXE_DIR);
let nsis_dir = PathBuf::from(NSIS_DIR);
setup_directories(&output_dir, &launcher_exe_dir, &nsis_dir)?;
build_launcher_binary()?;
extract_nsis_plugins()?;
copy_files(&output_dir)?;
sign_binaries(&output_dir)?;
copy_nsis_files(&nsis_dir)?;
build_uninstaller(&output_dir, &nsis_dir)?;
sign_file(&output_dir.join("uninstall.exe"))?;
generate_install_manifest(&output_dir)?;
build_installer(&output_dir, &nsis_dir)?;
sign_file(&PathBuf::from("../../../out/launcher_exe/anki-install.exe"))?;
println!("Build completed successfully!");
println!("Output directory: {}", output_dir.display());
println!("Installer: ../../../out/launcher_exe/anki-install.exe");
Ok(())
}
fn setup_directories(output_dir: &Path, launcher_exe_dir: &Path, nsis_dir: &Path) -> Result<()> {
println!("Setting up directories...");
// Remove existing output directories
if output_dir.exists() {
remove_dir_all(output_dir)?;
}
if launcher_exe_dir.exists() {
remove_dir_all(launcher_exe_dir)?;
}
if nsis_dir.exists() {
remove_dir_all(nsis_dir)?;
}
// Create output directories
create_dir_all(output_dir)?;
create_dir_all(launcher_exe_dir)?;
create_dir_all(nsis_dir)?;
Ok(())
}
fn build_launcher_binary() -> Result<()> {
println!("Building launcher binary...");
env::set_var("CARGO_TARGET_DIR", CARGO_TARGET_DIR);
Command::new("cargo")
.args([
"build",
"-p",
"launcher",
"--release",
"--target",
"x86_64-pc-windows-msvc",
])
.ensure_success()?;
Ok(())
}
fn extract_nsis_plugins() -> Result<()> {
println!("Extracting NSIS plugins...");
// Change to the anki root directory and run tools/ninja.bat
Command::new("cmd")
.args([
"/c",
"cd",
"/d",
"..\\..\\..\\",
"&&",
"tools\\ninja.bat",
"extract:nsis_plugins",
])
.ensure_success()?;
Ok(())
}
fn copy_files(output_dir: &Path) -> Result<()> {
println!("Copying binaries...");
// Copy launcher binary as anki.exe
let launcher_src =
PathBuf::from(CARGO_TARGET_DIR).join("x86_64-pc-windows-msvc/release/launcher.exe");
let launcher_dst = output_dir.join("anki.exe");
copy_file(&launcher_src, &launcher_dst)?;
// Copy anki-console binary
let console_src =
PathBuf::from(CARGO_TARGET_DIR).join("x86_64-pc-windows-msvc/release/anki-console.exe");
let console_dst = output_dir.join("anki-console.exe");
copy_file(&console_src, &console_dst)?;
// Copy uv.exe and uvw.exe
let uv_src = PathBuf::from("../../../out/extracted/uv/uv.exe");
let uv_dst = output_dir.join("uv.exe");
copy_file(&uv_src, &uv_dst)?;
let uv_src = PathBuf::from("../../../out/extracted/uv/uvw.exe");
let uv_dst = output_dir.join("uvw.exe");
copy_file(&uv_src, &uv_dst)?;
println!("Copying support files...");
// Copy pyproject.toml
copy_file("../pyproject.toml", output_dir.join("pyproject.toml"))?;
// Copy .python-version
copy_file(
"../../../.python-version",
output_dir.join(".python-version"),
)?;
Ok(())
}
fn sign_binaries(output_dir: &Path) -> Result<()> {
sign_file(&output_dir.join("anki.exe"))?;
sign_file(&output_dir.join("anki-console.exe"))?;
sign_file(&output_dir.join("uv.exe"))?;
Ok(())
}
fn sign_file(file_path: &Path) -> Result<()> {
let codesign = env::var("CODESIGN").unwrap_or_default();
if codesign != "1" {
println!(
"Skipping code signing for {} (CODESIGN not set to 1)",
file_path.display()
);
return Ok(());
}
let signtool_path = find_signtool()?;
println!("Signing {}...", file_path.display());
Command::new(&signtool_path)
.args([
"sign",
"/sha1",
"dccfc6d312fc0432197bb7be951478e5866eebf8",
"/fd",
"sha256",
"/tr",
"http://time.certum.pl",
"/td",
"sha256",
"/v",
])
.arg(file_path)
.ensure_success()?;
Ok(())
}
fn find_signtool() -> Result<PathBuf> {
println!("Locating signtool.exe...");
let output = Command::new("where")
.args([
"/r",
"C:\\Program Files (x86)\\Windows Kits",
"signtool.exe",
])
.utf8_output()?;
// Find signtool.exe with "arm64" in the path (as per original batch logic)
for line in output.stdout.lines() {
if line.contains("\\arm64\\") {
let signtool_path = PathBuf::from(line.trim());
println!("Using signtool: {}", signtool_path.display());
return Ok(signtool_path);
}
}
anyhow::bail!("Could not find signtool.exe with arm64 architecture");
}
fn generate_install_manifest(output_dir: &Path) -> Result<()> {
println!("Generating install manifest...");
let mut manifest_content = String::new();
let entries = anki_io::read_dir_files(output_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if let Some(file_name) = path.file_name() {
let file_name_str = file_name.to_string_lossy();
// Skip manifest file and uninstaller (can't delete itself)
if file_name_str != "anki.install-manifest" && file_name_str != "uninstall.exe" {
if let Ok(relative_path) = path.strip_prefix(output_dir) {
// Convert to Windows-style backslashes for NSIS
let windows_path = relative_path.display().to_string().replace('/', "\\");
// Use Windows line endings (\r\n) as expected by NSIS
manifest_content.push_str(&format!("{}\r\n", windows_path));
}
}
}
}
write_file(output_dir.join("anki.install-manifest"), manifest_content)?;
Ok(())
}
fn copy_nsis_files(nsis_dir: &Path) -> Result<()> {
println!("Copying NSIS support files...");
// Copy anki.template.nsi as anki.nsi
copy_file("anki.template.nsi", nsis_dir.join("anki.nsi"))?;
// Copy fileassoc.nsh
copy_file("fileassoc.nsh", nsis_dir.join("fileassoc.nsh"))?;
// Copy nsProcess.dll
copy_file(
"../../../out/extracted/nsis_plugins/nsProcess.dll",
nsis_dir.join("nsProcess.dll"),
)?;
Ok(())
}
fn build_uninstaller(output_dir: &Path, nsis_dir: &Path) -> Result<()> {
println!("Building uninstaller...");
let mut flags = vec!["-V3", "-DWRITE_UNINSTALLER"];
if env::var("NO_COMPRESS").unwrap_or_default() == "1" {
println!("NO_COMPRESS=1 detected, disabling compression");
flags.push("-DNO_COMPRESS");
}
run_nsis(
&PathBuf::from("anki.nsi"),
&flags,
nsis_dir, // Run from nsis directory
)?;
// Copy uninstaller from nsis directory to output directory
copy_file(
nsis_dir.join("uninstall.exe"),
output_dir.join("uninstall.exe"),
)?;
Ok(())
}
fn build_installer(_output_dir: &Path, nsis_dir: &Path) -> Result<()> {
println!("Building installer...");
let mut flags = vec!["-V3"];
if env::var("NO_COMPRESS").unwrap_or_default() == "1" {
println!("NO_COMPRESS=1 detected, disabling compression");
flags.push("-DNO_COMPRESS");
}
run_nsis(
&PathBuf::from("anki.nsi"),
&flags,
nsis_dir, // Run from nsis directory
)?;
Ok(())
}
fn run_nsis(script_path: &Path, flags: &[&str], working_dir: &Path) -> Result<()> {
let mut cmd = Command::new(NSIS_PATH);
cmd.args(flags).arg(script_path).current_dir(working_dir);
cmd.ensure_success()?;
Ok(())
}