2 releases
Uses new Rust 2024
| new 0.1.1 | Feb 5, 2026 |
|---|---|
| 0.1.0 | Feb 4, 2026 |
#141 in Configuration
99KB
1K
SLoC
prjenv
Environment detection and configuration management for Cargo workspaces and packages.
prjenv provides a unified interface for working with Cargo project
environments:
- ๐ Auto-detecting workspace vs standalone vs library environments
- ๐ฆ Reading package and workspace metadata from
Cargo.toml - โ๏ธ Managing runtime configuration via environment variables
- ๐ Discovering filesystem paths (project root, assets, database)
- ๐๏ธ Scaffolding new packages with templates
- ๐ง Macro-based initialization (optional
macrosfeature)
Quick Start
use prjenv::prelude::*;
fn main() {
let env = get();
println!("Running: {}", env.summary());
println!("Server: {}:{}", env.config.ip, env.config.port);
}
Installation
# Minimal installation (no optional features)
cargo add prjenv
# With specific features
cargo add prjenv --features macros
cargo add prjenv --features "macros,tracing"
# All features
cargo add prjenv --features full
Or add to Cargo.toml:
[dependencies]
prjenv = "0.1"
# With features
prjenv = { version = "0.1", features = ["macros"] }
Features
| Feature | Description | Default |
|---|---|---|
full |
Enables all features (tracing + macros) |
โ |
tracing |
Adds instrumentation for debugging | โ |
macros |
Provides setenv!() and getenv!() macros |
โ |
All features are disabled by default to minimize dependencies.
Usage
Basic Environment Detection
use prjenv::prelude::*;
fn main() {
let env = get();
match env.kind {
Kind::Workspace => {
println!("Workspace: {}", env.workspace.metadata.display_name());
println!("Packages: {}", env.workspace.package_count());
println!("Running: {}", env.package.metadata.display_name());
}
Kind::Standalone => {
println!("Standalone: {}", env.package.metadata.display_name());
}
Kind::Library => {
println!("Library mode");
}
}
}
Custom Configuration
use prjenv::prelude::*;
fn main() {
let env = Environment::workspace()
.with_pkg_name(env!("CARGO_PKG_NAME"))
.with_pkg_version(env!("CARGO_PKG_VERSION"))
.with_db("postgres://localhost/mydb")
.with_port(8080)
.with_ip("0.0.0.0");
set(env);
// Now accessible globally
let env = get();
println!("Server: {}:{}", env.config.ip, env.config.port);
}
With Macros (requires macros feature)
use prjenv::prelude::*;
fn main() {
// Initialize from CARGO_PKG_* environment variables
setenv!();
// Access via convenience macros
let name = getenv!(pkg_name);
let version = getenv!(pkg_version);
let port = getenv!(port);
println!("Starting {} v{} on port {}", name, version, port);
}
Environment Variables
prjenv reads these environment variables with sensible defaults:
| Variable | Type | Default | Description |
|---|---|---|---|
DATABASE_URL |
String | {workspace}/assets/db |
Database connection URL or file path |
IP |
String | localhost |
Server bind address |
PORT |
u16 | 3000 |
Server bind port |
RUST_LOG |
String | (empty) | Tracing filter directives |
Configuration Precedence
- Explicitly set values (via builder)
- Environment variables
- Built-in defaults
use prjenv::prelude::*;
// Override defaults programmatically
let config = Configuration::new()
.with_db("postgres://localhost/mydb")
.with_ip("0.0.0.0")
.with_port(8080)
.with_rust_log("debug");
Or via environment:
DATABASE_URL=postgres://localhost/mydb \
IP=0.0.0.0 \
PORT=8080 \
RUST_LOG=debug \
cargo run
Examples
Package Scaffolding
use prjenv::prelude::*;
let scaffold = PackageScaffold::new("my-service")
.version("1.0.0")
.description("My microservice")
.author("Development Team")
.dependency("tokio", "1.0")
.dependency("axum", "0.7")
.binary();
let package_path = scaffold.create("packages")?;
println!("Created: {}", package_path.display());
# Ok::<(), std::io::Error>(())
This creates:
packages/my-service/
โโโ Cargo.toml
โโโ src/
โโโ main.rs
Workspace Management
use prjenv::prelude::*;
let workspace = Workspace::new()
.with_name("my-workspace")
.with_version("1.0.0")
.with_package_name("api")
.with_package_name("cli")
.with_package_name("web");
println!("Workspace: {}", workspace.metadata.display_name());
println!("Packages: {}", workspace.package_count());
if let Some(api) = workspace.find_package("api") {
println!("Found: {}", api.metadata.display_name());
}
Accessing Filesystem Paths
use prjenv::prelude::*;
let env = get();
let paths = &env.paths;
println!("Project root: {}", paths.project.display());
println!("Assets dir: {}", paths.assets.display());
println!("Database dir: {}", paths.database.display());
// Use in your application
let config_path = paths.assets.join("config.toml");
let db_path = paths.database.join("app.db");
Complete Application Setup
use prjenv::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize with compile-time metadata
#[cfg(feature = "macros")]
setenv!();
#[cfg(not(feature = "macros"))]
set(Environment::new()
.with_pkg_name(env!("CARGO_PKG_NAME"))
.with_pkg_version(env!("CARGO_PKG_VERSION")));
let env = get();
eprintln!("๐ Starting: {}", env.summary());
eprintln!("๐ Root: {}", env.paths.project.display());
eprintln!("โ๏ธ Server: {}:{}", env.config.ip, env.config.port);
eprintln!("๐พ Database: {}", env.config.db);
// Your application logic here
Ok(())
}
Running the Examples
The crate includes several examples demonstrating different features:
# Basic usage (no features required)
cargo run --example basic
# With macros
cargo run --example macros --features macros
# With tracing instrumentation
RUST_LOG=trace cargo run --example tracing --features tracing
# Advanced usage (all features)
cargo run --example advanced --features full
Architecture
prjenv is organized into focused modules:
core- Environment detection and global state managementmetadata- Package and workspace metadata (name, version, description)infrastructure- Runtime configuration and filesystem pathsworkspace- Workspace domain model and managementpackage- Package domain model and scaffoldingmacros- Optional convenience macros (requiresmacrosfeature)
This separation ensures clear boundaries and makes testing easier.
Performance Characteristics
- First
get()call: 5-50ms (workspace discovery + file I/O) - Subsequent calls: <1ยตs (static
OnceLockcache) - Metadata loading: 5-15ms (TOML parsing, cached)
- Path discovery: 1-2ms (directory traversal, cached)
All expensive operations are cached in static storage for zero-cost subsequent access.
Thread Safety
All public APIs are thread-safe:
OnceLockensures one-time initialization- Multiple threads calling
get()orset()coordinate safely - First caller wins (idempotent behavior)
- No locks after initialization (zero-cost access)
use std::thread;
use prjenv::prelude::*;
// Safe to call from multiple threads
let handles: Vec<_> = (0..10)
.map(|_| thread::spawn(|| {
let env = get(); // All threads get the same cached instance
env.package.metadata.name.clone()
}))
.collect();
for handle in handles {
println!("{}", handle.join().unwrap());
}
Testing
Override environment detection for tests:
#[cfg(test)]
mod tests {
use prjenv::prelude::*;
#[test]
fn test_custom_environment() {
let env = Environment::library()
.with_name("test-package")
.with_version("0.0.0")
.with_db("sqlite::memory:");
let env = set(env);
assert_eq!(env.package.metadata.name, "test-package");
assert_eq!(env.config.db, "sqlite::memory:");
}
}
Or use environment variables:
# Override project root for testing
PROJECT_ROOT=/tmp/test cargo test
# Override configuration
DATABASE_URL=sqlite::memory: cargo test
Troubleshooting
"PORT must be a valid number" panic
The PORT environment variable must be a valid u16 (0-65535):
# โ Will panic
PORT=invalid cargo run
# โ
Correct
PORT=8080 cargo run
Workspace not detected
Ensure your workspace Cargo.toml has:
[workspace]
members = ["packages/*"]
resolver = "2"
Or override detection:
WORKSPACE_ROOT=/path/to/workspace cargo run
Database path not found
Create the assets directory:
mkdir -p assets/db
Or set explicitly:
DATABASE_URL=sqlite:///tmp/app.db cargo run
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing) - Add tests for new functionality
- Ensure tests pass (
cargo test --all-features) - Run formatting (
cargo fmt) - Run clippy (
cargo clippy --all-features -- -D warnings) - Submit a pull request
See CONTRIBUTING.md for detailed guidelines.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Acknowledgments
Built with:
toml- TOML parsingdotenvy-.envfile supporttracing- Optional instrumentation (withtracingfeature)
Dependencies
~0.4โ2.3MB
~25K SLoC