Skip to content

Commit

Permalink
Merge pull request #2845 from itowlson/search-up-dir-tree-for-spin-toml
Browse files Browse the repository at this point in the history
Enable running some commands from subdirectories
  • Loading branch information
itowlson authored Sep 23, 2024
2 parents 11e0d32 + 7d9e69d commit 9c3c5e0
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 21 deletions.
58 changes: 58 additions & 0 deletions crates/common/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ use crate::ui::quoted_path;
/// The name given to the default manifest file.
pub const DEFAULT_MANIFEST_FILE: &str = "spin.toml";

/// Attempts to find a manifest. If a path is provided, that path is resolved
/// using `resolve_manifest_file_path`; otherwise, a directory search is carried out
/// using `search_upwards_for_manifest`. If we had to search, and a manifest is found,
/// a (non-zero) usize is returned indicating how far above the current directory it
/// was found. (A usize of 0 indicates that the manifest was provided or found
/// in the current directory.) This can be used to notify the user that a
/// non-default manifest is being used.
pub fn find_manifest_file_path(
provided_path: Option<impl AsRef<Path>>,
) -> Result<(PathBuf, usize)> {
match provided_path {
Some(provided_path) => resolve_manifest_file_path(provided_path).map(|p| (p, 0)),
None => search_upwards_for_manifest()
.ok_or_else(|| anyhow!("\"{}\" not found", DEFAULT_MANIFEST_FILE)),
}
}

/// Resolves a manifest path provided by a user, which may be a file or
/// directory, to a path to a manifest file.
pub fn resolve_manifest_file_path(provided_path: impl AsRef<Path>) -> Result<PathBuf> {
Expand Down Expand Up @@ -36,6 +53,43 @@ pub fn resolve_manifest_file_path(provided_path: impl AsRef<Path>) -> Result<Pat
}
}

/// Starting from the current directory, searches upward through
/// the directory tree for a manifest (that is, a file with the default
/// manifest name `spin.toml`). If found, the path to the manifest
/// is returned, with a usize indicating how far above the current directory it
/// was found. (A usize of 0 indicates that the manifest was provided or found
/// in the current directory.) This can be used to notify the user that a
/// non-default manifest is being used.
/// If no matching file is found, the function returns None.
///
/// The search is abandoned if it reaches the root directory, or the
/// root of a Git repository, without finding a 'spin.toml'.
pub fn search_upwards_for_manifest() -> Option<(PathBuf, usize)> {
let candidate = PathBuf::from(DEFAULT_MANIFEST_FILE);

if candidate.is_file() {
return Some((candidate, 0));
}

for distance in 1..20 {
let inferred_dir = PathBuf::from("../".repeat(distance));
if !inferred_dir.is_dir() {
return None;
}

let candidate = inferred_dir.join(DEFAULT_MANIFEST_FILE);
if candidate.is_file() {
return Some((candidate, distance));
}

if is_git_root(&inferred_dir) {
return None;
}
}

None
}

/// Resolves the parent directory of a path, returning an error if the path
/// has no parent. A path with a single component will return ".".
pub fn parent_dir(path: impl AsRef<Path>) -> Result<PathBuf> {
Expand All @@ -49,6 +103,10 @@ pub fn parent_dir(path: impl AsRef<Path>) -> Result<PathBuf> {
Ok(parent.into())
}

fn is_git_root(dir: &Path) -> bool {
dir.join(".git").is_dir()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
10 changes: 10 additions & 0 deletions crates/terminal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ macro_rules! ceprint {
};
}

#[macro_export]
macro_rules! ceprintln {
($color:expr, $($arg:tt)*) => {
use std::io::Write;
let mut out = $crate::ColorText::stderr($color);
let _ = writeln!(out, $($arg)*);
drop(out); // Reset colors
};
}

pub mod colors {
use termcolor::{Color, ColorSpec};

Expand Down
13 changes: 9 additions & 4 deletions src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use std::{ffi::OsString, path::PathBuf};
use anyhow::Result;
use clap::Parser;

use crate::opts::{APP_MANIFEST_FILE_OPT, BUILD_UP_OPT, DEFAULT_MANIFEST_FILE};
use crate::{
directory_rels::notify_if_nondefault_rel,
opts::{APP_MANIFEST_FILE_OPT, BUILD_UP_OPT},
};

use super::up::UpCommand;

Expand All @@ -19,9 +22,8 @@ pub struct BuildCommand {
short = 'f',
long = "from",
alias = "file",
default_value = DEFAULT_MANIFEST_FILE
)]
pub app_source: PathBuf,
pub app_source: Option<PathBuf>,

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

impl BuildCommand {
pub async fn run(self) -> Result<()> {
let manifest_file = spin_common::paths::resolve_manifest_file_path(&self.app_source)?;
let (manifest_file, distance) =
spin_common::paths::find_manifest_file_path(self.app_source.as_ref())?;
notify_if_nondefault_rel(&manifest_file, distance);

spin_build::build(&manifest_file, &self.component_id).await?;

if self.up {
Expand Down
16 changes: 11 additions & 5 deletions src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use clap::Parser;
use dialoguer::{console::Emoji, Confirm, Select};
use spin_doctor::{Diagnosis, DryRunNotSupported, PatientDiagnosis};

use crate::opts::{APP_MANIFEST_FILE_OPT, DEFAULT_MANIFEST_FILE};
use crate::opts::APP_MANIFEST_FILE_OPT;

#[derive(Parser, Debug)]
#[clap(about = "Detect and fix problems with Spin applications")]
Expand All @@ -17,15 +17,21 @@ pub struct DoctorCommand {
name = APP_MANIFEST_FILE_OPT,
short = 'f',
long = "from",
alias = "file",
default_value = DEFAULT_MANIFEST_FILE
alias = "file"
)]
pub app_source: PathBuf,
pub app_source: Option<PathBuf>,
}

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

println!("{icon}The Spin Doctor is in.", icon = Emoji("📟 ", ""));
println!(
Expand Down
10 changes: 6 additions & 4 deletions src/commands/registry.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::opts::*;
use crate::{directory_rels::notify_if_nondefault_rel, opts::*};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use indicatif::{ProgressBar, ProgressStyle};
Expand Down Expand Up @@ -37,9 +37,8 @@ pub struct Push {
short = 'f',
long = "from",
alias = "file",
default_value = DEFAULT_MANIFEST_FILE
)]
pub app_source: PathBuf,
pub app_source: Option<PathBuf>,

/// Ignore server certificate errors
#[clap(
Expand Down Expand Up @@ -71,7 +70,10 @@ pub struct Push {

impl Push {
pub async fn run(self) -> Result<()> {
let app_file = spin_common::paths::resolve_manifest_file_path(&self.app_source)?;
let (app_file, distance) =
spin_common::paths::find_manifest_file_path(self.app_source.as_ref())?;
notify_if_nondefault_rel(&app_file, distance);

if self.build {
spin_build::build(&app_file, &[]).await?;
}
Expand Down
10 changes: 8 additions & 2 deletions src/commands/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use spin_oci::OciLoader;
use spin_trigger::cli::{LaunchMetadata, SPIN_LOCAL_APP_DIR, SPIN_LOCKED_URL, SPIN_WORKING_DIR};
use tempfile::TempDir;

use crate::opts::*;
use crate::{directory_rels::notify_if_nondefault_rel, opts::*};

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

Expand Down Expand Up @@ -401,7 +401,13 @@ impl UpCommand {
);
AppSource::Unresolvable(msg)
} else {
AppSource::None
match spin_common::paths::search_upwards_for_manifest() {
Some((manifest_path, distance)) => {
notify_if_nondefault_rel(&manifest_path, distance);
AppSource::File(manifest_path)
}
None => AppSource::None,
}
}
}

Expand Down
14 changes: 8 additions & 6 deletions src/commands/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ use spin_common::paths::parent_dir;
use uuid::Uuid;
use watchexec::Watchexec;

use crate::opts::{
APP_MANIFEST_FILE_OPT, DEFAULT_MANIFEST_FILE, WATCH_CLEAR_OPT, WATCH_DEBOUNCE_OPT,
WATCH_SKIP_BUILD_OPT,
use crate::{
directory_rels::notify_if_nondefault_rel,
opts::{APP_MANIFEST_FILE_OPT, WATCH_CLEAR_OPT, WATCH_DEBOUNCE_OPT, WATCH_SKIP_BUILD_OPT},
};

mod buildifier;
Expand All @@ -41,9 +41,8 @@ pub struct WatchCommand {
short = 'f',
long = "from",
alias = "file",
default_value = DEFAULT_MANIFEST_FILE
)]
pub app_source: PathBuf,
pub app_source: Option<PathBuf>,

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

let spin_bin = std::env::current_exe()?;
let manifest_file = spin_common::paths::resolve_manifest_file_path(&self.app_source)?;
let (manifest_file, distance) =
spin_common::paths::find_manifest_file_path(self.app_source.as_ref())?;
notify_if_nondefault_rel(&manifest_file, distance);

let manifest_file = manifest_file.absolutize()?.to_path_buf(); // or watchexec misses files in subdirectories
let manifest_dir = parent_dir(&manifest_file)?;

Expand Down
37 changes: 37 additions & 0 deletions src/directory_rels.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! Human-readable descriptions for directory relationships,
//! and helpers for standard display.
use std::path::Path;

fn parent_rel(distance: usize) -> String {
match distance {
0 => "".to_owned(),
1 => "parent".to_owned(),
2 => "grandparent".to_owned(),
_ => format!("{}grandparent", "great-".repeat(distance - 2)),
}
}

pub fn notify_if_nondefault_rel(manifest_file: &Path, distance: usize) {
if distance > 0 {
terminal::einfo!(
"No 'spin.toml' in current directory.",
"Using 'spin.toml' from {} directory ({})",
parent_rel(distance),
manifest_file.display(),
);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn ancestry_text_is_correct() {
assert_eq!("parent", parent_rel(1));
assert_eq!("grandparent", parent_rel(2));
assert_eq!("great-grandparent", parent_rel(3));
assert_eq!("great-great-great-grandparent", parent_rel(5)); // I hope you're happy Lann
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod build_info;
pub mod commands;
mod directory_rels;
pub(crate) mod opts;
pub mod subprocess;

Expand Down

0 comments on commit 9c3c5e0

Please sign in to comment.