1use std::collections::{HashMap, HashSet};
5
6use rayon::prelude::*;
7
8use crate::error::Result;
9use crate::io::{create_dir_all, write_file_if_changed};
10use crate::lockfile::{DllRef, Lockfile, RefCategory};
11use crate::paths::{DEFAULT_GENERATOR_ROOT, join_path, resolve_real_path};
12use crate::project_scanner::{AsmDefRecord, ProjectCategory, ProjectScanner, ScanResult};
13use crate::xml::{deterministic_guid, xml_escape};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum BuildPlatform {
17 Ios,
18 Android,
19 Osx,
20}
21
22impl BuildPlatform {
23 pub fn parse(s: &str) -> Option<Self> {
24 Some(match s {
25 "ios" => BuildPlatform::Ios,
26 "android" => BuildPlatform::Android,
27 "osx" => BuildPlatform::Osx,
28 _ => return None,
29 })
30 }
31
32 pub fn raw(self) -> &'static str {
33 match self {
34 BuildPlatform::Ios => "ios",
35 BuildPlatform::Android => "android",
36 BuildPlatform::Osx => "osx",
37 }
38 }
39
40 pub fn unity_platform_name(self) -> &'static str {
42 match self {
43 BuildPlatform::Ios => "iOS",
44 BuildPlatform::Android => "Android",
45 BuildPlatform::Osx => "macOSStandalone",
46 }
47 }
48
49 pub fn platform_defines(self) -> &'static [&'static str] {
50 match self {
51 BuildPlatform::Ios => &["UNITY_IOS", "UNITY_IPHONE"],
52 BuildPlatform::Android => &["UNITY_ANDROID"],
53 BuildPlatform::Osx => &["UNITY_STANDALONE", "UNITY_STANDALONE_OSX"],
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum BuildConfig {
60 Editor,
61 Dev,
62 Prod,
63}
64
65impl BuildConfig {
66 pub fn parse(s: &str) -> Option<Self> {
67 Some(match s {
68 "editor" => BuildConfig::Editor,
69 "dev" => BuildConfig::Dev,
70 "prod" => BuildConfig::Prod,
71 _ => return None,
72 })
73 }
74
75 pub fn raw(self) -> &'static str {
76 match self {
77 BuildConfig::Editor => "editor",
78 BuildConfig::Dev => "dev",
79 BuildConfig::Prod => "prod",
80 }
81 }
82}
83
84const EDITOR_DEFINES: &[&str] = &["UNITY_EDITOR", "UNITY_EDITOR_64", "UNITY_EDITOR_OSX"];
85const DEBUG_DEFINES: &[&str] = &["DEBUG", "TRACE", "UNITY_ASSERTIONS"];
86
87#[derive(Debug, Clone)]
88pub struct GenerateOptions {
89 pub project_root: String,
90 pub generator_root: String,
91 pub verbose: bool,
92 pub output_dir: Option<String>,
94 pub extra_refs: Vec<DllRef>,
95 pub platform: BuildPlatform,
96 pub build_config: BuildConfig,
97}
98
99impl GenerateOptions {
100 pub fn new(project_root: impl Into<String>, platform: BuildPlatform) -> Self {
101 Self {
102 project_root: project_root.into(),
103 generator_root: DEFAULT_GENERATOR_ROOT.to_string(),
104 verbose: false,
105 output_dir: None,
106 extra_refs: Vec::new(),
107 platform,
108 build_config: BuildConfig::Prod,
109 }
110 }
111
112 pub fn with_generator_root(mut self, generator_root: impl Into<String>) -> Self {
113 self.generator_root = generator_root.into();
114 self
115 }
116
117 pub fn with_build_config(mut self, build_config: BuildConfig) -> Self {
118 self.build_config = build_config;
119 self
120 }
121
122 pub fn with_output_dir(mut self, output_dir: Option<&str>) -> Self {
123 self.output_dir = output_dir.map(|d| {
124 let trimmed = d.trim_end_matches('/');
125 if trimmed.is_empty() {
126 ".".to_string()
127 } else {
128 trimmed.to_string()
129 }
130 });
131 self
132 }
133
134 pub fn with_extra_refs(mut self, extra_refs: Vec<DllRef>) -> Self {
135 self.extra_refs = extra_refs;
136 self
137 }
138
139 pub fn with_verbose(mut self, verbose: bool) -> Self {
140 self.verbose = verbose;
141 self
142 }
143}
144
145#[derive(Debug, Clone)]
146pub struct GenerateResult {
147 pub warnings: Vec<String>,
148 pub variant_csprojs: Vec<String>,
149 pub variant_sln_path: String,
150}
151
152#[derive(Debug, Clone)]
153struct ProjectInfo {
154 name: String,
155 guid: String,
156}
157
158impl ProjectInfo {
159 fn csproj_path(&self) -> String {
160 format!("{}.csproj", self.name)
161 }
162}
163
164struct GenerationContext {
165 project_root: String,
166 scan: ScanResult,
167 project_by_name: HashMap<String, ProjectInfo>,
168 patterns_by_project: HashMap<String, Vec<String>>,
169 included_projects: Vec<ProjectInfo>,
170 non_runtime_names: HashSet<String>,
171 variant_dir: String,
172 result_prefix: String,
173 warnings: Vec<String>,
174}
175
176fn build_context(
177 options: &GenerateOptions,
178 project_root: String,
179 projects: Vec<ProjectInfo>,
180 scan: ScanResult,
181) -> GenerationContext {
182 let project_by_name: HashMap<String, ProjectInfo> = projects
183 .iter()
184 .map(|p| (p.name.clone(), p.clone()))
185 .collect();
186
187 let mut warnings: Vec<String> = Vec::new();
188 if !scan.unresolved_dirs.is_empty() {
189 warnings.push(format!(
190 "Unresolved source directories: {}",
191 scan.unresolved_dirs.len()
192 ));
193 }
194 if options.verbose {
195 for d in scan.unresolved_dirs.iter().take(20) {
196 warnings.push(format!("Unresolved: {}/", d));
197 }
198 }
199
200 let depth: usize = if let Some(out) = options.output_dir.as_deref() {
201 if out == "." {
202 0
203 } else {
204 out.split('/').filter(|s| !s.is_empty()).count()
205 }
206 } else {
207 options
209 .generator_root
210 .split('/')
211 .filter(|s| !s.is_empty())
212 .count()
213 + 1
214 };
215 let variant_prefix: String = "../".repeat(depth);
216
217 let mut patterns_by_project: HashMap<String, Vec<String>> = HashMap::new();
218 for project in &projects {
219 let mut dirs: Vec<String> = scan
220 .dirs_by_project
221 .get(&project.name)
222 .cloned()
223 .unwrap_or_default();
224 dirs.sort();
225 let pats: Vec<String> = dirs
226 .iter()
227 .map(|d| {
228 if d.is_empty() {
229 format!("{}*.cs", variant_prefix)
230 } else {
231 format!("{}{}/*.cs", variant_prefix, d)
232 }
233 })
234 .collect();
235 patterns_by_project.insert(project.name.clone(), pats);
236 }
237
238 let is_editor = options.build_config == BuildConfig::Editor;
239 let mut included_projects: Vec<ProjectInfo> = Vec::new();
240 let mut non_runtime_names: HashSet<String> = HashSet::new();
241 if is_editor {
242 included_projects = projects.clone();
243 } else {
244 for project in &projects {
245 let (category, matches_platform) = match scan.asm_def_by_name.get(&project.name) {
246 Some(asm) => {
247 let platforms: Vec<&str> = asm
248 .include_platforms
249 .iter()
250 .filter(|p| p.as_str() != "Editor")
251 .map(String::as_str)
252 .collect();
253 let matches =
254 platforms.is_empty() || platforms.contains(&options.platform.unity_platform_name());
255 (asm.category, matches)
256 }
257 None => (ProjectCategory::Runtime, true),
258 };
259 if category == ProjectCategory::Runtime && matches_platform {
260 included_projects.push(project.clone());
261 } else {
262 non_runtime_names.insert(project.name.clone());
263 }
264 }
265 }
266
267 let config_str = format!(
268 "{}-{}",
269 options.platform.raw(),
270 options.build_config.raw()
271 );
272 let (variant_dir, result_prefix) = if let Some(out) = options.output_dir.as_deref() {
273 if out == "." {
274 (project_root.clone(), String::new())
275 } else {
276 let dir = join_path(&project_root, out);
277 create_dir_all(&dir);
278 (dir, format!("{}/", out))
279 }
280 } else {
281 let generator_dir = join_path(&project_root, &options.generator_root);
282 let dir = join_path(&generator_dir, &config_str);
283 create_dir_all(&dir);
284 (dir, format!("{}/{}/", options.generator_root, config_str))
285 };
286
287 GenerationContext {
288 project_root,
289 scan,
290 project_by_name,
291 patterns_by_project,
292 included_projects,
293 non_runtime_names,
294 variant_dir,
295 result_prefix,
296 warnings,
297 }
298}
299
300fn write_variant<R>(ctx: &GenerationContext, render_csproj: R) -> Result<GenerateResult>
301where
302 R: Fn(&ProjectInfo, &str, &str) -> String + Sync,
303{
304 let _span = tracing::info_span!("generate.write_variant", n = ctx.included_projects.len()).entered();
305 let included = &ctx.included_projects;
306 let patterns = &ctx.patterns_by_project;
307 let exclude_names = &ctx.non_runtime_names;
308 let asm_def_by_name = &ctx.scan.asm_def_by_name;
309 let project_by_name = &ctx.project_by_name;
310 let variant_dir = &ctx.variant_dir;
311
312 let results: Vec<Result<()>> = included
314 .par_iter()
315 .map(|project| -> Result<()> {
316 let empty: Vec<String> = Vec::new();
317 let pats = patterns.get(&project.name).unwrap_or(&empty);
318 let source_block = render_compile_patterns(pats);
319 let reference_block = render_project_references(
320 project,
321 asm_def_by_name,
322 project_by_name,
323 exclude_names,
324 );
325 let rendered = render_csproj(project, &source_block, &reference_block);
326 write_file_if_changed(&join_path(variant_dir, &project.csproj_path()), &rendered)?;
327 Ok(())
328 })
329 .collect();
330 for r in results {
331 r?;
332 }
333
334 let project_name = ctx
335 .project_root
336 .rsplit('/')
337 .find(|s| !s.is_empty())
338 .unwrap_or("Project")
339 .to_string();
340 let sln_name = format!("{}.sln", project_name);
341 write_file_if_changed(
342 &join_path(variant_dir, &sln_name),
343 &render_sln(&ctx.included_projects),
344 )?;
345
346 let mut variant_csprojs: Vec<String> = ctx
347 .included_projects
348 .iter()
349 .map(|p| format!("{}{}", ctx.result_prefix, p.csproj_path()))
350 .collect();
351 variant_csprojs.sort();
352
353 Ok(GenerateResult {
354 warnings: ctx.warnings.clone(),
355 variant_csprojs,
356 variant_sln_path: format!("{}{}", ctx.result_prefix, sln_name),
357 })
358}
359
360pub struct SolutionGenerator;
361
362impl SolutionGenerator {
363 pub fn new() -> Self {
364 SolutionGenerator
365 }
366
367 pub fn generate_from_lockfile(
368 &self,
369 options: &GenerateOptions,
370 lockfile: &Lockfile,
371 ) -> Result<GenerateResult> {
372 let _span = tracing::info_span!("generate.from_lockfile").entered();
373 let project_root = resolve_real_path(&options.project_root);
374
375 if let Some(cached) = {
380 let _s = tracing::info_span!("generate.fingerprint_check").entered();
381 crate::generate_cache::try_load_valid(&project_root, options)
382 } {
383 return Ok(cached);
384 }
385
386 let scan = ProjectScanner::scan(&project_root, &options.generator_root)?;
387
388 let mut projects: Vec<ProjectInfo> = Vec::new();
389 let mut all_names: HashSet<String> = HashSet::new();
390 for name in scan.asm_def_by_name.keys() {
391 if !scan.dirs_by_project.contains_key(name) {
392 continue;
393 }
394 projects.push(ProjectInfo {
395 name: name.clone(),
396 guid: deterministic_guid(name),
397 });
398 all_names.insert(name.clone());
399 }
400 for name in scan.dirs_by_project.keys() {
401 if !all_names.contains(name) {
402 projects.push(ProjectInfo {
403 name: name.clone(),
404 guid: deterministic_guid(name),
405 });
406 }
407 }
408 projects.sort_by(|a, b| a.name.cmp(&b.name));
409
410 let ctx = build_context(options, project_root.clone(), projects, scan);
411
412 let mut static_defines: Vec<String> = lockfile.defines.clone();
413 static_defines.extend(lockfile.defines_scripting.iter().cloned());
414 write_file_if_changed(
415 &join_path(&ctx.variant_dir, "Directory.Build.props"),
416 &render_directory_build_props(
417 &ctx.project_root,
418 Some(&lockfile.unity_path),
419 Some(&crate::paths::usg_cache_dir(&lockfile.unity_version)),
420 options.platform,
421 options.build_config,
422 &static_defines,
423 ),
424 )?;
425
426 let refs_block = collect_references_block(
427 lockfile,
428 options.platform,
429 options.build_config == BuildConfig::Editor,
430 &options.extra_refs,
431 );
432 let analyzer_block = render_analyzers(&lockfile.analyzers);
433 let lang_version = lockfile.lang_version.clone();
434 let asm_def_by_name = ctx.scan.asm_def_by_name.clone();
435
436 let result = write_variant(&ctx, |project, source_block, reference_block| {
437 let allow_unsafe = asm_def_by_name
438 .get(&project.name)
439 .map(|a| a.allow_unsafe_code)
440 .unwrap_or(false);
441 let mut out = render_csproj_header(
442 &project.name,
443 &project.guid,
444 &lang_version,
445 allow_unsafe,
446 );
447 out.push_str(&analyzer_block);
448 out.push_str(&refs_block);
449 out.push_str(" <ItemGroup>\n");
450 if !source_block.is_empty() {
451 out.push_str(source_block);
452 out.push('\n');
453 }
454 if !reference_block.is_empty() {
455 out.push_str(reference_block);
456 out.push('\n');
457 }
458 out.push_str(" </ItemGroup>\n");
459 out.push_str(" <Import Project=\"$(MSBuildToolsPath)\\Microsoft.CSharp.targets\" />\n");
460 out.push_str("</Project>\n");
461 out
462 })?;
463
464 crate::generate_cache::write_after_generate(&project_root, options, &result);
465 Ok(result)
466 }
467
468}
469
470impl Default for SolutionGenerator {
471 fn default() -> Self {
472 Self::new()
473 }
474}
475
476fn render_compile_patterns(patterns: &[String]) -> String {
479 patterns
480 .iter()
481 .map(|p| format!(" <Compile Include=\"{}\" />", xml_escape(p)))
482 .collect::<Vec<_>>()
483 .join("\n")
484}
485
486fn render_project_references(
487 project: &ProjectInfo,
488 asm_def_by_name: &HashMap<String, AsmDefRecord>,
489 project_by_name: &HashMap<String, ProjectInfo>,
490 exclude_names: &HashSet<String>,
491) -> String {
492 let Some(asm_def) = asm_def_by_name.get(&project.name) else {
493 return String::new();
494 };
495 let mut seen: HashSet<String> = HashSet::new();
496 let mut blocks: Vec<String> = Vec::new();
497 for reference in &asm_def.references {
498 if exclude_names.contains(reference) {
499 continue;
500 }
501 let Some(ref_proj) = project_by_name.get(reference) else {
502 continue;
503 };
504 if !seen.insert(reference.clone()) {
505 continue;
506 }
507 blocks.push(format!(
508 " <ProjectReference Include=\"{}\">\n <Project>{}</Project>\n <Name>{}</Name>\n </ProjectReference>",
509 xml_escape(&ref_proj.csproj_path()),
510 ref_proj.guid,
511 xml_escape(&ref_proj.name),
512 ));
513 }
514 blocks.join("\n")
515}
516
517fn render_csproj_header(
518 project_name: &str,
519 project_guid: &str,
520 lang_version: &str,
521 allow_unsafe_blocks: bool,
522) -> String {
523 let unsafe_str = if allow_unsafe_blocks { "True" } else { "False" };
524 format!(
525 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\
526<Project ToolsVersion=\"4.0\" DefaultTargets=\"Build\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n\
527 <PropertyGroup>\n\
528 <LangVersion>{lang_version}</LangVersion>\n\
529 <_TargetFrameworkDirectories>non_empty_path_generated_by_unity.rider.package</_TargetFrameworkDirectories>\n\
530 <_FullFrameworkReferenceAssemblyPaths>non_empty_path_generated_by_unity.rider.package</_FullFrameworkReferenceAssemblyPaths>\n\
531 <DisableHandlePackageFileConflicts>true</DisableHandlePackageFileConflicts>\n\
532 </PropertyGroup>\n\
533 <PropertyGroup>\n\
534 <Configuration Condition=\" '$(Configuration)' == '' \">Debug</Configuration>\n\
535 <Platform Condition=\" '$(Platform)' == '' \">AnyCPU</Platform>\n\
536 <ProductVersion>10.0.20506</ProductVersion>\n\
537 <SchemaVersion>2.0</SchemaVersion>\n\
538 <RootNamespace></RootNamespace>\n\
539 <ProjectGuid>{project_guid}</ProjectGuid>\n\
540 <ProjectTypeGuids>{{E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1}};{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}</ProjectTypeGuids>\n\
541 <OutputType>Library</OutputType>\n\
542 <AppDesignerFolder>Properties</AppDesignerFolder>\n\
543 <AssemblyName>{project_name}</AssemblyName>\n\
544 <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>\n\
545 <FileAlignment>512</FileAlignment>\n\
546 <BaseDirectory>.</BaseDirectory>\n\
547 </PropertyGroup>\n\
548 <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' \">\n\
549 <DebugSymbols>true</DebugSymbols>\n\
550 <DebugType>full</DebugType>\n\
551 <Optimize>false</Optimize>\n\
552 <OutputPath>Temp\\Bin\\Debug\\{project_name}\\</OutputPath>\n\
553 <DefineConstants>$(DefineConstants)</DefineConstants>\n\
554 <ErrorReport>prompt</ErrorReport>\n\
555 <WarningLevel>4</WarningLevel>\n\
556 <NoWarn>0169,0649</NoWarn>\n\
557 <AllowUnsafeBlocks>{unsafe_str}</AllowUnsafeBlocks>\n\
558 <TreatWarningsAsErrors>False</TreatWarningsAsErrors>\n\
559 </PropertyGroup>\n\
560 <PropertyGroup>\n\
561 <NoConfig>true</NoConfig>\n\
562 <NoStdLib>true</NoStdLib>\n\
563 <AddAdditionalExplicitAssemblyReferences>false</AddAdditionalExplicitAssemblyReferences>\n\
564 <ImplicitlyExpandNETStandardFacades>false</ImplicitlyExpandNETStandardFacades>\n\
565 <ImplicitlyExpandDesignTimeFacades>false</ImplicitlyExpandDesignTimeFacades>\n\
566 </PropertyGroup>\n",
567 )
568}
569
570fn render_analyzers(analyzers: &[String]) -> String {
571 if analyzers.is_empty() {
572 return String::new();
573 }
574 let mut s = String::from(" <ItemGroup>\n");
575 for path in analyzers {
576 s.push_str(&format!(" <Analyzer Include=\"{}\" />\n", xml_escape(path)));
577 }
578 s.push_str(" </ItemGroup>\n");
579 s
580}
581
582fn collect_references_block(
583 lockfile: &Lockfile,
584 platform: BuildPlatform,
585 is_editor: bool,
586 extra_refs: &[DllRef],
587) -> String {
588 let mut refs: Vec<&DllRef> = Vec::new();
589 let mut seen: HashSet<String> = HashSet::new();
590 let mut cats: Vec<RefCategory> = vec![RefCategory::Engine];
591 if is_editor {
592 cats.push(RefCategory::Editor);
593 }
594 cats.push(RefCategory::PlaybackStandalone);
595 match platform {
596 BuildPlatform::Ios => cats.push(RefCategory::PlaybackIos),
597 BuildPlatform::Android => cats.push(RefCategory::PlaybackAndroid),
598 BuildPlatform::Osx => {}
599 }
600 cats.push(RefCategory::Project);
601 cats.push(RefCategory::Netstandard);
602 for cat in cats {
603 for r in lockfile.refs_for(cat) {
604 if seen.insert(r.name.clone()) {
605 refs.push(r);
606 }
607 }
608 }
609 for r in extra_refs {
610 if seen.insert(r.name.clone()) {
611 refs.push(r);
612 }
613 }
614
615 if refs.is_empty() {
616 return String::new();
617 }
618 let mut s = String::from(" <ItemGroup>\n");
619 for r in &refs {
620 s.push_str(&format!(" <Reference Include=\"{}\">\n", xml_escape(&r.name)));
621 s.push_str(&format!(" <HintPath>{}</HintPath>\n", xml_escape(&r.path)));
622 s.push_str(" </Reference>\n");
623 }
624 s.push_str(" </ItemGroup>\n");
625 s
626}
627
628pub fn render_directory_build_props(
629 project_root: &str,
630 unity_path: Option<&str>,
631 usg_cache: Option<&str>,
632 platform: BuildPlatform,
633 build_config: BuildConfig,
634 static_defines: &[String],
635) -> String {
636 let mut dynamic: Vec<&str> = platform.platform_defines().to_vec();
637 if build_config == BuildConfig::Editor {
638 dynamic.extend_from_slice(EDITOR_DEFINES);
639 }
640 if matches!(build_config, BuildConfig::Editor | BuildConfig::Dev) {
641 dynamic.extend_from_slice(DEBUG_DEFINES);
642 }
643 let mut all: Vec<String> = static_defines.to_vec();
644 all.extend(dynamic.iter().map(|s| s.to_string()));
645
646 let mut props = format!(
647 "<Project>\n<PropertyGroup>\n<ProjectRoot>{}</ProjectRoot>\n",
648 project_root
649 );
650 if let Some(up) = unity_path {
651 props.push_str(&format!("<UnityPath>{}</UnityPath>\n", up));
652 }
653 if let Some(uc) = usg_cache {
654 props.push_str(&format!("<UsgCache>{}</UsgCache>\n", uc));
655 }
656 props.push_str(&format!(
657 "<DefineConstants>$(DefineConstants);{}</DefineConstants>\n</PropertyGroup>\n</Project>\n",
658 all.join(";")
659 ));
660 props
661}
662
663const CSHARP_PROJECT_TYPE_GUID: &str = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}";
664
665fn render_sln(projects: &[ProjectInfo]) -> String {
666 let mut lines: Vec<String> = vec![
667 "Microsoft Visual Studio Solution File, Format Version 11.00".into(),
668 "# Visual Studio 2010".into(),
669 ];
670 for p in projects {
671 lines.push(format!(
672 "Project(\"{}\") = \"{}\", \"{}\", \"{}\"",
673 CSHARP_PROJECT_TYPE_GUID,
674 p.name,
675 p.csproj_path(),
676 p.guid
677 ));
678 lines.push("EndProject".into());
679 }
680 lines.push("Global".into());
681 lines.push("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution".into());
682 lines.push("\t\tDebug|Any CPU = Debug|Any CPU".into());
683 lines.push("\tEndGlobalSection".into());
684 lines.push("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution".into());
685 for p in projects {
686 lines.push(format!("\t\t{}.Debug|Any CPU.ActiveCfg = Debug|Any CPU", p.guid));
687 lines.push(format!("\t\t{}.Debug|Any CPU.Build.0 = Debug|Any CPU", p.guid));
688 }
689 lines.push("\tEndGlobalSection".into());
690 lines.push("EndGlobal".into());
691 lines.push(String::new());
692 lines.join("\n")
693}