1
2#![allow(non_camel_case_types)]
6
7use glam::Vec4;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum BuildPlatform {
16 Windows,
17 WindowsArm64,
18 MacOS,
19 MacOSArm64,
20 Linux,
21 LinuxArm64,
22 Android,
23 AndroidX86,
24 iOS,
25 WebGL,
26 WebGPU,
27 XboxSeriesX,
28 XboxOne,
29 PlayStation5,
30 PlayStation4,
31 SwitchNX,
32 TVos,
33 VisionOS,
34 WasmModule,
35 EmbeddedLinux,
36}
37
38impl BuildPlatform {
39 pub fn label(&self) -> &'static str {
40 match self {
41 Self::Windows => "Windows (x64)",
42 Self::WindowsArm64 => "Windows (ARM64)",
43 Self::MacOS => "macOS (x64)",
44 Self::MacOSArm64 => "macOS (Apple Silicon)",
45 Self::Linux => "Linux (x64)",
46 Self::LinuxArm64 => "Linux (ARM64)",
47 Self::Android => "Android (ARM64)",
48 Self::AndroidX86 => "Android (x86_64)",
49 Self::iOS => "iOS",
50 Self::WebGL => "WebGL",
51 Self::WebGPU => "WebGPU",
52 Self::XboxSeriesX => "Xbox Series X/S",
53 Self::XboxOne => "Xbox One",
54 Self::PlayStation5 => "PlayStation 5",
55 Self::PlayStation4 => "PlayStation 4",
56 Self::SwitchNX => "Nintendo Switch",
57 Self::TVos => "tvOS",
58 Self::VisionOS => "visionOS",
59 Self::WasmModule => "WASM Module",
60 Self::EmbeddedLinux => "Embedded Linux",
61 }
62 }
63
64 pub fn is_mobile(&self) -> bool {
65 matches!(self, Self::Android | Self::AndroidX86 | Self::iOS | Self::TVos)
66 }
67 pub fn is_console(&self) -> bool {
68 matches!(self, Self::XboxSeriesX | Self::XboxOne | Self::PlayStation5 | Self::PlayStation4 | Self::SwitchNX)
69 }
70 pub fn is_web(&self) -> bool {
71 matches!(self, Self::WebGL | Self::WebGPU | Self::WasmModule)
72 }
73 pub fn is_desktop(&self) -> bool {
74 matches!(self, Self::Windows | Self::WindowsArm64 | Self::MacOS | Self::MacOSArm64 | Self::Linux | Self::LinuxArm64)
75 }
76 pub fn supports_il2cpp(&self) -> bool {
77 !self.is_web()
78 }
79 pub fn requires_signing(&self) -> bool {
80 matches!(self, Self::iOS | Self::MacOS | Self::MacOSArm64 | Self::TVos | Self::VisionOS | Self::Android)
81 }
82 pub fn file_extension(&self) -> &'static str {
83 match self {
84 Self::Windows | Self::WindowsArm64 => "exe",
85 Self::MacOS | Self::MacOSArm64 => "app",
86 Self::Linux | Self::LinuxArm64 | Self::EmbeddedLinux => "",
87 Self::Android | Self::AndroidX86 => "apk",
88 Self::iOS | Self::TVos | Self::VisionOS => "ipa",
89 Self::WebGL | Self::WebGPU | Self::WasmModule => "html",
90 _ => "pkg",
91 }
92 }
93 pub fn min_sdk_version(&self) -> Option<u32> {
94 match self {
95 Self::Android | Self::AndroidX86 => Some(21),
96 Self::iOS => Some(14),
97 _ => None,
98 }
99 }
100 pub fn icon(&self) -> &'static str {
101 match self {
102 Self::Windows | Self::WindowsArm64 => "🪟",
103 Self::MacOS | Self::MacOSArm64 | Self::TVos | Self::VisionOS => "🍎",
104 Self::Linux | Self::LinuxArm64 => "🐧",
105 Self::Android | Self::AndroidX86 => "🤖",
106 Self::iOS => "📱",
107 Self::WebGL | Self::WebGPU | Self::WasmModule => "🌐",
108 Self::XboxSeriesX | Self::XboxOne => "🎮",
109 Self::PlayStation5 | Self::PlayStation4 => "🎮",
110 Self::SwitchNX => "🎮",
111 _ => "📦",
112 }
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121pub enum BuildConfiguration {
122 Debug,
123 DebugWithOptimizations,
124 Development,
125 Release,
126 ReleaseFinal,
127 Profiling,
128 Shipping,
129 QA,
130}
131
132impl BuildConfiguration {
133 pub fn label(&self) -> &'static str {
134 match self {
135 Self::Debug => "Debug",
136 Self::DebugWithOptimizations => "Debug (Optimized)",
137 Self::Development => "Development",
138 Self::Release => "Release",
139 Self::ReleaseFinal => "Release Final",
140 Self::Profiling => "Profiling",
141 Self::Shipping => "Shipping",
142 Self::QA => "QA",
143 }
144 }
145 pub fn optimization_level(&self) -> u8 {
146 match self {
147 Self::Debug => 0,
148 Self::DebugWithOptimizations => 1,
149 Self::Development => 2,
150 Self::Release | Self::Profiling | Self::QA => 3,
151 Self::ReleaseFinal | Self::Shipping => 3,
152 }
153 }
154 pub fn includes_debug_info(&self) -> bool {
155 matches!(self, Self::Debug | Self::DebugWithOptimizations | Self::Development | Self::Profiling | Self::QA)
156 }
157 pub fn includes_assertions(&self) -> bool {
158 matches!(self, Self::Debug | Self::DebugWithOptimizations | Self::Development | Self::QA)
159 }
160 pub fn strip_logs(&self) -> bool {
161 matches!(self, Self::Shipping | Self::ReleaseFinal)
162 }
163 pub fn enables_cheats(&self) -> bool {
164 matches!(self, Self::Debug | Self::DebugWithOptimizations | Self::Development | Self::QA)
165 }
166 pub fn enable_profiler(&self) -> bool {
167 matches!(self, Self::Profiling | Self::Development | Self::Debug)
168 }
169 pub fn defines(&self) -> Vec<&'static str> {
170 let mut defs = vec![];
171 if self.includes_debug_info() { defs.push("DEBUG"); }
172 if self.includes_assertions() { defs.push("ASSERTIONS_ENABLED"); }
173 if self.strip_logs() { defs.push("STRIP_LOGS"); }
174 if self.enables_cheats() { defs.push("CHEATS_ENABLED"); }
175 if self.enable_profiler() { defs.push("PROFILER_ENABLED"); }
176 match self {
177 Self::Shipping | Self::ReleaseFinal => defs.push("FINAL_BUILD"),
178 Self::QA => defs.push("QA_BUILD"),
179 _ => {}
180 }
181 defs
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq)]
190pub enum ScriptingBackend { Mono, IL2CPP, NativeAOT, WASM }
191
192impl ScriptingBackend {
193 pub fn label(&self) -> &'static str {
194 match self {
195 Self::Mono => "Mono",
196 Self::IL2CPP => "IL2CPP",
197 Self::NativeAOT => "Native AOT",
198 Self::WASM => "WebAssembly",
199 }
200 }
201 pub fn supports_hot_reload(&self) -> bool {
202 matches!(self, Self::Mono)
203 }
204 pub fn compile_time_estimate_min(&self) -> u32 {
205 match self {
206 Self::Mono => 1,
207 Self::IL2CPP => 8,
208 Self::NativeAOT => 6,
209 Self::WASM => 4,
210 }
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
219pub enum AssetProcessorKind {
220 TextureCompressor,
221 AudioTranscoder,
222 MeshOptimizer,
223 ShaderCompiler,
224 FontPacker,
225 VideoEncoder,
226 LocalizationPacker,
227 AnimationCompressor,
228 PhysicsColliderBuilder,
229 NavMeshBaker,
230 LightmapBaker,
231 SdfGenerator,
232 AtlasPacker,
233 PrefabSerializer,
234 SceneSerializer,
235 MaterialBaker,
236 LutBaker,
237 EnvironmentProbeConvolver,
238}
239
240impl AssetProcessorKind {
241 pub fn label(&self) -> &'static str {
242 match self {
243 Self::TextureCompressor => "Texture Compressor",
244 Self::AudioTranscoder => "Audio Transcoder",
245 Self::MeshOptimizer => "Mesh Optimizer",
246 Self::ShaderCompiler => "Shader Compiler",
247 Self::FontPacker => "Font Packer",
248 Self::VideoEncoder => "Video Encoder",
249 Self::LocalizationPacker => "Localization Packer",
250 Self::AnimationCompressor => "Animation Compressor",
251 Self::PhysicsColliderBuilder => "Physics Collider Builder",
252 Self::NavMeshBaker => "NavMesh Baker",
253 Self::LightmapBaker => "Lightmap Baker",
254 Self::SdfGenerator => "SDF Generator",
255 Self::AtlasPacker => "Atlas Packer",
256 Self::PrefabSerializer => "Prefab Serializer",
257 Self::SceneSerializer => "Scene Serializer",
258 Self::MaterialBaker => "Material Baker",
259 Self::LutBaker => "LUT Baker",
260 Self::EnvironmentProbeConvolver => "Env Probe Convolver",
261 }
262 }
263 pub fn is_slow(&self) -> bool {
264 matches!(self, Self::LightmapBaker | Self::VideoEncoder | Self::ShaderCompiler | Self::EnvironmentProbeConvolver)
265 }
266 pub fn can_parallelize(&self) -> bool {
267 !matches!(self, Self::AtlasPacker | Self::NavMeshBaker)
268 }
269 pub fn estimated_time_secs(&self, asset_count: usize) -> f32 {
270 let per_asset = match self {
271 Self::TextureCompressor => 0.5,
272 Self::AudioTranscoder => 0.3,
273 Self::MeshOptimizer => 0.2,
274 Self::ShaderCompiler => 2.0,
275 Self::LightmapBaker => 60.0,
276 Self::VideoEncoder => 30.0,
277 Self::NavMeshBaker => 10.0,
278 _ => 0.1,
279 };
280 per_asset * asset_count as f32
281 }
282}
283
284#[derive(Debug, Clone)]
285pub struct AssetProcessorSettings {
286 pub kind: AssetProcessorKind,
287 pub enabled: bool,
288 pub worker_threads: u32,
289 pub cache_enabled: bool,
290 pub incremental: bool,
291 pub platform_overrides: HashMap<BuildPlatform, HashMap<String, String>>,
292 pub params: HashMap<String, String>,
293}
294
295impl AssetProcessorSettings {
296 pub fn new(kind: AssetProcessorKind) -> Self {
297 Self {
298 kind,
299 enabled: true,
300 worker_threads: 4,
301 cache_enabled: true,
302 incremental: true,
303 platform_overrides: HashMap::new(),
304 params: HashMap::new(),
305 }
306 }
307 pub fn with_param(mut self, key: &str, value: &str) -> Self {
308 self.params.insert(key.to_string(), value.to_string());
309 self
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq)]
318pub enum TextureCompressionFormat {
319 Uncompressed,
320 DXT1,
321 DXT5,
322 BC7,
323 BC6H,
324 ETC2,
325 ASTC4x4,
326 ASTC6x6,
327 ASTC8x8,
328 PVRTC,
329 Crunch,
330 Basis,
331}
332
333impl TextureCompressionFormat {
334 pub fn label(&self) -> &'static str {
335 match self {
336 Self::Uncompressed => "Uncompressed",
337 Self::DXT1 => "DXT1 (BC1)",
338 Self::DXT5 => "DXT5 (BC3)",
339 Self::BC7 => "BC7 (high quality)",
340 Self::BC6H => "BC6H (HDR)",
341 Self::ETC2 => "ETC2",
342 Self::ASTC4x4 => "ASTC 4x4",
343 Self::ASTC6x6 => "ASTC 6x6",
344 Self::ASTC8x8 => "ASTC 8x8",
345 Self::PVRTC => "PVRTC",
346 Self::Crunch => "Crunch",
347 Self::Basis => "Basis Universal",
348 }
349 }
350 pub fn compression_ratio(&self) -> f32 {
351 match self {
352 Self::Uncompressed => 1.0,
353 Self::DXT1 => 6.0,
354 Self::DXT5 => 4.0,
355 Self::BC7 => 4.0,
356 Self::BC6H => 4.0,
357 Self::ETC2 => 4.0,
358 Self::ASTC4x4 => 8.0,
359 Self::ASTC6x6 => 18.0,
360 Self::ASTC8x8 => 32.0,
361 Self::PVRTC => 4.0,
362 Self::Crunch => 20.0,
363 Self::Basis => 25.0,
364 }
365 }
366 pub fn supported_platforms(&self) -> Vec<BuildPlatform> {
367 match self {
368 Self::DXT1 | Self::DXT5 | Self::BC7 | Self::BC6H => vec![BuildPlatform::Windows, BuildPlatform::WindowsArm64, BuildPlatform::Linux, BuildPlatform::MacOS],
369 Self::ETC2 => vec![BuildPlatform::Android, BuildPlatform::AndroidX86],
370 Self::ASTC4x4 | Self::ASTC6x6 | Self::ASTC8x8 => vec![BuildPlatform::iOS, BuildPlatform::Android],
371 Self::PVRTC => vec![BuildPlatform::iOS],
372 Self::Basis | Self::Crunch => vec![BuildPlatform::Windows, BuildPlatform::Linux, BuildPlatform::MacOS, BuildPlatform::Android, BuildPlatform::iOS, BuildPlatform::WebGL],
373 Self::Uncompressed => vec![],
374 }
375 }
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
383pub enum BuildStepKind {
384 CleanOutputDir,
385 CompileScripts,
386 CompileShaders,
387 ProcessAssets,
388 GenerateAssetManifest,
389 LinkBinaries,
390 CopyNativeLibraries,
391 PackageData,
392 SignBinaries,
393 CompressOutput,
394 UploadSymbols,
395 RunTests,
396 GenerateDocumentation,
397 NotifyCISystem,
398 ArchiveBuild,
399 DeployToStore,
400 RunSmokeTest,
401 BumpVersion,
402}
403
404impl BuildStepKind {
405 pub fn label(&self) -> &'static str {
406 match self {
407 Self::CleanOutputDir => "Clean Output Directory",
408 Self::CompileScripts => "Compile Scripts",
409 Self::CompileShaders => "Compile Shaders",
410 Self::ProcessAssets => "Process Assets",
411 Self::GenerateAssetManifest => "Generate Asset Manifest",
412 Self::LinkBinaries => "Link Binaries",
413 Self::CopyNativeLibraries => "Copy Native Libraries",
414 Self::PackageData => "Package Data",
415 Self::SignBinaries => "Sign Binaries",
416 Self::CompressOutput => "Compress Output",
417 Self::UploadSymbols => "Upload Debug Symbols",
418 Self::RunTests => "Run Tests",
419 Self::GenerateDocumentation => "Generate Documentation",
420 Self::NotifyCISystem => "Notify CI System",
421 Self::ArchiveBuild => "Archive Build",
422 Self::DeployToStore => "Deploy to Store",
423 Self::RunSmokeTest => "Run Smoke Test",
424 Self::BumpVersion => "Bump Version",
425 }
426 }
427 pub fn can_skip(&self) -> bool {
428 matches!(self, Self::RunTests | Self::GenerateDocumentation | Self::UploadSymbols | Self::RunSmokeTest)
429 }
430 pub fn requires_internet(&self) -> bool {
431 matches!(self, Self::UploadSymbols | Self::NotifyCISystem | Self::DeployToStore)
432 }
433 pub fn estimated_duration_secs(&self) -> f32 {
434 match self {
435 Self::CleanOutputDir => 2.0,
436 Self::CompileScripts => 30.0,
437 Self::CompileShaders => 120.0,
438 Self::ProcessAssets => 180.0,
439 Self::GenerateAssetManifest => 5.0,
440 Self::LinkBinaries => 15.0,
441 Self::CopyNativeLibraries => 3.0,
442 Self::PackageData => 60.0,
443 Self::SignBinaries => 10.0,
444 Self::CompressOutput => 20.0,
445 Self::UploadSymbols => 30.0,
446 Self::RunTests => 300.0,
447 Self::GenerateDocumentation => 60.0,
448 Self::NotifyCISystem => 2.0,
449 Self::ArchiveBuild => 30.0,
450 Self::DeployToStore => 600.0,
451 Self::RunSmokeTest => 120.0,
452 Self::BumpVersion => 1.0,
453 }
454 }
455}
456
457#[derive(Debug, Clone)]
458pub struct BuildStep {
459 pub kind: BuildStepKind,
460 pub enabled: bool,
461 pub skip_on_error: bool,
462 pub run_on_failure: bool,
463 pub custom_command: Option<String>,
464 pub environment_vars: HashMap<String, String>,
465 pub timeout_secs: Option<u32>,
466}
467
468impl BuildStep {
469 pub fn new(kind: BuildStepKind) -> Self {
470 Self {
471 kind,
472 enabled: true,
473 skip_on_error: false,
474 run_on_failure: false,
475 custom_command: None,
476 environment_vars: HashMap::new(),
477 timeout_secs: None,
478 }
479 }
480 pub fn optional(mut self) -> Self { self.skip_on_error = true; self }
481 pub fn with_timeout(mut self, secs: u32) -> Self { self.timeout_secs = Some(secs); self }
482 pub fn with_command(mut self, cmd: &str) -> Self { self.custom_command = Some(cmd.to_string()); self }
483}
484
485#[derive(Debug, Clone)]
490pub struct BuildTarget {
491 pub name: String,
492 pub platform: BuildPlatform,
493 pub configuration: BuildConfiguration,
494 pub scripting_backend: ScriptingBackend,
495 pub architecture: Architecture,
496 pub output_path: String,
497 pub steps: Vec<BuildStep>,
498 pub asset_processors: Vec<AssetProcessorSettings>,
499 pub texture_compression: TextureCompressionFormat,
500 pub defines: Vec<String>,
501 pub excluded_scenes: Vec<String>,
502 pub included_scenes: Vec<String>,
503 pub compression: PackageCompression,
504 pub split_application_binary: bool,
505 pub development_build: bool,
506 pub connect_profiler: bool,
507 pub allow_debugging: bool,
508 pub il2cpp_optimization: IL2CPPOptimization,
509 pub strip_engine_code: bool,
510 pub strip_unused_code: bool,
511 pub managed_stripping_level: StrippingLevel,
512 pub android_settings: Option<AndroidBuildSettings>,
513 pub ios_settings: Option<IosBuildSettings>,
514 pub signing_settings: Option<SigningSettings>,
515 pub custom_build_script: Option<String>,
516 pub post_build_script: Option<String>,
517 pub enabled: bool,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq)]
521pub enum Architecture { X64, ARM64, X86, Universal, ARMv7 }
522#[derive(Debug, Clone, Copy, PartialEq)]
523pub enum PackageCompression { None, LZ4, LZ4HC, LZMA, Zstd }
524#[derive(Debug, Clone, Copy, PartialEq)]
525pub enum IL2CPPOptimization { None, Size, Speed }
526#[derive(Debug, Clone, Copy, PartialEq)]
527pub enum StrippingLevel { Disabled, Minimal, Medium, High, Experimental }
528
529impl Architecture {
530 pub fn label(&self) -> &'static str {
531 match self {
532 Self::X64 => "x86_64", Self::ARM64 => "ARM64", Self::X86 => "x86",
533 Self::Universal => "Universal", Self::ARMv7 => "ARMv7",
534 }
535 }
536}
537
538impl PackageCompression {
539 pub fn label(&self) -> &'static str {
540 match self {
541 Self::None => "None", Self::LZ4 => "LZ4", Self::LZ4HC => "LZ4HC (Slow)",
542 Self::LZMA => "LZMA", Self::Zstd => "Zstd",
543 }
544 }
545 pub fn ratio_estimate(&self) -> f32 {
546 match self {
547 Self::None => 1.0, Self::LZ4 => 1.4, Self::LZ4HC => 1.6,
548 Self::LZMA => 2.2, Self::Zstd => 2.0,
549 }
550 }
551}
552
553#[derive(Debug, Clone)]
554pub struct AndroidBuildSettings {
555 pub package_name: String,
556 pub version_code: u32,
557 pub version_name: String,
558 pub min_sdk: u32,
559 pub target_sdk: u32,
560 pub keystore_path: String,
561 pub keystore_alias: String,
562 pub build_apk: bool,
563 pub build_aab: bool,
564 pub split_apks: bool,
565 pub target_architectures: Vec<Architecture>,
566 pub install_location: AndroidInstallLocation,
567 pub internet_access: bool,
568 pub vibration: bool,
569 pub accelerometer: bool,
570 pub enable_arm_neon: bool,
571 pub il2cpp_target_api_level: u32,
572}
573
574#[derive(Debug, Clone, Copy, PartialEq)]
575pub enum AndroidInstallLocation { Auto, InternalOnly, PreferExternal }
576
577impl Default for AndroidBuildSettings {
578 fn default() -> Self {
579 Self {
580 package_name: "com.example.game".to_string(),
581 version_code: 1,
582 version_name: "1.0.0".to_string(),
583 min_sdk: 21,
584 target_sdk: 33,
585 keystore_path: String::new(),
586 keystore_alias: String::new(),
587 build_apk: true,
588 build_aab: false,
589 split_apks: false,
590 target_architectures: vec![Architecture::ARM64],
591 install_location: AndroidInstallLocation::Auto,
592 internet_access: true,
593 vibration: true,
594 accelerometer: false,
595 enable_arm_neon: true,
596 il2cpp_target_api_level: 29,
597 }
598 }
599}
600
601#[derive(Debug, Clone)]
602pub struct IosBuildSettings {
603 pub bundle_id: String,
604 pub short_version: String,
605 pub bundle_version: String,
606 pub target_device: IosTargetDevice,
607 pub target_ios_version: String,
608 pub uses_location: bool,
609 pub uses_camera: bool,
610 pub uses_microphone: bool,
611 pub uses_motion_sensors: bool,
612 pub hdr_display_mode: bool,
613 pub requires_full_screen: bool,
614 pub application_category: IosCategory,
615 pub bitcode_enabled: bool,
616 pub apple_developer_team: String,
617 pub provisioning_profile: String,
618 pub metal_enabled: bool,
619}
620
621#[derive(Debug, Clone, Copy, PartialEq)]
622pub enum IosTargetDevice { iPhone, iPad, iPhoneAndiPad }
623#[derive(Debug, Clone, Copy, PartialEq)]
624pub enum IosCategory { Games, Entertainment, Utilities, Education, Other }
625
626impl Default for IosBuildSettings {
627 fn default() -> Self {
628 Self {
629 bundle_id: "com.example.game".to_string(),
630 short_version: "1.0".to_string(),
631 bundle_version: "1".to_string(),
632 target_device: IosTargetDevice::iPhoneAndiPad,
633 target_ios_version: "14.0".to_string(),
634 uses_location: false,
635 uses_camera: false,
636 uses_microphone: false,
637 uses_motion_sensors: false,
638 hdr_display_mode: false,
639 requires_full_screen: true,
640 application_category: IosCategory::Games,
641 bitcode_enabled: false,
642 apple_developer_team: String::new(),
643 provisioning_profile: String::new(),
644 metal_enabled: true,
645 }
646 }
647}
648
649#[derive(Debug, Clone)]
650pub struct SigningSettings {
651 pub certificate_path: String,
652 pub certificate_password: String,
653 pub notarize: bool,
654 pub staple: bool,
655 pub entitlements_path: String,
656 pub codesign_identity: String,
657 pub timestamp_server: String,
658}
659
660impl BuildTarget {
661 pub fn windows_release() -> Self {
662 Self {
663 name: "Windows Release".to_string(),
664 platform: BuildPlatform::Windows,
665 configuration: BuildConfiguration::Release,
666 scripting_backend: ScriptingBackend::NativeAOT,
667 architecture: Architecture::X64,
668 output_path: "build/windows".to_string(),
669 steps: vec![
670 BuildStep::new(BuildStepKind::CleanOutputDir),
671 BuildStep::new(BuildStepKind::CompileShaders),
672 BuildStep::new(BuildStepKind::CompileScripts),
673 BuildStep::new(BuildStepKind::ProcessAssets),
674 BuildStep::new(BuildStepKind::GenerateAssetManifest),
675 BuildStep::new(BuildStepKind::LinkBinaries),
676 BuildStep::new(BuildStepKind::PackageData),
677 BuildStep::new(BuildStepKind::CompressOutput),
678 BuildStep::new(BuildStepKind::RunTests).optional(),
679 BuildStep::new(BuildStepKind::ArchiveBuild),
680 ],
681 asset_processors: vec![
682 AssetProcessorSettings::new(AssetProcessorKind::TextureCompressor).with_param("format", "BC7"),
683 AssetProcessorSettings::new(AssetProcessorKind::ShaderCompiler).with_param("api", "D3D12"),
684 AssetProcessorSettings::new(AssetProcessorKind::AudioTranscoder).with_param("format", "OGG"),
685 AssetProcessorSettings::new(AssetProcessorKind::MeshOptimizer),
686 ],
687 texture_compression: TextureCompressionFormat::BC7,
688 defines: vec!["PLATFORM_WINDOWS".to_string()],
689 excluded_scenes: Vec::new(),
690 included_scenes: Vec::new(),
691 compression: PackageCompression::LZ4HC,
692 split_application_binary: false,
693 development_build: false,
694 connect_profiler: false,
695 allow_debugging: false,
696 il2cpp_optimization: IL2CPPOptimization::Speed,
697 strip_engine_code: true,
698 strip_unused_code: true,
699 managed_stripping_level: StrippingLevel::Medium,
700 android_settings: None,
701 ios_settings: None,
702 signing_settings: None,
703 custom_build_script: None,
704 post_build_script: None,
705 enabled: true,
706 }
707 }
708
709 pub fn android_release() -> Self {
710 let mut t = Self::windows_release();
711 t.name = "Android Release".to_string();
712 t.platform = BuildPlatform::Android;
713 t.output_path = "build/android".to_string();
714 t.texture_compression = TextureCompressionFormat::ASTC4x4;
715 t.scripting_backend = ScriptingBackend::IL2CPP;
716 t.android_settings = Some(AndroidBuildSettings::default());
717 t.defines = vec!["PLATFORM_ANDROID".to_string()];
718 t.asset_processors = vec![
719 AssetProcessorSettings::new(AssetProcessorKind::TextureCompressor).with_param("format", "ASTC"),
720 AssetProcessorSettings::new(AssetProcessorKind::ShaderCompiler).with_param("api", "Vulkan"),
721 AssetProcessorSettings::new(AssetProcessorKind::AudioTranscoder).with_param("format", "OGG"),
722 ];
723 t
724 }
725
726 pub fn web_gl() -> Self {
727 let mut t = Self::windows_release();
728 t.name = "WebGL".to_string();
729 t.platform = BuildPlatform::WebGL;
730 t.output_path = "build/webgl".to_string();
731 t.scripting_backend = ScriptingBackend::WASM;
732 t.texture_compression = TextureCompressionFormat::Basis;
733 t.compression = PackageCompression::Zstd;
734 t.defines = vec!["PLATFORM_WEBGL".to_string()];
735 t.asset_processors = vec![
736 AssetProcessorSettings::new(AssetProcessorKind::TextureCompressor).with_param("format", "Basis"),
737 AssetProcessorSettings::new(AssetProcessorKind::ShaderCompiler).with_param("api", "WebGL2"),
738 ];
739 t
740 }
741
742 pub fn total_estimated_build_time_secs(&self) -> f32 {
743 self.steps.iter()
744 .filter(|s| s.enabled)
745 .map(|s| s.kind.estimated_duration_secs())
746 .sum()
747 }
748
749 pub fn step_count(&self) -> usize {
750 self.steps.iter().filter(|s| s.enabled).count()
751 }
752}
753
754#[derive(Debug, Clone, Copy, PartialEq)]
759pub enum LogLevel { Trace, Debug, Info, Warning, Error, Fatal }
760
761impl LogLevel {
762 pub fn label(&self) -> &'static str {
763 match self {
764 Self::Trace => "TRACE", Self::Debug => "DEBUG", Self::Info => "INFO",
765 Self::Warning => "WARN", Self::Error => "ERROR", Self::Fatal => "FATAL",
766 }
767 }
768 pub fn color(&self) -> Vec4 {
769 match self {
770 Self::Trace => Vec4::new(0.5, 0.5, 0.5, 1.0),
771 Self::Debug => Vec4::new(0.6, 0.8, 1.0, 1.0),
772 Self::Info => Vec4::new(0.9, 0.9, 0.9, 1.0),
773 Self::Warning => Vec4::new(1.0, 0.8, 0.0, 1.0),
774 Self::Error => Vec4::new(1.0, 0.3, 0.3, 1.0),
775 Self::Fatal => Vec4::new(1.0, 0.0, 0.5, 1.0),
776 }
777 }
778 pub fn is_error(&self) -> bool { matches!(self, Self::Error | Self::Fatal) }
779}
780
781#[derive(Debug, Clone)]
782pub struct BuildLogEntry {
783 pub timestamp_ms: f64,
784 pub level: LogLevel,
785 pub step: Option<BuildStepKind>,
786 pub message: String,
787 pub file: Option<String>,
788 pub line: Option<u32>,
789 pub asset_path: Option<String>,
790 pub duration_ms: Option<f32>,
791}
792
793impl BuildLogEntry {
794 pub fn info(msg: &str) -> Self {
795 Self { timestamp_ms: 0.0, level: LogLevel::Info, step: None, message: msg.to_string(), file: None, line: None, asset_path: None, duration_ms: None }
796 }
797 pub fn warning(msg: &str) -> Self {
798 Self { level: LogLevel::Warning, ..Self::info(msg) }
799 }
800 pub fn error(msg: &str) -> Self {
801 Self { level: LogLevel::Error, ..Self::info(msg) }
802 }
803}
804
805#[derive(Debug, Clone)]
806pub struct BuildLog {
807 pub entries: Vec<BuildLogEntry>,
808 pub warnings: u32,
809 pub errors: u32,
810 pub start_time_ms: f64,
811 pub end_time_ms: Option<f64>,
812 pub success: Option<bool>,
813}
814
815impl BuildLog {
816 pub fn new() -> Self {
817 Self { entries: Vec::new(), warnings: 0, errors: 0, start_time_ms: 0.0, end_time_ms: None, success: None }
818 }
819 pub fn push(&mut self, entry: BuildLogEntry) {
820 if entry.level == LogLevel::Warning { self.warnings += 1; }
821 if entry.level.is_error() { self.errors += 1; }
822 self.entries.push(entry);
823 }
824 pub fn duration_ms(&self) -> Option<f64> {
825 self.end_time_ms.map(|e| e - self.start_time_ms)
826 }
827 pub fn duration_secs(&self) -> Option<f32> {
828 self.duration_ms().map(|d| d as f32 / 1000.0)
829 }
830 pub fn filter_by_level(&self, level: LogLevel) -> Vec<&BuildLogEntry> {
831 self.entries.iter().filter(|e| e.level == level).collect()
832 }
833 pub fn errors_and_warnings(&self) -> Vec<&BuildLogEntry> {
834 self.entries.iter().filter(|e| e.level.is_error() || e.level == LogLevel::Warning).collect()
835 }
836 pub fn generate_summary(&self) -> String {
837 let dur = self.duration_secs().map(|d| format!("{:.1}s", d)).unwrap_or_else(|| "N/A".to_string());
838 let status = match self.success {
839 Some(true) => "SUCCESS",
840 Some(false) => "FAILED",
841 None => "IN PROGRESS",
842 };
843 format!("Build {}: {} warnings, {} errors, duration: {}", status, self.warnings, self.errors, dur)
844 }
845}
846
847impl Default for BuildLog {
848 fn default() -> Self { Self::new() }
849}
850
851#[derive(Debug, Clone, Copy, PartialEq)]
856pub enum BuildState {
857 Idle,
858 Preparing,
859 Running,
860 Paused,
861 Completed,
862 Failed,
863 Cancelled,
864}
865
866impl BuildState {
867 pub fn label(&self) -> &'static str {
868 match self {
869 Self::Idle => "Idle",
870 Self::Preparing => "Preparing",
871 Self::Running => "Running",
872 Self::Paused => "Paused",
873 Self::Completed => "Completed",
874 Self::Failed => "Failed",
875 Self::Cancelled => "Cancelled",
876 }
877 }
878 pub fn is_active(&self) -> bool { matches!(self, Self::Running | Self::Preparing) }
879 pub fn color(&self) -> Vec4 {
880 match self {
881 Self::Idle => Vec4::new(0.6, 0.6, 0.6, 1.0),
882 Self::Preparing | Self::Running => Vec4::new(0.4, 0.8, 1.0, 1.0),
883 Self::Paused => Vec4::new(1.0, 0.8, 0.2, 1.0),
884 Self::Completed => Vec4::new(0.3, 0.9, 0.3, 1.0),
885 Self::Failed => Vec4::new(1.0, 0.3, 0.3, 1.0),
886 Self::Cancelled => Vec4::new(0.8, 0.5, 0.2, 1.0),
887 }
888 }
889}
890
891#[derive(Debug, Clone)]
896pub struct VersionInfo {
897 pub major: u32,
898 pub minor: u32,
899 pub patch: u32,
900 pub build_number: u32,
901 pub pre_release: String,
902 pub git_commit: String,
903 pub git_branch: String,
904 pub build_date: String,
905 pub channel: ReleaseChannel,
906}
907
908#[derive(Debug, Clone, Copy, PartialEq)]
909pub enum ReleaseChannel { Dev, Alpha, Beta, ReleaseCandidate, Stable, Hotfix }
910
911impl ReleaseChannel {
912 pub fn label(&self) -> &'static str {
913 match self {
914 Self::Dev => "dev", Self::Alpha => "alpha", Self::Beta => "beta",
915 Self::ReleaseCandidate => "rc", Self::Stable => "stable", Self::Hotfix => "hotfix",
916 }
917 }
918}
919
920impl VersionInfo {
921 pub fn format_full(&self) -> String {
922 let pre = if self.pre_release.is_empty() { String::new() } else { format!("-{}", self.pre_release) };
923 format!("{}.{}.{}{} (build {})", self.major, self.minor, self.patch, pre, self.build_number)
924 }
925 pub fn format_short(&self) -> String {
926 format!("{}.{}.{}", self.major, self.minor, self.patch)
927 }
928 pub fn bump_patch(&mut self) { self.patch += 1; self.build_number += 1; }
929 pub fn bump_minor(&mut self) { self.minor += 1; self.patch = 0; self.build_number += 1; }
930 pub fn bump_major(&mut self) { self.major += 1; self.minor = 0; self.patch = 0; self.build_number += 1; }
931}
932
933impl Default for VersionInfo {
934 fn default() -> Self {
935 Self {
936 major: 0, minor: 1, patch: 0, build_number: 1,
937 pre_release: "alpha".to_string(),
938 git_commit: "abcdef1".to_string(),
939 git_branch: "main".to_string(),
940 build_date: "2026-03-29".to_string(),
941 channel: ReleaseChannel::Alpha,
942 }
943 }
944}
945
946#[derive(Debug, Clone, Copy, PartialEq)]
951pub enum CIProvider { GitHub, GitLab, Jenkins, CircleCI, TeamCity, AzureDevOps, Buildkite, Bamboo, Custom }
952
953impl CIProvider {
954 pub fn label(&self) -> &'static str {
955 match self {
956 Self::GitHub => "GitHub Actions",
957 Self::GitLab => "GitLab CI",
958 Self::Jenkins => "Jenkins",
959 Self::CircleCI => "CircleCI",
960 Self::TeamCity => "TeamCity",
961 Self::AzureDevOps => "Azure DevOps",
962 Self::Buildkite => "Buildkite",
963 Self::Bamboo => "Bamboo",
964 Self::Custom => "Custom",
965 }
966 }
967 pub fn webhook_format(&self) -> &'static str {
968 match self {
969 Self::GitHub => "json",
970 Self::Jenkins => "text/plain",
971 _ => "json",
972 }
973 }
974}
975
976#[derive(Debug, Clone)]
977pub struct CISettings {
978 pub provider: CIProvider,
979 pub webhook_url: String,
980 pub api_token: String,
981 pub trigger_on_push: bool,
982 pub trigger_on_tag: bool,
983 pub trigger_branches: Vec<String>,
984 pub notify_on_success: bool,
985 pub notify_on_failure: bool,
986 pub notify_emails: Vec<String>,
987 pub artifact_retention_days: u32,
988 pub parallel_jobs: u32,
989}
990
991impl Default for CISettings {
992 fn default() -> Self {
993 Self {
994 provider: CIProvider::GitHub,
995 webhook_url: String::new(),
996 api_token: String::new(),
997 trigger_on_push: true,
998 trigger_on_tag: true,
999 trigger_branches: vec!["main".to_string(), "release/*".to_string()],
1000 notify_on_success: false,
1001 notify_on_failure: true,
1002 notify_emails: Vec::new(),
1003 artifact_retention_days: 30,
1004 parallel_jobs: 4,
1005 }
1006 }
1007}
1008
1009#[derive(Debug, Clone, Copy, PartialEq)]
1014pub enum BuildEditorPanel {
1015 Targets,
1016 AssetPipeline,
1017 Versioning,
1018 CIIntegration,
1019 Log,
1020 Artifacts,
1021 Settings,
1022}
1023
1024impl BuildEditorPanel {
1025 pub fn label(&self) -> &'static str {
1026 match self {
1027 Self::Targets => "Build Targets",
1028 Self::AssetPipeline => "Asset Pipeline",
1029 Self::Versioning => "Versioning",
1030 Self::CIIntegration => "CI/CD",
1031 Self::Log => "Build Log",
1032 Self::Artifacts => "Artifacts",
1033 Self::Settings => "Settings",
1034 }
1035 }
1036}
1037
1038#[derive(Debug, Clone)]
1039pub struct BuildArtifact {
1040 pub name: String,
1041 pub platform: BuildPlatform,
1042 pub configuration: BuildConfiguration,
1043 pub version: String,
1044 pub file_size_bytes: u64,
1045 pub compressed_size_bytes: u64,
1046 pub path: String,
1047 pub created_at: String,
1048 pub sha256: String,
1049 pub upload_url: Option<String>,
1050 pub build_log_path: String,
1051}
1052
1053impl BuildArtifact {
1054 pub fn size_mb(&self) -> f32 { self.file_size_bytes as f32 / (1024.0 * 1024.0) }
1055 pub fn compressed_mb(&self) -> f32 { self.compressed_size_bytes as f32 / (1024.0 * 1024.0) }
1056 pub fn compression_ratio(&self) -> f32 {
1057 if self.file_size_bytes == 0 { return 1.0; }
1058 self.file_size_bytes as f32 / self.compressed_size_bytes.max(1) as f32
1059 }
1060}
1061
1062#[derive(Debug)]
1063pub struct BuildSystemEditor {
1064 pub targets: Vec<BuildTarget>,
1065 pub selected_target: Option<usize>,
1066 pub active_panel: BuildEditorPanel,
1067 pub build_state: BuildState,
1068 pub current_step_index: usize,
1069 pub step_progress: f32,
1070 pub overall_progress: f32,
1071 pub current_log: BuildLog,
1072 pub historical_logs: Vec<BuildLog>,
1073 pub artifacts: Vec<BuildArtifact>,
1074 pub version: VersionInfo,
1075 pub ci_settings: CISettings,
1076 pub asset_processors: Vec<AssetProcessorSettings>,
1077 pub build_queue: Vec<usize>,
1078 pub last_build_duration_secs: f32,
1079 pub search_filter: String,
1080 pub log_level_filter: Option<LogLevel>,
1081 pub show_only_errors: bool,
1082 pub auto_run_tests: bool,
1083 pub parallel_asset_processing: bool,
1084 pub asset_cache_path: String,
1085 pub output_base_path: String,
1086}
1087
1088impl Default for BuildSystemEditor {
1089 fn default() -> Self {
1090 let targets = vec![
1091 BuildTarget::windows_release(),
1092 BuildTarget::android_release(),
1093 BuildTarget::web_gl(),
1094 ];
1095
1096 let mut log = BuildLog::new();
1097 log.push(BuildLogEntry::info("Build system initialized"));
1098 log.push(BuildLogEntry::info("Loaded 3 build targets"));
1099 log.push(BuildLogEntry::warning("Shader cache is stale, recommend recompiling"));
1100
1101 let asset_processors = vec![
1102 AssetProcessorSettings::new(AssetProcessorKind::TextureCompressor).with_param("quality", "high"),
1103 AssetProcessorSettings::new(AssetProcessorKind::AudioTranscoder).with_param("bitrate", "192"),
1104 AssetProcessorSettings::new(AssetProcessorKind::MeshOptimizer).with_param("lod_levels", "3"),
1105 AssetProcessorSettings::new(AssetProcessorKind::ShaderCompiler).with_param("cache", "true"),
1106 AssetProcessorSettings::new(AssetProcessorKind::FontPacker),
1107 AssetProcessorSettings::new(AssetProcessorKind::AnimationCompressor),
1108 AssetProcessorSettings::new(AssetProcessorKind::NavMeshBaker),
1109 AssetProcessorSettings::new(AssetProcessorKind::LutBaker),
1110 ];
1111
1112 Self {
1113 targets,
1114 selected_target: Some(0),
1115 active_panel: BuildEditorPanel::Targets,
1116 build_state: BuildState::Idle,
1117 current_step_index: 0,
1118 step_progress: 0.0,
1119 overall_progress: 0.0,
1120 current_log: log,
1121 historical_logs: Vec::new(),
1122 artifacts: Vec::new(),
1123 version: VersionInfo::default(),
1124 ci_settings: CISettings::default(),
1125 asset_processors,
1126 build_queue: Vec::new(),
1127 last_build_duration_secs: 0.0,
1128 search_filter: String::new(),
1129 log_level_filter: None,
1130 show_only_errors: false,
1131 auto_run_tests: false,
1132 parallel_asset_processing: true,
1133 asset_cache_path: ".build_cache".to_string(),
1134 output_base_path: "build".to_string(),
1135 }
1136 }
1137}
1138
1139impl BuildSystemEditor {
1140 pub fn selected_target(&self) -> Option<&BuildTarget> {
1141 self.selected_target.and_then(|i| self.targets.get(i))
1142 }
1143
1144 pub fn start_build(&mut self, target_index: usize) {
1145 self.build_state = BuildState::Running;
1146 self.current_step_index = 0;
1147 self.step_progress = 0.0;
1148 self.overall_progress = 0.0;
1149 self.current_log = BuildLog::new();
1150 self.current_log.push(BuildLogEntry::info(&format!("Starting build: {}", self.targets[target_index].name)));
1151 }
1152
1153 pub fn simulate_build_tick(&mut self, dt: f32) {
1154 if self.build_state != BuildState::Running { return; }
1155 let target = match self.selected_target.and_then(|i| self.targets.get(i)) {
1156 Some(t) => t,
1157 None => return,
1158 };
1159 let total_steps = target.step_count();
1160 if total_steps == 0 { self.build_state = BuildState::Completed; return; }
1161
1162 self.step_progress += dt * 0.5;
1163 if self.step_progress >= 1.0 {
1164 self.step_progress = 0.0;
1165 self.current_step_index += 1;
1166 if self.current_step_index >= total_steps {
1167 self.build_state = BuildState::Completed;
1168 self.current_log.success = Some(true);
1169 self.current_log.push(BuildLogEntry::info("Build completed successfully"));
1170 return;
1171 }
1172 let enabled_steps: Vec<&BuildStep> = target.steps.iter().filter(|s| s.enabled).collect();
1173 if let Some(step) = enabled_steps.get(self.current_step_index) {
1174 self.current_log.push(BuildLogEntry::info(&format!("Step: {}", step.kind.label())));
1175 }
1176 }
1177 self.overall_progress = (self.current_step_index as f32 + self.step_progress) / total_steps as f32;
1178 }
1179
1180 pub fn cancel_build(&mut self) {
1181 if self.build_state.is_active() {
1182 self.build_state = BuildState::Cancelled;
1183 self.current_log.success = Some(false);
1184 self.current_log.push(BuildLogEntry::warning("Build cancelled by user"));
1185 }
1186 }
1187
1188 pub fn filtered_log_entries(&self) -> Vec<&BuildLogEntry> {
1189 let q = self.search_filter.to_lowercase();
1190 self.current_log.entries.iter().filter(|e| {
1191 let level_ok = if self.show_only_errors { e.level.is_error() }
1192 else if let Some(lvl) = self.log_level_filter { e.level == lvl } else { true };
1193 let text_ok = q.is_empty() || e.message.to_lowercase().contains(&q);
1194 level_ok && text_ok
1195 }).collect()
1196 }
1197
1198 pub fn add_to_queue(&mut self, target_index: usize) {
1199 if !self.build_queue.contains(&target_index) {
1200 self.build_queue.push(target_index);
1201 }
1202 }
1203
1204 pub fn remove_from_queue(&mut self, target_index: usize) {
1205 self.build_queue.retain(|&i| i != target_index);
1206 }
1207
1208 pub fn total_estimated_build_time(&self) -> f32 {
1209 self.targets.iter().filter(|t| t.enabled).map(|t| t.total_estimated_build_time_secs()).sum()
1210 }
1211
1212 pub fn platforms_summary(&self) -> Vec<(BuildPlatform, usize)> {
1213 let mut counts: HashMap<BuildPlatform, usize> = HashMap::new();
1214 for t in &self.targets {
1215 *counts.entry(t.platform).or_insert(0) += 1;
1216 }
1217 counts.into_iter().collect()
1218 }
1219
1220 pub fn generate_ci_yaml(&self) -> String {
1221 match self.ci_settings.provider {
1222 CIProvider::GitHub => {
1223 let mut lines = vec![
1224 "name: Build".to_string(),
1225 "on:".to_string(),
1226 " push:".to_string(),
1227 " branches: [main, 'release/*']".to_string(),
1228 " tags: ['v*']".to_string(),
1229 "jobs:".to_string(),
1230 " build:".to_string(),
1231 " runs-on: ubuntu-latest".to_string(),
1232 " steps:".to_string(),
1233 " - uses: actions/checkout@v4".to_string(),
1234 " - name: Build All Targets".to_string(),
1235 " run: cargo build --release".to_string(),
1236 ];
1237 for target in &self.targets {
1238 if target.enabled {
1239 lines.push(format!(" # Target: {} ({})", target.name, target.platform.label()));
1240 }
1241 }
1242 lines.join("\n")
1243 }
1244 _ => "# CI configuration not yet generated for this provider".to_string(),
1245 }
1246 }
1247}