Files
brenden 682afacd30 Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 +
tools/ci) with a "Connect to a Different Server" menu item and popout
that lets the desktop app switch to any self-hosted Fluxer instance,
plus fixes for well-known discovery on single-domain self-hosted
deployments and a false-positive ERR_ABORTED on same-origin client
redirects during the switch. Defaults to chat.fluxr.chat and uses an
isolated userData directory from the official build.
2026-07-01 18:22:43 -04:00

547 lines
21 KiB
Rust

use std::{env, fs, path};
fn var(k: &str) -> String {
env::var(k).unwrap()
}
fn use_masm() -> bool {
env::var("CARGO_CFG_TARGET_ENV") == Ok("msvc".to_string())
&& var("CARGO_CFG_TARGET_ARCH") == "x86_64"
&& var("HOST").contains("-windows-")
}
fn include_amx() -> bool {
let arch = var("CARGO_CFG_TARGET_ARCH");
let os = var("CARGO_CFG_TARGET_OS");
os == "macos"
|| (env::var("CARGO_FEATURE_APPLE_AMX_IOS").is_ok() && os == "ios" && arch == "aarch64")
}
fn include_sme() -> bool {
let arch = var("CARGO_CFG_TARGET_ARCH");
let os = var("CARGO_CFG_TARGET_OS");
arch == "aarch64" && (os == "macos" || os == "linux")
}
// Probe whether the target assembler can actually assemble SME instructions.
// Old binutils (e.g. the Debian stretch aarch64 cross-toolchain used in CI)
// predate SME and reject the mnemonics even with `.arch armv9-a+sme2`, which
// breaks the build. When the probe fails we skip the SME kernels entirely;
// the matching `tract_sme` cfg keeps the Rust side from referencing the
// (now absent) kernel symbols, and dispatch falls back to the portable path.
fn assembler_supports_sme() -> bool {
cc::Build::new()
.file("arm64/sme/dummy_sme.S")
.cargo_metadata(false)
.cargo_warnings(false)
.warnings(false)
.try_compile("tract_sme_probe")
.is_ok()
}
// Probe whether the target assembler can encode FEAT_DotProd `sdot` (the
// indexed int8 form used by arm64simd_mmm_i32_8x8_dot). Old binutils — notably
// the Debian stretch aarch64 cross-toolchain in CI — predate FEAT_DotProd and
// reject `.cpu ...+dotprod` / `sdot` outright. When the probe fails we skip the
// SDOT kernel and the `tract_arm64_dotprod` cfg; the runtime falls back to the
// SMLAL 8x8 i32 kernel.
fn assembler_supports_dotprod() -> bool {
cc::Build::new()
.file("arm64/arm64simd/dummy_dotprod.S")
.cargo_metadata(false)
.cargo_warnings(false)
.warnings(false)
.try_compile("tract_dotprod_probe")
.is_ok()
}
// Probe whether the target assembler can encode `vpdpbusd ymm` (AVX-512 VNNI
// with AVX-512 VL, i.e. the 256-bit form). binutils gained this in ~2.30
// (2018); the Debian stretch toolchain ships 2.28 and rejects the mnemonic.
// When the probe fails we skip the VNNI kernel and the `tract_avx512vnni` cfg;
// the runtime falls back to the AVX2 i32 path.
fn assembler_supports_avx512vnni() -> bool {
cc::Build::new()
.file("x86_64/avx512vnni/dummy_vnni.S")
.cargo_metadata(false)
.cargo_warnings(false)
.warnings(false)
.try_compile("tract_avx512vnni_probe")
.is_ok()
}
fn include_sve() -> bool {
// SVE/SVE2 lives on ARMv9 server/mobile cores (Neoverse V1+/N2+, Cortex-X2+,
// Graviton 3/4) — Linux aarch64. No Apple silicon has SVE.
var("CARGO_CFG_TARGET_ARCH") == "aarch64" && var("CARGO_CFG_TARGET_OS") == "linux"
}
// Probe whether the C compiler supports SVE intrinsics (arm_sve.h + `+sve`).
// Old toolchains (e.g. the Debian stretch cross-gcc) lack them; when the probe
// fails we skip the SVE kernels and the `tract_sve` cfg, so the Rust side never
// references the (absent) symbols and dispatch falls back to NEON.
fn compiler_supports_sve() -> bool {
let out_dir = path::PathBuf::from(var("OUT_DIR"));
let probe = out_dir.join("sve_probe.c");
fs::write(&probe, "#include <arm_sve.h>\nint p(void){ return (int)svcntw(); }\n").unwrap();
cc::Build::new()
.file(&probe)
.flag("-march=armv8.2-a+sve")
.cargo_metadata(false)
.cargo_warnings(false)
.warnings(false)
.try_compile("tract_sve_probe")
.is_ok()
}
fn jump_table() -> Vec<String> {
println!("cargo:rerun-if-changed=src/frame/mmm/fuse.rs");
std::fs::read_to_string("src/frame/mmm/fuse.rs")
.unwrap()
.lines()
.filter(|l| l.contains("// jump_to:"))
.map(|l| l.split("jump_to:").nth(1).unwrap().to_owned())
.collect()
}
#[derive(Clone, Debug)]
struct ConfigForHalf {
extra_flags: Vec<String>,
needs_pragma: bool,
}
impl ConfigForHalf {
fn new(extra_flags: Vec<String>, needs_pragma: bool) -> ConfigForHalf {
ConfigForHalf { extra_flags, needs_pragma }
}
fn all() -> Vec<ConfigForHalf> {
let mut configs = vec![];
for extra_flags in
[vec![], vec!["-march=armv8.2-a".to_string()], vec!["-mcpu=cortex-a55".to_string()]]
{
for needs_pragma in [false, true] {
configs.push(ConfigForHalf::new(extra_flags.clone(), needs_pragma))
}
}
configs
}
fn cc(&self) -> cc::Build {
let mut cc = cc::Build::new();
for flag in &self.extra_flags {
cc.flag(flag);
}
cc
}
fn works(&self) -> bool {
let filename = if self.needs_pragma {
"arm64/arm64fp16/dummy_fmla_pragma.S"
} else {
"arm64/arm64fp16/dummy_fmla_no_pragma.S"
};
self.cc().file(filename).try_compile("dummy").is_ok()
}
pub fn probe() -> Option<ConfigForHalf> {
Self::all().iter().find(|c| c.works()).cloned()
}
}
fn main() {
let target = var("TARGET");
let arch = var("CARGO_CFG_TARGET_ARCH");
let os = var("CARGO_CFG_TARGET_OS");
let out_dir = path::PathBuf::from(var("OUT_DIR"));
let suffix = env!("CARGO_PKG_VERSION").replace(['-', '.'], "_");
make_extern_kernel_decl_macro(&out_dir, &suffix);
// `tract_sme` is set below only when both include_sme() and the assembler
// SME probe succeed; declare it so rustc's unexpected-cfg lint stays quiet.
println!("cargo:rustc-check-cfg=cfg(tract_sme)");
// Set below only when include_sve() and the SVE compiler probe both pass.
println!("cargo:rustc-check-cfg=cfg(tract_sve)");
// Set below only when the aarch64 assembler probe for `sdot` passes.
println!("cargo:rustc-check-cfg=cfg(tract_arm64_dotprod)");
// Set below only when the x86_64 assembler probe for vpdpbusd ymm passes.
println!("cargo:rustc-check-cfg=cfg(tract_avx512vnni)");
match arch.as_ref() {
"x86_64" => {
let mut files = preprocess_files("x86_64/fma", &[], &suffix, false);
// The VNNI kernel is compiled separately (conditional on a probe) to
// avoid breaking old assemblers. Remove it from the main file list.
files.retain(|f| {
!f.file_name().and_then(|n| n.to_str()).map_or(false, |n| n.contains("avx512vnni"))
});
files.extend(preprocess_files("x86_64/avx512", &[], &suffix, false));
if os == "windows" {
if use_masm() {
let mut lib_exe = cc::windows_registry::find(&target, "lib.exe")
.expect("Could not find lib.exe");
lib_exe
.arg(format!("/out:{}", out_dir.join("x86_64_fma.lib").to_str().unwrap()));
for f in files {
let mut obj = f.clone();
obj.set_extension("o");
let mut ml_exe = cc::windows_registry::find(&target, "ml64.exe")
.expect("Could not find ml64.exe");
if !ml_exe
.arg("/Fo")
.arg(&obj)
.arg("/c")
.arg(&f)
.status()
.unwrap()
.success()
{
for (i, l) in std::fs::read_to_string(&f).unwrap().lines().enumerate() {
println!("{i:8} {l}");
}
panic!();
}
lib_exe.arg(obj);
}
assert!(lib_exe.status().unwrap().success());
println!("cargo:rustc-link-search=native={}", out_dir.to_str().unwrap());
println!("cargo:rustc-link-lib=static=x86_64_fma");
} else {
cc::Build::new()
.files(files)
.flag("-mfma")
.flag("-mf16c")
.compile("x86_64_fma");
// clang at least (dunno about gcc) outputs .asm files in the
// root directory that we need to clean up so we don't pollute
// the build output/working directory
let _ = fs::remove_file("fma_mmm_f32_16x6.asm");
let _ = fs::remove_file("fma_mmm_i32_8x8.asm");
let _ = fs::remove_file("fma_sigmoid_f32.asm");
let _ = fs::remove_file("fma_tanh_f32.asm");
}
} else {
cc::Build::new().files(files).flag("-mfma").compile("x86_64_fma");
}
// VNNI kernel compiled separately so old assemblers (binutils < 2.30,
// e.g. Debian stretch) that can't encode `vpdpbusd ymm` don't break
// the whole x86_64 build. The `tract_avx512vnni` cfg gates the
// matching Rust extern declarations and dispatch registration.
//
// The template stays in x86_64/fma/ (alongside dispatcher.j2 and the
// other partials it includes) so the jinja env can resolve its includes.
if assembler_supports_avx512vnni() {
let tmpl = path::Path::new("x86_64/fma/avx512vnni_mmm_i32_8x8.S.j2");
let out = out_dir.join(format!("avx512vnni_mmm_i32_8x8_{suffix}.S"));
preprocess_file(tmpl, &out, &[], &suffix, false);
cc::Build::new().file(&out).flag("-mfma").compile("x86_64_avx512vnni");
println!("cargo:rustc-cfg=tract_avx512vnni");
}
}
"arm" | "armv7" => {
let files = preprocess_files("arm32/armvfpv2", &[], &suffix, false);
cc::Build::new().files(files).flag("-marm").flag("-mfpu=vfp").compile("armvfpv2");
let files = preprocess_files(
"arm32/armv7neon",
&[("core", vec!["cortexa7", "cortexa9", "generic"])],
&suffix,
false,
);
cc::Build::new().files(files).flag("-marm").flag("-mfpu=neon").compile("armv7neon");
}
"aarch64" => {
let mut files = preprocess_files(
"arm64/arm64simd",
&[("core", vec!["a53", "a55", "gen"])],
&suffix,
false,
);
// The SDOT kernel is compiled separately (conditional on a probe) so
// old assemblers (binutils < 2.30, e.g. Debian stretch) that can't
// encode `sdot` don't break the whole arm64simd build. Remove it
// from the main file list.
files.retain(|f| {
!f.file_name().and_then(|n| n.to_str()).map_or(false, |n| n.contains("_dot"))
});
cc::Build::new().files(files).compile("arm64simd");
// The template stays in arm64/arm64simd/ (alongside the jinja partials
// it includes) so the env can resolve its includes. The
// `tract_arm64_dotprod` cfg gates the matching Rust extern + dispatch.
if assembler_supports_dotprod() {
let tmpl = path::Path::new("arm64/arm64simd/arm64simd_mmm_i32_8x8_dot.S.j2");
let out = out_dir.join(format!("arm64simd_mmm_i32_8x8_dot_{suffix}.S"));
preprocess_file(tmpl, &out, &[], &suffix, false);
cc::Build::new().file(&out).compile("arm64simd_dot");
println!("cargo:rustc-cfg=tract_arm64_dotprod");
}
if include_amx() {
let files = preprocess_files("arm64/apple_amx", &[], &suffix, false);
cc::Build::new().files(files).compile("appleamx");
}
if include_sme() && assembler_supports_sme() {
let files = preprocess_files("arm64/sme", &[], &suffix, false);
cc::Build::new().files(files).compile("sme");
println!("cargo:rustc-cfg=tract_sme");
}
if include_sve() && compiler_supports_sve() {
// VLA SVE kernels (C intrinsics, fixed symbols — not suffix-templated).
cc::Build::new()
.file("arm64/sve/sve_mmm_f32.c")
.file("arm64/sve/sve_mmv_f32_64x1.c")
.file("arm64/sve/sve_mmm_i32.c")
.file("arm64/sve/sve_mmm_i32_64x1.c")
.flag("-march=armv8.2-a+sve")
.compile("tract_sve_kernels");
// f16 kernels need native FP16 arithmetic (+fp16); compiled
// separately so the +sve-only kernels above never gain fp16
// codegen. Runtime-gated on has_fp16() as well as SVE2.
cc::Build::new()
.file("arm64/sve/sve_mmm_f16.c")
.file("arm64/sve/sve_mmv_f16_64x1.c")
.flag("-march=armv8.2-a+sve+fp16")
.compile("tract_sve_f16_kernels");
println!("cargo:rustc-cfg=tract_sve");
}
if std::env::var("CARGO_FEATURE_NO_FP16").is_err() {
let config =
ConfigForHalf::probe().expect("No configuration found for fp16 support");
let files = preprocess_files(
"arm64/arm64fp16",
&[("core", vec!["a55", "gen"])],
&suffix,
config.needs_pragma,
);
config.cc().files(files).compile("arm64fp16")
}
}
_ => {}
}
}
type Variant = (&'static str, Vec<&'static str>);
fn preprocess_files(
input: impl AsRef<path::Path>,
variants: &[Variant],
suffix: &str,
needs_pragma: bool,
) -> Vec<path::PathBuf> {
let out_dir = path::PathBuf::from(var("OUT_DIR"));
let mut files = vec![];
let dir_entries = {
let mut dir_entries: Vec<fs::DirEntry> =
input.as_ref().read_dir().unwrap().map(|f| f.unwrap()).collect();
dir_entries.sort_by_key(|a| a.path());
dir_entries
};
for f in dir_entries {
let fname = f.path().file_name().unwrap().to_str().unwrap().to_owned();
if fname.ends_with(".S.j2") {
let tmpl_file = fname;
let concerned_variants: Vec<&Variant> =
variants.iter().filter(|v| tmpl_file.contains(v.0)).collect();
let expanded_variants = concerned_variants.iter().map(|pair| pair.1.len()).product();
for v in 0..expanded_variants {
let mut tmpl_file = tmpl_file.clone();
let mut id = v;
let mut globals = vec![];
for variable in variants {
let key = variable.0;
let value = variable.1[id % variable.1.len()];
globals.push((key, value));
tmpl_file = tmpl_file.replace(key, value);
id /= variable.1.len();
}
let out_name = tmpl_file.strip_suffix(".S.j2").unwrap();
let file = out_dir.join(format!("{out_name}.S"));
preprocess_file(f.path(), &file, &globals, suffix, needs_pragma);
files.push(file);
}
}
}
files
}
/// Replace `//` assembly comments with `;` for MSVC assembler.
/// Must be called on rendered output (not on Jinja2 source, which uses `//` for integer division).
fn strip_comments(s: &str) -> String {
s.lines().map(|line| line.replace("//", ";")).collect::<Vec<String>>().join("\n")
}
fn preprocess_file(
template: impl AsRef<path::Path>,
output: impl AsRef<path::Path>,
variants: &[(&'static str, &'static str)],
suffix: &str,
needs_pragma: bool,
) {
println!("cargo:rerun-if-changed={}", template.as_ref().to_string_lossy());
let family = var("CARGO_CFG_TARGET_FAMILY");
let os = var("CARGO_CFG_TARGET_OS");
let msvc = use_masm();
println!("cargo:rerun-if-changed={}", template.as_ref().to_string_lossy());
let input = fs::read_to_string(&template).unwrap();
let l = if os == "macos" {
"L"
} else if family == "windows" {
""
} else {
".L"
}
.to_owned();
let long = if msvc { "dd" } else { ".long" };
let g = if os == "macos" || os == "ios" || os == "watchos" || os == "tvos" { "_" } else { "" };
let align = if msvc { "align" } else { ".align" };
let offset = if msvc { "offset" } else { "rip + " };
let mut env = build_jinja_env(template.as_ref().parent().unwrap());
let main_name = template.as_ref().file_name().unwrap().to_str().unwrap();
env.add_template_owned(main_name.to_string(), input).unwrap_or_else(|e| {
eprintln!("Parsing {}: {e}", template.as_ref().to_string_lossy());
panic!();
});
let tmpl = env.get_template(main_name).unwrap();
let mut ctx = std::collections::BTreeMap::<String, minijinja::Value>::new();
ctx.insert("msvc".into(), msvc.into());
ctx.insert("needs_pragma".into(), needs_pragma.into());
ctx.insert("family".into(), family.into());
ctx.insert("os".into(), os.into());
ctx.insert("L".into(), l.into());
ctx.insert("G".into(), g.into());
ctx.insert("suffix".into(), suffix.into());
ctx.insert("long".into(), long.into());
ctx.insert("jump_table".into(), minijinja::Value::from_serialize(&jump_table()));
ctx.insert("align".into(), align.into());
ctx.insert("offset".into(), offset.into());
for (k, v) in variants {
ctx.insert(k.to_string(), (*v).into());
}
if include_amx() {
let (amx_set, amx_clr) = amx_globals();
ctx.insert("AMX_SET".into(), amx_set.into());
ctx.insert("AMX_CLR".into(), amx_clr.into());
}
match tmpl.render(&ctx) {
Ok(rendered) => {
let rendered = if msvc { strip_comments(&rendered) } else { rendered };
fs::write(&output, rendered).unwrap();
}
Err(e) => {
eprintln!("Rendering {}: {e:#}", template.as_ref().to_string_lossy());
panic!();
}
}
}
fn build_jinja_env(template_dir: &path::Path) -> minijinja::Environment<'static> {
let mut env = minijinja::Environment::new();
// Custom filters
env.add_filter("float16", float16_filter);
env.add_filter("setting", setting_filter);
env.add_filter("lsl", lsl_filter);
env.add_filter("u", unsigned_filter);
// Custom function: amx("op", gpr) -> assembly .word directive
env.add_function("amx", amx_function);
// Load all partials (.j2 = Jinja2 macros/includes, .S.raw = raw assembly with brace escaping)
for f in walkdir::WalkDir::new(template_dir) {
let f = f.unwrap();
if f.path().is_dir() {
continue;
}
let fname = f.path().file_name().unwrap().to_str().unwrap().to_owned();
let text = std::fs::read_to_string(f.path()).unwrap_or_else(|_| panic!("file {f:?}"));
let text = if fname.ends_with(".S.raw") {
Some(text.replace("{{", "{").replace("}}", "}"))
} else if fname.ends_with(".j2") && !fname.ends_with(".S.j2") {
Some(text)
} else {
None
};
if let Some(text) = text {
let key = f
.path()
.strip_prefix(template_dir)
.unwrap()
.to_str()
.unwrap()
.to_owned()
.replace('\\', "/");
println!("cargo:rerun-if-changed={}", f.path().to_string_lossy().replace('\\', "/"));
env.add_template_owned(key, text).unwrap_or_else(|e| {
eprintln!("Parsing partial {}: {e}", f.path().to_string_lossy());
panic!();
});
}
}
env
}
fn make_extern_kernel_decl_macro(out_dir: &path::Path, suffix: &str) {
let macro_decl = r#"
macro_rules! extern_kernel {
(fn $name: ident($($par_name:ident : $par_type: ty ),*) -> $rv: ty) => {
paste! {
unsafe extern "C" { pub fn [<$name _ _suffix>]($(par_name: $par_type),*) -> $rv; }
pub use [<$name _ _suffix>] as $name;
}
}
}"#
.replace("_suffix", suffix);
std::fs::write(out_dir.join("extern_kernel_macro.rs"), macro_decl).unwrap();
}
// --- Custom filters and functions ---
fn float16_filter(value: f64) -> String {
let bits = half::f16::from_f32(value as f32).to_bits();
format!(".short {bits}")
}
fn setting_filter(value: i64, bit: i64) -> String {
let result = value | (1i64 << bit);
result.to_string()
}
fn lsl_filter(value: i64, shift: i64) -> String {
let result = value << shift;
result.to_string()
}
fn unsigned_filter(value: i64) -> String {
let result = value as u64;
result.to_string()
}
fn amx_function(op: String, gpr: u32) -> String {
let ops = [
"ldx", "ldy", "stx", "sty", "ldz", "stz", "ldzi", "stzi", "extrx", "extry", "fma64",
"fms64", "fma32", "fms32", "mac16", "fma16", "fms16", "setclr", "vecint", "vecfp",
"matint", "matfp", "genlut",
];
let op_id = ops.iter().position(|x| *x == op.as_str()).unwrap();
format!(".word 0x{:x} \t\t\t\t// AMX {} x{}\n", 0x201000 + (op_id << 5) + gpr as usize, op, gpr)
}
fn amx_nop_op_imm5(op: usize, imm5: usize) -> String {
format!("nop\nnop\nnop\n.word 0x{:x}\n", (0x201000 + (op << 5) + imm5))
}
fn amx_globals() -> (String, String) {
(amx_nop_op_imm5(17, 0), amx_nop_op_imm5(17, 1))
}