|
1 | 1 | use crate::dotnet::project::ProjectType;
|
2 | 2 | use crate::dotnet::solution::Solution;
|
| 3 | +use crate::Project; |
3 | 4 | use libcnb::data::launch::{Process, ProcessBuilder, ProcessType, ProcessTypeError};
|
| 5 | +use std::path::PathBuf; |
4 | 6 |
|
5 | 7 | #[derive(Debug)]
|
6 | 8 | pub(crate) enum LaunchProcessDetectionError {
|
7 | 9 | ProcessType(ProcessTypeError),
|
8 | 10 | }
|
9 | 11 |
|
| 12 | +/// Detects processes in a solution's projects |
10 | 13 | pub(crate) fn detect_solution_processes(
|
11 | 14 | solution: &Solution,
|
12 | 15 | ) -> Result<Vec<Process>, LaunchProcessDetectionError> {
|
13 | 16 | solution
|
14 | 17 | .projects
|
15 | 18 | .iter()
|
16 |
| - .filter(|project| { |
17 |
| - matches!( |
18 |
| - project.project_type, |
19 |
| - ProjectType::ConsoleApplication |
20 |
| - | ProjectType::WebApplication |
21 |
| - | ProjectType::WorkerService |
22 |
| - ) |
23 |
| - }) |
24 |
| - .map(|project| { |
25 |
| - let executable_path = project |
| 19 | + .filter_map(|project| project_launch_process(solution, project)) |
| 20 | + .collect::<Result<_, _>>() |
| 21 | +} |
| 22 | + |
| 23 | +/// Determines if a project should have a launchable process and constructs it |
| 24 | +fn project_launch_process( |
| 25 | + solution: &Solution, |
| 26 | + project: &Project, |
| 27 | +) -> Option<Result<Process, LaunchProcessDetectionError>> { |
| 28 | + if !matches!( |
| 29 | + project.project_type, |
| 30 | + ProjectType::ConsoleApplication | ProjectType::WebApplication | ProjectType::WorkerService |
| 31 | + ) { |
| 32 | + return None; |
| 33 | + } |
| 34 | + let relative_executable_path = relative_executable_path(solution, project); |
| 35 | + |
| 36 | + let mut command = format!( |
| 37 | + "cd {}; ./{}", |
| 38 | + relative_executable_path |
| 39 | + .parent() |
| 40 | + .expect("Path to always have a parent directory") |
| 41 | + .display(), |
| 42 | + relative_executable_path |
| 43 | + .file_name() |
| 44 | + .expect("Path to never terminate in `..`") |
| 45 | + .to_string_lossy() |
| 46 | + ); |
| 47 | + |
| 48 | + if project.project_type == ProjectType::WebApplication { |
| 49 | + command.push_str(" --urls http://*:$PORT"); |
| 50 | + } |
| 51 | + |
| 52 | + Some( |
| 53 | + project_process_type(project).map(|process_type| { |
| 54 | + ProcessBuilder::new(process_type, ["bash", "-c", &command]).build() |
| 55 | + }), |
| 56 | + ) |
| 57 | +} |
| 58 | + |
| 59 | +fn project_process_type(project: &Project) -> Result<ProcessType, LaunchProcessDetectionError> { |
| 60 | + project |
| 61 | + .assembly_name |
| 62 | + .parse::<ProcessType>() |
| 63 | + .map_err(LaunchProcessDetectionError::ProcessType) |
| 64 | +} |
| 65 | + |
| 66 | +/// Returns the (expected) relative executable path from the solution's parent directory |
| 67 | +fn relative_executable_path(solution: &Solution, project: &Project) -> PathBuf { |
| 68 | + project_executable_path(project) |
| 69 | + .strip_prefix( |
| 70 | + solution |
26 | 71 | .path
|
27 | 72 | .parent()
|
28 |
| - .expect("Project file should always have a parent directory") |
29 |
| - .join("bin") |
30 |
| - .join("publish") |
31 |
| - .join(&project.assembly_name); |
32 |
| - |
33 |
| - let relative_executable_path = executable_path |
34 |
| - .strip_prefix( |
35 |
| - solution |
36 |
| - .path |
37 |
| - .parent() |
38 |
| - .expect("Solution path to have a parent"), |
39 |
| - ) |
40 |
| - .expect("Project to be nested in solution parent directory"); |
41 |
| - |
42 |
| - let mut command = format!( |
43 |
| - "cd {}; ./{}", |
44 |
| - relative_executable_path |
45 |
| - .parent() |
46 |
| - .expect("Path to always have a parent directory") |
47 |
| - .display(), |
48 |
| - relative_executable_path |
49 |
| - .file_name() |
50 |
| - .expect("Path to never terminate in `..`") |
51 |
| - .to_string_lossy() |
52 |
| - ); |
53 |
| - |
54 |
| - if project.project_type == ProjectType::WebApplication { |
55 |
| - command.push_str(" --urls http://*:$PORT"); |
56 |
| - } |
57 |
| - |
58 |
| - project |
59 |
| - .assembly_name |
60 |
| - .parse::<ProcessType>() |
61 |
| - .map_err(LaunchProcessDetectionError::ProcessType) |
62 |
| - .map(|process_type| { |
63 |
| - ProcessBuilder::new(process_type, ["bash", "-c", &command]).build() |
64 |
| - }) |
65 |
| - }) |
66 |
| - .collect::<Result<_, _>>() |
| 73 | + .expect("Solution path to have a parent"), |
| 74 | + ) |
| 75 | + .expect("Project to be nested in solution parent directory") |
| 76 | + .to_path_buf() |
| 77 | +} |
| 78 | + |
| 79 | +/// Returns the (expected) absolute path to the project's compiled executable |
| 80 | +fn project_executable_path(project: &Project) -> PathBuf { |
| 81 | + project |
| 82 | + .path |
| 83 | + .parent() |
| 84 | + .expect("Project file should always have a parent directory") |
| 85 | + .join("bin") |
| 86 | + .join("publish") |
| 87 | + .join(&project.assembly_name) |
| 88 | +} |
| 89 | + |
| 90 | +#[cfg(test)] |
| 91 | +mod tests { |
| 92 | + use super::*; |
| 93 | + use libcnb::data::launch::{Process, WorkingDirectory}; |
| 94 | + use libcnb::data::process_type; |
| 95 | + use std::path::PathBuf; |
| 96 | + |
| 97 | + #[test] |
| 98 | + fn test_detect_solution_processes_web_app() { |
| 99 | + let solution = Solution { |
| 100 | + path: PathBuf::from("/tmp/foo.sln"), |
| 101 | + projects: vec![Project { |
| 102 | + path: PathBuf::from("/tmp/bar/bar.csproj"), |
| 103 | + target_framework: "net9.0".to_string(), |
| 104 | + project_type: ProjectType::WebApplication, |
| 105 | + assembly_name: "bar".to_string(), |
| 106 | + }], |
| 107 | + }; |
| 108 | + |
| 109 | + let expected_processes = vec![Process { |
| 110 | + r#type: process_type!("bar"), |
| 111 | + command: vec![ |
| 112 | + "bash".to_string(), |
| 113 | + "-c".to_string(), |
| 114 | + "cd bar/bin/publish; ./bar --urls http://*:$PORT".to_string(), |
| 115 | + ], |
| 116 | + args: vec![], |
| 117 | + default: false, |
| 118 | + working_directory: WorkingDirectory::App, |
| 119 | + }]; |
| 120 | + |
| 121 | + assert_eq!( |
| 122 | + detect_solution_processes(&solution).unwrap(), |
| 123 | + expected_processes |
| 124 | + ); |
| 125 | + } |
| 126 | + |
| 127 | + #[test] |
| 128 | + fn test_detect_solution_processes_console_app() { |
| 129 | + let solution = Solution { |
| 130 | + path: PathBuf::from("/tmp/foo.sln"), |
| 131 | + projects: vec![Project { |
| 132 | + path: PathBuf::from("/tmp/bar/bar.csproj"), |
| 133 | + target_framework: "net9.0".to_string(), |
| 134 | + project_type: ProjectType::ConsoleApplication, |
| 135 | + assembly_name: "bar".to_string(), |
| 136 | + }], |
| 137 | + }; |
| 138 | + |
| 139 | + let expected_processes = vec![Process { |
| 140 | + r#type: process_type!("bar"), |
| 141 | + command: vec![ |
| 142 | + "bash".to_string(), |
| 143 | + "-c".to_string(), |
| 144 | + "cd bar/bin/publish; ./bar".to_string(), |
| 145 | + ], |
| 146 | + args: vec![], |
| 147 | + default: false, |
| 148 | + working_directory: WorkingDirectory::App, |
| 149 | + }]; |
| 150 | + |
| 151 | + assert_eq!( |
| 152 | + detect_solution_processes(&solution).unwrap(), |
| 153 | + expected_processes |
| 154 | + ); |
| 155 | + } |
| 156 | + |
| 157 | + #[test] |
| 158 | + fn test_project_launch_process_non_executable() { |
| 159 | + let solution = Solution { |
| 160 | + path: PathBuf::from("/tmp/foo.sln"), |
| 161 | + projects: vec![Project { |
| 162 | + path: PathBuf::from("/tmp/bar/bar.csproj"), |
| 163 | + target_framework: "net9.0".to_string(), |
| 164 | + project_type: ProjectType::Unknown, |
| 165 | + assembly_name: "bar".to_string(), |
| 166 | + }], |
| 167 | + }; |
| 168 | + |
| 169 | + assert!(detect_solution_processes(&solution).unwrap().is_empty()); |
| 170 | + } |
| 171 | + |
| 172 | + #[test] |
| 173 | + fn test_project_executable_path() { |
| 174 | + let project = Project { |
| 175 | + path: PathBuf::from("/tmp/project/project.csproj"), |
| 176 | + target_framework: "net9.0".to_string(), |
| 177 | + project_type: ProjectType::ConsoleApplication, |
| 178 | + assembly_name: "TestApp".to_string(), |
| 179 | + }; |
| 180 | + |
| 181 | + assert_eq!( |
| 182 | + project_executable_path(&project), |
| 183 | + PathBuf::from("/tmp/project/bin/publish/TestApp") |
| 184 | + ); |
| 185 | + } |
| 186 | + |
| 187 | + #[test] |
| 188 | + fn test_relative_executable_path() { |
| 189 | + let solution = Solution { |
| 190 | + path: PathBuf::from("/tmp/solution.sln"), |
| 191 | + projects: vec![], |
| 192 | + }; |
| 193 | + |
| 194 | + let project = Project { |
| 195 | + path: PathBuf::from("/tmp/project/project.csproj"), |
| 196 | + target_framework: "net9.0".to_string(), |
| 197 | + project_type: ProjectType::ConsoleApplication, |
| 198 | + assembly_name: "TestApp".to_string(), |
| 199 | + }; |
| 200 | + |
| 201 | + assert_eq!( |
| 202 | + relative_executable_path(&solution, &project), |
| 203 | + PathBuf::from("project/bin/publish/TestApp") |
| 204 | + ); |
| 205 | + } |
67 | 206 | }
|
0 commit comments