cargo-truce 0.20.0

Build tool for truce audio plugins
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
//! `cargo truce build` — produce per-format bundles in `target/bundles/`
//! without installing.
//!
//! Every format flag (`--clap` / `--vst3` / `--vst2` / `--lv2` / `--au2`
//! / `--au3` / `--aax`) produces a self-contained, signed bundle in
//! `target/bundles/`; `cargo truce install` then copies those bundles
//! to system paths.

#[cfg(target_os = "macos")]
use crate::commands::package::stage::stage_au2;
use crate::commands::package::stage::{lv2_slug, stage_clap, stage_lv2, stage_vst2, stage_vst3};
use crate::util::fs_ctx;
use crate::{
    PluginDef, Res, cargo_build, deployment_target, detect_default_features, load_config,
    project_root, release_lib,
};
use std::process::Command;

pub(crate) fn cmd_build(args: &[String]) -> Res {
    let config = load_config()?;

    let mut clap = false;
    let mut vst3 = false;
    let mut vst2 = false;
    let mut lv2 = false;
    let mut au2 = false;
    let mut au3 = false;
    let mut aax = false;
    let mut shell_mode = false;
    let mut debug = false;
    let mut plugin_filter: Option<String> = None;

    let mut i = 0;
    while i < args.len() {
        match args[i].as_str() {
            "--clap" => clap = true,
            "--vst3" => vst3 = true,
            "--vst2" => vst2 = true,
            "--lv2" => lv2 = true,
            "--au2" => au2 = true,
            "--au3" => au3 = true,
            "--aax" => aax = true,
            "--shell" => shell_mode = true,
            "--debug" => debug = true,
            "-p" => {
                i += 1;
                plugin_filter = Some(
                    args.get(i)
                        .cloned()
                        .ok_or("-p requires a plugin crate name")?,
                );
            }
            other => return Err(format!("unknown flag: {other}").into()),
        }
        i += 1;
    }

    // No format flags → enable every format in the project's default
    // features, mirroring `install`'s discovery rule.
    if !clap && !vst3 && !vst2 && !lv2 && !au2 && !au3 && !aax {
        let available = detect_default_features();
        clap = available.contains("clap");
        vst3 = available.contains("vst3");
        vst2 = available.contains("vst2");
        lv2 = available.contains("lv2");
        // AU is macOS-only at runtime, but flip the flags on every platform
        // so the build path can emit per-plugin skip lines for Linux /
        // Windows users with `"au"` in their `[features].default`.
        au2 = available.contains("au");
        au3 = available.contains("au");
        aax = available.contains("aax");
    }

    let plugins: Vec<&PluginDef> = if let Some(ref f) = plugin_filter {
        let matched: Vec<_> = config
            .plugin
            .iter()
            .filter(|p| p.crate_name == *f)
            .collect();
        if matched.is_empty() {
            return Err(format!(
                "No plugin with crate name '{f}'. Available: {}",
                config
                    .plugin
                    .iter()
                    .map(|p| p.crate_name.as_str())
                    .collect::<Vec<_>>()
                    .join(", ")
            )
            .into());
        }
        matched
    } else {
        config.plugin.iter().collect()
    };
    if plugins.is_empty() {
        return Err("no matching plugins".into());
    }

    // In shell mode `--debug` selects the *logic* profile (the dylib
    // the shell dlopens at runtime). The shell itself goes to
    // `target/shell/` via the custom `[profile.shell]`; default logic
    // profile is release for better DSP perf, debug for fast iteration.
    let logic_profile = if debug { "debug" } else { "release" };

    if shell_mode {
        // Bail early if the user's Cargo.toml is missing
        // `[profile.shell]` — clearer than cargo's downstream
        // "profile `shell` is not declared" message.
        crate::verify_shell_profile_declared()?;
        crate::set_build_profile("shell");
        // Bake the logic profile into the shell binary via truce-build
        // → `cargo:rustc-env=TRUCE_LOGIC_PROFILE=...`. The shell's
        // runtime dylib lookup uses option_env! to read it.
        // 2024-edition: `set_var` is unsafe (process-wide env state).
        // Single-threaded build path here, so no race; main thread is
        // the only writer.
        unsafe {
            std::env::set_var("TRUCE_LOGIC_PROFILE", logic_profile);
        }
    } else {
        crate::set_debug_profile(debug);
    }

    // AU v3 + shell is unreliable due to the appex sandbox. Same
    // warning as `cargo truce install --shell --au3`.
    if shell_mode && au3 && cfg!(target_os = "macos") {
        eprintln!(
            "note: AU v3 + --shell is unreliable. The appex sandbox blocks dlopen of \
             target/<profile>/lib<crate>.dylib, so hot-reload won't fire. Use --au2 \
             for hot-reload iteration."
        );
    }

    let root = project_root();
    let dt = &deployment_target();
    let bundles_dir = crate::target_dir(&root).join("bundles");
    fs_ctx::create_dir_all(&bundles_dir)?;

    let extra_features: Vec<&str> = if shell_mode { vec!["shell"] } else { vec![] };

    // --- Build dylibs per format ---
    //
    // Each format gets its own cargo build with `--features {format}`.
    // Because every build overwrites `target/release/lib{stem}.dylib`,
    // we immediately copy the output to a format-suffixed path
    // (`_clap`, `_vst3`, `_vst2`, ...) that the stage/install steps
    // read from. Same pattern across all formats — keeps each path
    // self-contained with no implicit ordering.
    if clap {
        let mut feats: Vec<&str> = vec!["clap"];
        for f in &extra_features {
            feats.push(f);
        }
        let combined = feats.join(",");
        let label = if extra_features.is_empty() {
            "Building CLAP...".to_string()
        } else {
            format!("Building CLAP ({})...", extra_features.join(" + "))
        };
        crate::vprintln!("{label}");
        for p in &plugins {
            let mut env_pairs: Vec<(&str, &str)> = Vec::new();
            if let Some(n) = p.clap_name.as_deref() {
                env_pairs.push(("TRUCE_CLAP_NAME_OVERRIDE", n));
            }
            cargo_build(
                &env_pairs,
                &[
                    "-p",
                    &p.crate_name,
                    "--no-default-features",
                    "--features",
                    &combined,
                ],
                dt,
            )?;
            let src = release_lib(&root, &p.dylib_stem());
            let dst = release_lib(&root, &format!("{}_clap", p.dylib_stem()));
            if src.exists() {
                fs_ctx::copy(&src, &dst)?;
            }
        }
    }

    if vst3 {
        let mut feats: Vec<&str> = vec!["vst3"];
        for f in &extra_features {
            feats.push(f);
        }
        let combined = feats.join(",");
        let label = if extra_features.is_empty() {
            "Building VST3...".to_string()
        } else {
            format!("Building VST3 ({})...", extra_features.join(" + "))
        };
        crate::vprintln!("{label}");
        for p in &plugins {
            let mut env_pairs: Vec<(&str, &str)> = Vec::new();
            if let Some(n) = p.vst3_name.as_deref() {
                env_pairs.push(("TRUCE_VST3_NAME_OVERRIDE", n));
            }
            cargo_build(
                &env_pairs,
                &[
                    "-p",
                    &p.crate_name,
                    "--no-default-features",
                    "--features",
                    &combined,
                ],
                dt,
            )?;
            let src = release_lib(&root, &p.dylib_stem());
            let dst = release_lib(&root, &format!("{}_vst3", p.dylib_stem()));
            if src.exists() {
                fs_ctx::copy(&src, &dst)?;
            }
        }
    }

    if vst2 {
        crate::vprintln!("Building VST2...");
        for p in &plugins {
            let mut env_pairs: Vec<(&str, &str)> = Vec::new();
            if let Some(n) = p.vst2_name.as_deref() {
                env_pairs.push(("TRUCE_VST2_NAME_OVERRIDE", n));
            }
            cargo_build(
                &env_pairs,
                &[
                    "-p",
                    &p.crate_name,
                    "--no-default-features",
                    "--features",
                    "vst2",
                ],
                dt,
            )?;
            let src = release_lib(&root, &p.dylib_stem());
            let dst = release_lib(&root, &format!("{}_vst2", p.dylib_stem()));
            fs_ctx::copy(&src, &dst)?;
        }
    }

    if lv2 {
        crate::vprintln!("Building LV2...");
        for p in &plugins {
            let mut env_pairs: Vec<(&str, &str)> = Vec::new();
            if let Some(n) = p.lv2_name.as_deref() {
                env_pairs.push(("TRUCE_LV2_NAME_OVERRIDE", n));
            }
            cargo_build(
                &env_pairs,
                &[
                    "-p",
                    &p.crate_name,
                    "--no-default-features",
                    "--features",
                    "lv2",
                ],
                dt,
            )?;
            let src = release_lib(&root, &p.dylib_stem());
            let dst = release_lib(&root, &format!("{}_lv2", p.dylib_stem()));
            fs_ctx::copy(&src, &dst)?;
        }
    }

    if au2 {
        #[cfg(target_os = "macos")]
        {
            crate::vprintln!("Building AU v2...");
            for p in &plugins {
                let mut env_pairs: Vec<(&str, &str)> = vec![
                    ("TRUCE_AU_VERSION", "2"),
                    ("TRUCE_AU_PLUGIN_ID", &p.bundle_id),
                ];
                if let Some(n) = p.au_name.as_deref() {
                    env_pairs.push(("TRUCE_AU_NAME_OVERRIDE", n));
                }
                cargo_build(
                    &env_pairs,
                    &[
                        "-p",
                        &p.crate_name,
                        "--no-default-features",
                        "--features",
                        "au",
                    ],
                    dt,
                )?;
                let src = release_lib(&root, &p.dylib_stem());
                let dst = release_lib(&root, &format!("{}_au", p.dylib_stem()));
                fs_ctx::copy(&src, &dst)?;
            }
        }
        #[cfg(not(target_os = "macos"))]
        crate::log_skip(
            "AU v2: not supported on this platform. Audio Unit is macOS-only.".to_string(),
        );
    }

    if aax {
        #[cfg(any(target_os = "macos", target_os = "windows"))]
        {
            // AAX SDK-not-configured is also a project-wide condition,
            // not per-plugin — emit one skip line and bypass the loop
            // entirely so we don't redundantly cargo-build the `aax`
            // feature only to have `emit_aax_bundle` skip each plugin.
            if crate::resolve_aax_sdk_path(&config).is_none() {
                let hint = if cfg!(target_os = "windows") {
                    "[windows].aax_sdk_path"
                } else {
                    "[macos].aax_sdk_path"
                };
                crate::log_skip(format!(
                    "AAX: SDK not configured. Set {hint} in truce.toml or the AAX_SDK_PATH env var."
                ));
            } else {
                crate::vprintln!("Building AAX...");
                for p in &plugins {
                    let mut env_pairs: Vec<(&str, &str)> = Vec::new();
                    if let Some(n) = p.aax_name.as_deref() {
                        env_pairs.push(("TRUCE_AAX_NAME_OVERRIDE", n));
                    }
                    cargo_build(
                        &env_pairs,
                        &[
                            "-p",
                            &p.crate_name,
                            "--no-default-features",
                            "--features",
                            "aax",
                        ],
                        dt,
                    )?;
                    let src = release_lib(&root, &p.dylib_stem());
                    let dst = release_lib(&root, &format!("{}_aax", p.dylib_stem()));
                    fs_ctx::copy(&src, &dst)?;
                    crate::commands::install::aax::emit_aax_bundle(&root, p, &config, false)?;
                }
            }
        }
        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
        crate::log_skip(
            "AAX: not supported on this platform. Use macOS or Windows to build AAX.".to_string(),
        );
    }

    // Shell mode: also build the debug dylibs (the logic dylib each
    // shell will dlopen at runtime). Scoped per-plugin — `--workspace`
    // would rebuild every example + framework crate.
    if shell_mode {
        for p in &plugins {
            crate::vprintln!(
                "Building {} logic dylib for {}...",
                logic_profile,
                p.crate_name
            );
            let mut cmd = Command::new("cargo");
            cmd.arg("build").arg("-p").arg(&p.crate_name);
            match logic_profile {
                "debug" => {} // cargo default
                "release" => {
                    cmd.arg("--release");
                }
                other => {
                    cmd.arg("--profile").arg(other);
                }
            }
            #[cfg(target_os = "macos")]
            cmd.env("MACOSX_DEPLOYMENT_TARGET", dt);
            let status = cmd.status()?;
            if !status.success() {
                return Err(format!("{logic_profile} build of {} failed", p.crate_name).into());
            }
        }
    }

    // --- Stage each format's bundle into target/bundles/ ---
    let identity = config.macos.application_identity();
    for p in &plugins {
        if clap {
            stage_clap(&root, p, &bundles_dir, identity)?;
            crate::log_output(format!(
                "CLAP: {}",
                bundles_dir.join(format!("{}.clap", p.name)).display()
            ));
        }
        if vst3 {
            stage_vst3(&root, p, &config, &bundles_dir)?;
            crate::log_output(format!(
                "VST3: {}",
                bundles_dir.join(format!("{}.vst3", p.name)).display()
            ));
        }
        if vst2 {
            // macOS produces a `.vst` directory bundle; Linux/Windows
            // get a bare `.so` / `.dll` since neither uses a bundle.
            let staged = stage_vst2(&root, p, &config, &bundles_dir)?;
            crate::log_output(format!("VST2: {}", staged.display()));
        }
        if lv2 {
            stage_lv2(&root, p, &bundles_dir)?;
            let slug = lv2_slug(&p.name);
            crate::log_output(format!(
                "LV2:  {}",
                bundles_dir.join(format!("{slug}.lv2")).display()
            ));
        }
        if au2 {
            #[cfg(target_os = "macos")]
            {
                stage_au2(&root, p, &config, &bundles_dir)?;
                crate::log_output(format!(
                    "AU:   {}",
                    bundles_dir.join(format!("{}.component", p.name)).display()
                ));
            }
            // AU is macOS-only; the build phase already log_skip'd above
            // for non-macOS, so nothing to do here.
        }
    }

    // AU v3 has its own driver that builds Rust-framework + xcodebuild
    // + codesign inside-out and writes directly to target/bundles/.
    // Host arch only; universal builds are reserved for `package`.
    // macOS-only; the function returns a clear error on other platforms.
    if au3 {
        #[cfg(target_os = "macos")]
        {
            use crate::{MacArch, extract_team_id};
            // Same gate as install: ad-hoc / no-team-id makes AU v3
            // unbuildable. The "no team id" case is project-wide
            // (signing identity isn't per-plugin), so emit one skip
            // line and bypass the per-plugin loop.
            let sign_id = config.macos.application_identity();
            if extract_team_id(sign_id).is_empty() {
                crate::log_skip(
                    "AU v3: needs a Developer ID with team ID. \
                     Set [macos.signing].application_identity in truce.toml \
                     (e.g., \"Developer ID Application: Your Name (TEAMID)\"); \
                     ad-hoc signing (\"-\") is not supported for AU v3 appex bundles."
                        .to_string(),
                );
            } else {
                crate::commands::install::au_v3::emit_au_v3_bundle(
                    &root,
                    &config,
                    &plugins,
                    &[MacArch::host()],
                )?;
                for p in &plugins {
                    crate::log_output(format!(
                        "AU3:  {}",
                        bundles_dir
                            .join(format!("{}.app", p.au3_app_name()))
                            .display()
                    ));
                }
            }
        }
        #[cfg(not(target_os = "macos"))]
        crate::log_skip(
            "AU v3: not supported on this platform. Audio Unit is macOS-only.".to_string(),
        );
    }

    let outputs = crate::take_outputs();
    if !outputs.is_empty() {
        eprintln!("\nBuilt:");
        for line in outputs {
            eprintln!("  {line}");
        }
    }
    let skipped = crate::take_skipped();
    if !skipped.is_empty() {
        eprintln!("\nSkipped:");
        for line in skipped {
            eprintln!("  {line}");
        }
    }
    eprintln!("\nBundles in {}", bundles_dir.display());
    Ok(())
}