Skip to content

Commit 9c3c5e0

Browse files
authored
Merge pull request #2845 from itowlson/search-up-dir-tree-for-spin-toml
Enable running some commands from subdirectories
2 parents 11e0d32 + 7d9e69d commit 9c3c5e0

File tree

9 files changed

+148
-21
lines changed

9 files changed

+148
-21
lines changed

crates/common/src/paths.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ use crate::ui::quoted_path;
88
/// The name given to the default manifest file.
99
pub const DEFAULT_MANIFEST_FILE: &str = "spin.toml";
1010

11+
/// Attempts to find a manifest. If a path is provided, that path is resolved
12+
/// using `resolve_manifest_file_path`; otherwise, a directory search is carried out
13+
/// using `search_upwards_for_manifest`. If we had to search, and a manifest is found,
14+
/// a (non-zero) usize is returned indicating how far above the current directory it
15+
/// was found. (A usize of 0 indicates that the manifest was provided or found
16+
/// in the current directory.) This can be used to notify the user that a
17+
/// non-default manifest is being used.
18+
pub fn find_manifest_file_path(
19+
provided_path: Option<impl AsRef<Path>>,
20+
) -> Result<(PathBuf, usize)> {
21+
match provided_path {
22+
Some(provided_path) => resolve_manifest_file_path(provided_path).map(|p| (p, 0)),
23+
None => search_upwards_for_manifest()
24+
.ok_or_else(|| anyhow!("\"{}\" not found", DEFAULT_MANIFEST_FILE)),
25+
}
26+
}
27+
1128
/// Resolves a manifest path provided by a user, which may be a file or
1229
/// directory, to a path to a manifest file.
1330
pub fn resolve_manifest_file_path(provided_path: impl AsRef<Path>) -> Result<PathBuf> {
@@ -36,6 +53,43 @@ pub fn resolve_manifest_file_path(provided_path: impl AsRef<Path>) -> Result<Pat
3653
}
3754
}
3855

56+
/// Starting from the current directory, searches upward through
57+
/// the directory tree for a manifest (that is, a file with the default
58+
/// manifest name `spin.toml`). If found, the path to the manifest
59+
/// is returned, with a usize indicating how far above the current directory it
60+
/// was found. (A usize of 0 indicates that the manifest was provided or found
61+
/// in the current directory.) This can be used to notify the user that a
62+
/// non-default manifest is being used.
63+
/// If no matching file is found, the function returns None.
64+
///
65+
/// The search is abandoned if it reaches the root directory, or the
66+
/// root of a Git repository, without finding a 'spin.toml'.
67+
pub fn search_upwards_for_manifest() -> Option<(PathBuf, usize)> {
68+
let candidate = PathBuf::from(DEFAULT_MANIFEST_FILE);
69+
70+
if candidate.is_file() {
71+
return Some((candidate, 0));
72+
}
73+
74+
for distance in 1..20 {
75+
let inferred_dir = PathBuf::from("../".repeat(distance));
76+
if !inferred_dir.is_dir() {
77+
return None;
78+
}
79+
80+
let candidate = inferred_dir.join(DEFAULT_MANIFEST_FILE);
81+
if candidate.is_file() {
82+
return Some((candidate, distance));
83+
}
84+
85+
if is_git_root(&inferred_dir) {
86+
return None;
87+
}
88+
}
89+
90+
None
91+
}
92+
3993
/// Resolves the parent directory of a path, returning an error if the path
4094
/// has no parent. A path with a single component will return ".".
4195
pub fn parent_dir(path: impl AsRef<Path>) -> Result<PathBuf> {
@@ -49,6 +103,10 @@ pub fn parent_dir(path: impl AsRef<Path>) -> Result<PathBuf> {
49103
Ok(parent.into())
50104
}
51105

106+
fn is_git_root(dir: &Path) -> bool {
107+
dir.join(".git").is_dir()
108+
}
109+
52110
#[cfg(test)]
53111
mod tests {
54112
use super::*;

crates/terminal/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ macro_rules! ceprint {
128128
};
129129
}
130130

131+
#[macro_export]
132+
macro_rules! ceprintln {
133+
($color:expr, $($arg:tt)*) => {
134+
use std::io::Write;
135+
let mut out = $crate::ColorText::stderr($color);
136+
let _ = writeln!(out, $($arg)*);
137+
drop(out); // Reset colors
138+
};
139+
}
140+
131141
pub mod colors {
132142
use termcolor::{Color, ColorSpec};
133143

src/commands/build.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use std::{ffi::OsString, path::PathBuf};
33
use anyhow::Result;
44
use clap::Parser;
55

6-
use crate::opts::{APP_MANIFEST_FILE_OPT, BUILD_UP_OPT, DEFAULT_MANIFEST_FILE};
6+
use crate::{
7+
directory_rels::notify_if_nondefault_rel,
8+
opts::{APP_MANIFEST_FILE_OPT, BUILD_UP_OPT},
9+
};
710

811
use super::up::UpCommand;
912

@@ -19,9 +22,8 @@ pub struct BuildCommand {
1922
short = 'f',
2023
long = "from",
2124
alias = "file",
22-
default_value = DEFAULT_MANIFEST_FILE
2325
)]
24-
pub app_source: PathBuf,
26+
pub app_source: Option<PathBuf>,
2527

2628
/// Component ID to build. This can be specified multiple times. The default is all components.
2729
#[clap(short = 'c', long, multiple = true)]
@@ -37,7 +39,10 @@ pub struct BuildCommand {
3739

3840
impl BuildCommand {
3941
pub async fn run(self) -> Result<()> {
40-
let manifest_file = spin_common::paths::resolve_manifest_file_path(&self.app_source)?;
42+
let (manifest_file, distance) =
43+
spin_common::paths::find_manifest_file_path(self.app_source.as_ref())?;
44+
notify_if_nondefault_rel(&manifest_file, distance);
45+
4146
spin_build::build(&manifest_file, &self.component_id).await?;
4247

4348
if self.up {

src/commands/doctor.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use clap::Parser;
55
use dialoguer::{console::Emoji, Confirm, Select};
66
use spin_doctor::{Diagnosis, DryRunNotSupported, PatientDiagnosis};
77

8-
use crate::opts::{APP_MANIFEST_FILE_OPT, DEFAULT_MANIFEST_FILE};
8+
use crate::opts::APP_MANIFEST_FILE_OPT;
99

1010
#[derive(Parser, Debug)]
1111
#[clap(about = "Detect and fix problems with Spin applications")]
@@ -17,15 +17,21 @@ pub struct DoctorCommand {
1717
name = APP_MANIFEST_FILE_OPT,
1818
short = 'f',
1919
long = "from",
20-
alias = "file",
21-
default_value = DEFAULT_MANIFEST_FILE
20+
alias = "file"
2221
)]
23-
pub app_source: PathBuf,
22+
pub app_source: Option<PathBuf>,
2423
}
2524

2625
impl DoctorCommand {
2726
pub async fn run(self) -> Result<()> {
28-
let manifest_file = spin_common::paths::resolve_manifest_file_path(&self.app_source)?;
27+
let (manifest_file, distance) =
28+
spin_common::paths::find_manifest_file_path(self.app_source.as_ref())?;
29+
if distance > 0 {
30+
anyhow::bail!(
31+
"No spin.toml in current directory - did you mean '--from {}'?",
32+
manifest_file.display()
33+
);
34+
}
2935

3036
println!("{icon}The Spin Doctor is in.", icon = Emoji("📟 ", ""));
3137
println!(

src/commands/registry.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::opts::*;
1+
use crate::{directory_rels::notify_if_nondefault_rel, opts::*};
22
use anyhow::{Context, Result};
33
use clap::{Parser, Subcommand};
44
use indicatif::{ProgressBar, ProgressStyle};
@@ -37,9 +37,8 @@ pub struct Push {
3737
short = 'f',
3838
long = "from",
3939
alias = "file",
40-
default_value = DEFAULT_MANIFEST_FILE
4140
)]
42-
pub app_source: PathBuf,
41+
pub app_source: Option<PathBuf>,
4342

4443
/// Ignore server certificate errors
4544
#[clap(
@@ -71,7 +70,10 @@ pub struct Push {
7170

7271
impl Push {
7372
pub async fn run(self) -> Result<()> {
74-
let app_file = spin_common::paths::resolve_manifest_file_path(&self.app_source)?;
73+
let (app_file, distance) =
74+
spin_common::paths::find_manifest_file_path(self.app_source.as_ref())?;
75+
notify_if_nondefault_rel(&app_file, distance);
76+
7577
if self.build {
7678
spin_build::build(&app_file, &[]).await?;
7779
}

src/commands/up.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use spin_oci::OciLoader;
1919
use spin_trigger::cli::{LaunchMetadata, SPIN_LOCAL_APP_DIR, SPIN_LOCKED_URL, SPIN_WORKING_DIR};
2020
use tempfile::TempDir;
2121

22-
use crate::opts::*;
22+
use crate::{directory_rels::notify_if_nondefault_rel, opts::*};
2323

2424
use self::app_source::{AppSource, ResolvedAppSource};
2525

@@ -401,7 +401,13 @@ impl UpCommand {
401401
);
402402
AppSource::Unresolvable(msg)
403403
} else {
404-
AppSource::None
404+
match spin_common::paths::search_upwards_for_manifest() {
405+
Some((manifest_path, distance)) => {
406+
notify_if_nondefault_rel(&manifest_path, distance);
407+
AppSource::File(manifest_path)
408+
}
409+
None => AppSource::None,
410+
}
405411
}
406412
}
407413

src/commands/watch.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ use spin_common::paths::parent_dir;
1212
use uuid::Uuid;
1313
use watchexec::Watchexec;
1414

15-
use crate::opts::{
16-
APP_MANIFEST_FILE_OPT, DEFAULT_MANIFEST_FILE, WATCH_CLEAR_OPT, WATCH_DEBOUNCE_OPT,
17-
WATCH_SKIP_BUILD_OPT,
15+
use crate::{
16+
directory_rels::notify_if_nondefault_rel,
17+
opts::{APP_MANIFEST_FILE_OPT, WATCH_CLEAR_OPT, WATCH_DEBOUNCE_OPT, WATCH_SKIP_BUILD_OPT},
1818
};
1919

2020
mod buildifier;
@@ -41,9 +41,8 @@ pub struct WatchCommand {
4141
short = 'f',
4242
long = "from",
4343
alias = "file",
44-
default_value = DEFAULT_MANIFEST_FILE
4544
)]
46-
pub app_source: PathBuf,
45+
pub app_source: Option<PathBuf>,
4746

4847
/// Clear the screen before each run.
4948
#[clap(
@@ -93,7 +92,10 @@ impl WatchCommand {
9392
// has just done so. Subsequent asset changes _do_ clear the screen.
9493

9594
let spin_bin = std::env::current_exe()?;
96-
let manifest_file = spin_common::paths::resolve_manifest_file_path(&self.app_source)?;
95+
let (manifest_file, distance) =
96+
spin_common::paths::find_manifest_file_path(self.app_source.as_ref())?;
97+
notify_if_nondefault_rel(&manifest_file, distance);
98+
9799
let manifest_file = manifest_file.absolutize()?.to_path_buf(); // or watchexec misses files in subdirectories
98100
let manifest_dir = parent_dir(&manifest_file)?;
99101

src/directory_rels.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! Human-readable descriptions for directory relationships,
2+
//! and helpers for standard display.
3+
4+
use std::path::Path;
5+
6+
fn parent_rel(distance: usize) -> String {
7+
match distance {
8+
0 => "".to_owned(),
9+
1 => "parent".to_owned(),
10+
2 => "grandparent".to_owned(),
11+
_ => format!("{}grandparent", "great-".repeat(distance - 2)),
12+
}
13+
}
14+
15+
pub fn notify_if_nondefault_rel(manifest_file: &Path, distance: usize) {
16+
if distance > 0 {
17+
terminal::einfo!(
18+
"No 'spin.toml' in current directory.",
19+
"Using 'spin.toml' from {} directory ({})",
20+
parent_rel(distance),
21+
manifest_file.display(),
22+
);
23+
}
24+
}
25+
26+
#[cfg(test)]
27+
mod tests {
28+
use super::*;
29+
30+
#[test]
31+
fn ancestry_text_is_correct() {
32+
assert_eq!("parent", parent_rel(1));
33+
assert_eq!("grandparent", parent_rel(2));
34+
assert_eq!("great-grandparent", parent_rel(3));
35+
assert_eq!("great-great-great-grandparent", parent_rel(5)); // I hope you're happy Lann
36+
}
37+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod build_info;
22
pub mod commands;
3+
mod directory_rels;
34
pub(crate) mod opts;
45
pub mod subprocess;
56

0 commit comments

Comments
 (0)