Skip to content

Commit a60b7a7

Browse files
Filter projects with missing executables from launch process detection (#364)
1 parent 935d83b commit a60b7a7

File tree

2 files changed

+126
-68
lines changed

2 files changed

+126
-68
lines changed

buildpacks/dotnet/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Launch process detection now skips projects whose executables don't exist at the expected path. ([#364](https://github.com/heroku/buildpacks-dotnet/pull/364))
13+
1014
## [0.14.0] - 2025-12-13
1115

1216
### Added

buildpacks/dotnet/src/launch_process.rs

Lines changed: 122 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use crate::dotnet::solution::Solution;
33
use crate::{Project, utils};
44
use libcnb::data::launch::{Process, ProcessBuilder, ProcessType};
55
use libcnb::data::process_type;
6+
use std::io;
67
use std::path::{Path, PathBuf};
8+
use tracing::instrument;
79

810
/// Detects processes in a solution's projects
911
pub(crate) fn detect_solution_processes(app_dir: &Path, solution: &Solution) -> Vec<Process> {
@@ -18,8 +20,16 @@ pub(crate) fn detect_solution_processes(app_dir: &Path, solution: &Solution) ->
1820
solution
1921
.projects
2022
.iter()
23+
.filter(|project| {
24+
matches!(
25+
project.project_type,
26+
ProjectType::ConsoleApplication
27+
| ProjectType::WebApplication
28+
| ProjectType::WorkerService
29+
)
30+
})
2131
.filter_map(|project| {
22-
let mut process = project_launch_process(app_dir, project)?;
32+
let mut process = project_launch_process(app_dir, project).ok()?;
2333

2434
// If it's a web app and the only one, override its type and make it default.
2535
if has_single_web_app && project.project_type == ProjectType::WebApplication {
@@ -32,21 +42,27 @@ pub(crate) fn detect_solution_processes(app_dir: &Path, solution: &Solution) ->
3242
.collect()
3343
}
3444

35-
/// Determines if a project should have a launchable process and constructs it
36-
fn project_launch_process(app_dir: &Path, project: &Project) -> Option<Process> {
37-
if !matches!(
38-
project.project_type,
39-
ProjectType::ConsoleApplication | ProjectType::WebApplication | ProjectType::WorkerService
40-
) {
41-
return None;
45+
#[instrument(skip(app_dir), err)]
46+
fn project_launch_process(app_dir: &Path, project: &Project) -> io::Result<Process> {
47+
let executable_path = project_executable_path(project);
48+
49+
if !executable_path.exists() {
50+
return Err(io::Error::new(
51+
io::ErrorKind::NotFound,
52+
format!("Executable not found: {}", executable_path.display()),
53+
));
4254
}
43-
let relative_executable_path = relative_executable_path(app_dir, project);
55+
56+
let relative_executable_path = executable_path
57+
.strip_prefix(app_dir)
58+
.expect("Executable path should be inside the app directory")
59+
.to_path_buf();
4460

4561
let command = build_command(&relative_executable_path, project.project_type);
4662

4763
let process_type = project_process_type(project);
4864

49-
Some(ProcessBuilder::new(process_type, ["bash", "-c", &command]).build())
65+
Ok(ProcessBuilder::new(process_type, ["bash", "-c", &command]).build())
5066
}
5167

5268
/// Constructs the shell command for launching the process
@@ -84,14 +100,6 @@ fn project_process_type(project: &Project) -> ProcessType {
84100
.expect("Sanitized process type name should always be valid")
85101
}
86102

87-
/// Returns the (expected) relative executable path from the app directory
88-
fn relative_executable_path(app_dir: &Path, project: &Project) -> PathBuf {
89-
project_executable_path(project)
90-
.strip_prefix(app_dir)
91-
.expect("Executable path should be inside the app directory")
92-
.to_path_buf()
93-
}
94-
95103
/// Returns the (expected) absolute path to the project's compiled executable
96104
fn project_executable_path(project: &Project) -> PathBuf {
97105
project
@@ -108,6 +116,7 @@ mod tests {
108116
use super::*;
109117
use libcnb::data::launch::{Process, WorkingDirectory};
110118
use libcnb::data::process_type;
119+
use std::fs;
111120
use std::path::PathBuf;
112121

113122
fn create_test_project(path: &str, assembly_name: &str, project_type: ProjectType) -> Project {
@@ -119,18 +128,45 @@ mod tests {
119128
}
120129
}
121130

131+
fn create_executable_for_project(project: &Project) {
132+
let executable_path = project_executable_path(project);
133+
fs::create_dir_all(executable_path.parent().unwrap()).unwrap();
134+
fs::write(&executable_path, "").unwrap();
135+
}
136+
122137
#[test]
123-
fn test_detect_solution_processes_single_web_app() {
124-
let app_dir = Path::new("/tmp");
138+
fn test_detect_solution_processes_missing_project_executable() {
139+
let temp_dir = tempfile::tempdir().unwrap();
140+
let app_dir = temp_dir.path();
141+
125142
let solution = Solution {
126-
path: PathBuf::from("/tmp/foo.sln"),
143+
path: app_dir.join("foo.sln"),
127144
projects: vec![create_test_project(
128-
"/tmp/bar/bar.csproj",
145+
&format!("{}/bar/bar.csproj", app_dir.display()),
129146
"bar",
130147
ProjectType::WebApplication,
131148
)],
132149
};
133150

151+
assert!(detect_solution_processes(app_dir, &solution).is_empty());
152+
}
153+
154+
#[test]
155+
fn test_detect_solution_processes_single_web_app() {
156+
let temp_dir = tempfile::tempdir().unwrap();
157+
let app_dir = temp_dir.path();
158+
let project = create_test_project(
159+
&format!("{}/bar/bar.csproj", app_dir.display()),
160+
"bar",
161+
ProjectType::WebApplication,
162+
);
163+
create_executable_for_project(&project);
164+
165+
let solution = Solution {
166+
path: app_dir.join("foo.sln"),
167+
projects: vec![project],
168+
};
169+
134170
let expected_processes = vec![Process {
135171
r#type: process_type!("web"),
136172
command: vec![
@@ -151,13 +187,24 @@ mod tests {
151187

152188
#[test]
153189
fn test_detect_solution_processes_multiple_web_apps() {
154-
let app_dir = Path::new("/tmp");
190+
let temp_dir = tempfile::tempdir().unwrap();
191+
let app_dir = temp_dir.path();
192+
let project1 = create_test_project(
193+
&format!("{}/bar/bar.csproj", app_dir.display()),
194+
"bar",
195+
ProjectType::WebApplication,
196+
);
197+
let project2 = create_test_project(
198+
&format!("{}/baz/baz.csproj", app_dir.display()),
199+
"baz",
200+
ProjectType::WebApplication,
201+
);
202+
create_executable_for_project(&project1);
203+
create_executable_for_project(&project2);
204+
155205
let solution = Solution {
156-
path: PathBuf::from("/tmp/foo.sln"),
157-
projects: vec![
158-
create_test_project("/tmp/bar/bar.csproj", "bar", ProjectType::WebApplication),
159-
create_test_project("/tmp/baz/baz.csproj", "baz", ProjectType::WebApplication),
160-
],
206+
path: app_dir.join("foo.sln"),
207+
projects: vec![project1, project2],
161208
};
162209
assert_eq!(
163210
detect_solution_processes(app_dir, &solution)
@@ -170,18 +217,29 @@ mod tests {
170217

171218
#[test]
172219
fn test_detect_solution_processes_single_web_app_and_console_app() {
173-
let app_dir = Path::new("/tmp");
220+
let temp_dir = tempfile::tempdir().unwrap();
221+
let app_dir = temp_dir.path();
222+
let project1 = create_test_project(
223+
&format!("{}/qux/qux.csproj", app_dir.display()),
224+
"qux",
225+
ProjectType::Unknown,
226+
);
227+
let project2 = create_test_project(
228+
&format!("{}/bar/bar.csproj", app_dir.display()),
229+
"bar",
230+
ProjectType::WebApplication,
231+
);
232+
let project3 = create_test_project(
233+
&format!("{}/baz/baz.csproj", app_dir.display()),
234+
"baz",
235+
ProjectType::ConsoleApplication,
236+
);
237+
create_executable_for_project(&project2);
238+
create_executable_for_project(&project3);
239+
174240
let solution = Solution {
175-
path: PathBuf::from("/tmp/foo.sln"),
176-
projects: vec![
177-
create_test_project("/tmp/qux/qux.csproj", "qux", ProjectType::Unknown),
178-
create_test_project("/tmp/bar/bar.csproj", "bar", ProjectType::WebApplication),
179-
create_test_project(
180-
"/tmp/baz/baz.csproj",
181-
"baz",
182-
ProjectType::ConsoleApplication,
183-
),
184-
],
241+
path: app_dir.join("foo.sln"),
242+
projects: vec![project1, project2, project3],
185243
};
186244
assert_eq!(
187245
detect_solution_processes(app_dir, &solution)
@@ -194,14 +252,21 @@ mod tests {
194252

195253
#[test]
196254
fn test_detect_solution_processes_with_spaces() {
197-
let app_dir = Path::new("/tmp");
255+
let temp_dir = tempfile::tempdir().unwrap();
256+
let app_dir = temp_dir.path();
257+
let project = create_test_project(
258+
&format!(
259+
"{}/My Project With Spaces/project.csproj",
260+
app_dir.display()
261+
),
262+
"My App",
263+
ProjectType::ConsoleApplication,
264+
);
265+
create_executable_for_project(&project);
266+
198267
let solution = Solution {
199-
path: PathBuf::from("/tmp/My Solution With Spaces.sln"),
200-
projects: vec![create_test_project(
201-
"/tmp/My Project With Spaces/project.csproj",
202-
"My App",
203-
ProjectType::ConsoleApplication,
204-
)],
268+
path: app_dir.join("My Solution With Spaces.sln"),
269+
projects: vec![project],
205270
};
206271

207272
let expected_processes = vec![Process {
@@ -222,21 +287,6 @@ mod tests {
222287
);
223288
}
224289

225-
#[test]
226-
fn test_relative_executable_path() {
227-
let app_dir = Path::new("/tmp");
228-
let project = create_test_project(
229-
"/tmp/project/project.csproj",
230-
"TestApp",
231-
ProjectType::ConsoleApplication,
232-
);
233-
234-
assert_eq!(
235-
relative_executable_path(app_dir, &project),
236-
PathBuf::from("project/bin/publish/TestApp")
237-
);
238-
}
239-
240290
#[test]
241291
fn test_project_executable_path() {
242292
let project = create_test_project(
@@ -279,14 +329,18 @@ mod tests {
279329

280330
#[test]
281331
fn test_detect_solution_processes_nested_solution() {
282-
let app_dir = Path::new("/tmp");
332+
let temp_dir = tempfile::tempdir().unwrap();
333+
let app_dir = temp_dir.path();
334+
let project = create_test_project(
335+
&format!("{}/src/MyApp/MyApp.csproj", app_dir.display()), // Project is also in src/ subdirectory
336+
"MyApp",
337+
ProjectType::WebApplication,
338+
);
339+
create_executable_for_project(&project);
340+
283341
let solution = Solution {
284-
path: PathBuf::from("/tmp/src/MyApp.sln"), // Solution is in src/ subdirectory
285-
projects: vec![create_test_project(
286-
"/tmp/src/MyApp/MyApp.csproj", // Project is also in src/ subdirectory
287-
"MyApp",
288-
ProjectType::WebApplication,
289-
)],
342+
path: app_dir.join("src/MyApp.sln"), // Solution is in src/ subdirectory
343+
projects: vec![project],
290344
};
291345

292346
let expected_processes = vec![Process {

0 commit comments

Comments
 (0)