Skip to main content

what_stack/
lib.rs

1//! Detect project roots and technology stacks from generic process metadata.
2//!
3//! `what-stack` is a small standalone library. It does not depend on Docker,
4//! socket collection, async runtimes, logging, serialization, CLI parsing, or
5//! any application-specific types. Callers pass ordinary strings and paths:
6//! image names, project directories, process names, executable paths, working
7//! directories, and command-line arguments.
8//!
9//! # Detection Model
10//!
11//! The crate exposes focused single-purpose functions plus [`StackDetector`]
12//! for repeated lookups with caching:
13//!
14//! - [`detect_from_image`] parses container or artifact image names.
15//! - [`detect_from_config`] scans one project-root directory for known project
16//!   files.
17//! - [`detect_from_process`] maps known runtime, server, database, and tool
18//!   executable names; [`detect_from_process_names`] adds an executable-name
19//!   fallback.
20//! - [`find_project_root`] walks upward from one directory until it finds a
21//!   project marker.
22//! - [`resolve_project_root`] applies the project-root fallback order used by
23//!   process collectors: current working directory, executable parent, then
24//!   absolute command-line argument parents.
25//! - [`StackDetector`] combines those rules and caches filesystem results.
26//!
27//! Every detected [`StackLabel`] carries a [`StackKind`] (runtime, framework,
28//! tool, database, or service).
29//!
30//! High-level stack detection in [`StackDetector::detect_stack`] uses this
31//! priority:
32//!
33//! 1. Image label.
34//! 2. Process label, when it is final (framework, database, or service).
35//! 3. Project config label.
36//! 4. Process label (runtime or tool).
37//!
38//! Config labels are guarded: a project config is used only when the process
39//! is a known runtime or tool, or when the process is unknown but its
40//! executable path belongs to the project root. This keeps a `postgres` or
41//! `nginx` process started from a Next.js folder labeled as itself, and keeps
42//! unrelated helper shells from inheriting a project's framework label just
43//! because their working directory happens to be inside that project.
44//!
45//! Config labels are also ecosystem-aware: a known runtime or tool accepts
46//! only config labels from its own ecosystem. In a Laravel project with
47//! `vite.config.js`, `php` is `Laravel` and `node` is `Vite`; a `python`
48//! process in a Next.js folder stays `Python`.
49//!
50//! # Scope
51//!
52//! This crate only detects labels. It does not discover running processes,
53//! inspect network ports, query container engines, kill processes, read custom
54//! rule files, or format user-facing output.
55//!
56//! # Examples
57//!
58//! Direct image and process detection:
59//!
60//! ```
61//! use what_stack::{StackKind, detect_from_image, detect_from_process};
62//!
63//! let nginx = detect_from_image("ghcr.io/org/nginx:latest").expect("known image");
64//! assert_eq!(nginx, "Nginx");
65//! assert_eq!(nginx.kind(), StackKind::Service);
66//! assert_eq!(detect_from_process("node.exe").expect("known process"), "Node.js");
67//! ```
68//!
69//! Cached high-level detection:
70//!
71//! ```
72//! use what_stack::{StackDetector, StackInput};
73//!
74//! let mut detector = StackDetector::new();
75//! let label = detector.detect_stack(StackInput::new("").image("redis:7-alpine"));
76//!
77//! assert_eq!(label.expect("known image"), "Redis");
78//! ```
79
80mod config;
81mod detector;
82mod ecosystem;
83mod image;
84mod process;
85mod project;
86mod types;
87
88pub use config::detect_from_config;
89pub use detector::StackDetector;
90pub use image::detect_from_image;
91pub use process::{detect_from_process, detect_from_process_names};
92pub use project::{
93    MAX_WALK_DEPTH, find_project_root, home_dir, project_name, resolve_project_root,
94};
95pub use types::{ProjectInput, StackInput, StackKind, StackLabel};
96
97/// Compiles the README examples as doctests.
98#[doc = include_str!("../README.md")]
99#[cfg(doctest)]
100pub struct ReadmeDoctests;
101
102#[cfg(test)]
103mod tests {
104    use std::collections::HashMap;
105
106    use super::*;
107
108    fn all_builtin_labels() -> Vec<&'static StackLabel> {
109        static STANDALONE: [StackLabel; 1] = [image::DOTNET_NAMESPACE_LABEL];
110
111        process::PROCESS_MAP
112            .iter()
113            .map(|(_, label, _)| label)
114            .chain(image::EXACT_IMAGE_RULES.iter().map(|(_, label)| label))
115            .chain(image::PREFIX_IMAGE_RULES.iter().map(|(_, label)| label))
116            .chain(STANDALONE.iter())
117            .chain(config::all_labels())
118            .collect()
119    }
120
121    #[test]
122    fn every_label_text_has_exactly_one_kind_across_all_rules() {
123        let mut kinds: HashMap<&str, StackKind> = HashMap::new();
124
125        for label in all_builtin_labels() {
126            let previous = kinds.insert(label.as_str(), label.kind());
127            assert!(
128                previous.is_none_or(|kind| kind == label.kind()),
129                "label {label} has conflicting kinds {previous:?} and {:?}",
130                label.kind()
131            );
132        }
133
134        assert!(kinds.len() > 40, "expected the full rule set to be scanned");
135    }
136
137    #[test]
138    fn stack_label_text_traits_ignore_kind_but_equality_does_not() {
139        let runtime = StackLabel::from_static("Vite", StackKind::Runtime);
140        let tool = StackLabel::new(String::from("Vite"), StackKind::Tool);
141
142        assert_eq!(runtime, "Vite");
143        assert_eq!("Vite", tool);
144        assert_eq!(runtime.as_str(), tool.as_ref());
145        assert_eq!(runtime.to_string(), "Vite");
146        assert_ne!(runtime, tool);
147        assert_eq!(String::from(tool.clone()), "Vite");
148        assert_eq!(tool.into_cow(), "Vite");
149    }
150
151    #[test]
152    fn builtin_labels_borrow_static_text() {
153        for label in all_builtin_labels() {
154            assert!(
155                matches!(label.clone().into_cow(), std::borrow::Cow::Borrowed(_)),
156                "label {label} should not allocate"
157            );
158        }
159    }
160}