blob: fb243e8505ce1b4d2f4571ecbf73939a0281c025 [file] [log] [blame]
// Copyright 2022 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
use std::{
ffi::OsStr,
fs,
path::{Path, PathBuf},
sync::Arc,
};
use crate::{executor::Executor, registry::Registry, Target};
use crate::{Error, Result};
use manifest::Manifest;
pub mod file;
pub(crate) mod manifest;
#[doc(inline)]
pub use file::File;
/// A `qg`-based project.
#[derive(Debug)]
pub struct Project {
root: PathBuf,
qg_dir: PathBuf,
name: String,
registry: Arc<Registry>,
}
impl Project {
const MANIFEST_FILE: &str = "qg.toml";
const QG_DIRECTORY: &str = "qg";
/// Checks if a project name is valid.
///
/// Names may consist of unicode letters or numbers, hyphens, and underscores.
#[must_use]
pub fn name_is_valid(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
}
/// Searches for a qg project in the current directory and its ancestors.
///
/// # Errors
/// May return one of the following errors:
/// - [`Error::File`]: Failed to access the filesystem.
/// - [`Error::ProjectNotFound`]: No project could be located.
pub fn locate() -> Result<Self> {
let cwd = std::env::current_dir()?;
let cwd = cwd.as_path();
Self::locate_from_path(cwd)
}
// Implementation details of `locate()` without looking up the
// current working directory. This allows testing of the core
// logic without dependence on the environment and makes it
// friendlier to parallel test execution.
fn locate_from_path<P: AsRef<Path>>(cwd: P) -> Result<Self> {
let mut cwd = cwd.as_ref();
loop {
if let Ok(project) = Self::load(cwd) {
return Ok(project);
}
cwd = match cwd.parent() {
Some(p) => p,
None => return Err(Error::ProjectNotFound),
}
}
}
/// Load a qg project from an specific path.
///
/// # Errors
/// May return one of the following errors:
/// - [`Error::File`]: Failed to access the filesystem.
/// - [`Error::ProjectNotFound`]: No project manifest found in `project_root`.
pub fn load<P: AsRef<Path>>(project_root: P) -> Result<Self> {
let project_root = project_root.as_ref();
let manifest_path = project_root.join(Self::MANIFEST_FILE);
if !manifest_path.exists() {
return Err(Error::ProjectNotFound);
}
let manifest_file = self::File::new(manifest_path);
let manifest = manifest_file.deserialize_toml::<Manifest>()?;
let registry = Registry::parse_manifests(&manifest_file)?;
Ok(Self {
root: project_root.to_owned(),
qg_dir: project_root.join(Self::QG_DIRECTORY),
name: manifest.project.name,
registry: Arc::new(registry),
})
}
/// Initializes a new project at the specified `root`.
///
/// # Errors
/// Returns an error if the root directory does not exist or is
/// inaccessible.
///
/// # Panics
/// Temporarily panics if a project already exists within `root` as
/// subprojects are not yet supported.
pub fn create(root: &Path, name: &str) -> Result<Self> {
let parent_project = match Self::locate() {
Ok(p) => Some(p),
Err(Error::ProjectNotFound) => None,
Err(e) => return Err(e),
};
// TODO(frolv): Initialize a new subproject in an existing project.
assert!(parent_project.is_none(), "subprojects not yet supported");
if !Self::name_is_valid(name) {
return Err(Error::InvalidName(name.into()));
}
let manifest = Manifest::new(name);
Self::relative_file(root, Path::new(Self::MANIFEST_FILE))?.serialize_toml(&manifest)?;
Self::load(root)
}
/// Returns the root directory of the project.
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
/// Returns the name of the project.
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
/// Returns a [`File`](self::File) representing some configuration file located
/// at `path` within the project directory.
///
/// # Errors
/// Returns an [`Error::InvalidPath`] if the provided path is absolute.
pub fn file(&self, path: impl AsRef<OsStr>) -> Result<self::File> {
Self::relative_file(&self.root, Path::new(path.as_ref()))
}
/// Returns a `PathBuf` pointing to the directory located at `path` within
/// the project root. Will create the directory if it does not exist.
///
/// # Errors
/// Returns an error if the directory does not exist and can not be
/// created.
fn directory(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
let dir_path = self.root.join(path.as_ref());
fs::create_dir_all(&dir_path)?;
Ok(dir_path)
}
/// Returns a `PathBuf` pointing to the output directory for a given target.
/// Will create the directory if it does not exist.
///
/// # Errors
/// Returns an error if the directory does not exist and can not be
/// created.
pub fn target_output_directory(&self, target: &Target) -> Result<PathBuf> {
self.directory(
self.qg_dir
.join("out")
.join(target.provider())
.join(target.name()),
)
}
/// Returns a `PathBuf` pointing to the output directory for a given target.
/// Will create the directory if it does not exist.
///
/// # Errors
/// Returns an error if the directory does not exist and can not be
/// created.
pub fn target_work_directory(&self, target: &Target) -> Result<PathBuf> {
self.directory(
self.qg_dir
.join("work")
.join(target.provider())
.join(target.name()),
)
}
/// Executes a single target in the build graph.
///
/// # Errors
/// Returns an error if the target or any of its dependencies failed to run.
pub async fn run_target(&self, target: &str) -> Result<()> {
// TODO(frolv): Configure the number of workers based on the system thread count.
let mut executor = Executor::new(self, 2);
executor.execute_target(target).await
}
/// Returns the target registry for this project.
#[must_use]
pub fn registry(&self) -> Arc<Registry> {
self.registry.clone()
}
fn relative_file(root: &Path, path: &Path) -> Result<self::File> {
if !path.as_os_str().is_empty() && !path.is_absolute() {
Ok(self::File::new(root.join(path)))
} else {
Err(Error::InvalidPath)
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{target::Metadata, test_util::TestProject};
use super::*;
#[test]
fn name_is_valid_valid() {
assert!(Project::name_is_valid("qg"));
assert!(Project::name_is_valid("my-project"));
assert!(Project::name_is_valid("my_other_project"));
assert!(Project::name_is_valid("мой-проект"));
}
#[test]
fn name_is_valid_invalid() {
assert!(!Project::name_is_valid(""));
assert!(!Project::name_is_valid("project!"));
assert!(!Project::name_is_valid("💩"));
}
#[test]
fn file_valid_path() {
let root = PathBuf::from("/qg");
let project = Project {
qg_dir: root.join("qg"),
root,
name: "qg2".into(),
registry: Arc::new(Registry::new()),
};
assert_eq!(
project.file("emoji.json").unwrap().path(),
Path::new("/qg/emoji.json")
);
assert_eq!(
project.file("secrets/nuclear_codes.toml").unwrap().path(),
Path::new("/qg/secrets/nuclear_codes.toml")
);
}
#[test]
fn file_invalid_path() {
let root = PathBuf::from("/qg");
let project = Project {
qg_dir: root.join("qg"),
root,
name: "qg2".into(),
registry: Arc::new(Registry::new()),
};
assert!(matches!(
project.file("/bin").unwrap_err(),
Error::InvalidPath,
));
assert!(matches!(project.file("").unwrap_err(), Error::InvalidPath));
}
#[test]
fn locate_test_project() {
// Test that `locate()` and find a manifest from a parent directory.
let project =
Project::locate_from_path("./src/test_projects/simple/subdir/subsubdir").unwrap();
assert_eq!(project.name, "test-qg-project");
}
#[test]
fn load_test_project() {
// Loads from the project root succeed.
let project = Project::load("./src/test_projects/simple").unwrap();
assert_eq!(project.name, "test-qg-project");
// Loads from a project subdirectory fail.
assert!(Project::load("./src/test_projects/simple/subdir/subsubdir").is_err());
}
#[test]
fn out_and_work_directories() {
let test_project = TestProject::new("./src/test_projects/dependency_test").unwrap();
let project = &test_project.project;
assert_eq!(project.name, "dep-test");
// Neither dep-test:a's out or work directories should exist before we
// ask for their paths.
assert!(!test_project.relative_path("qg/out/dep-test/a").exists());
assert!(!test_project.relative_path("qg/work/dep-test/a").exists());
let target = project.registry().get_target("dep-test:a").unwrap();
project.target_output_directory(&target).unwrap();
assert!(test_project.relative_path("qg/out/dep-test/a").exists());
project.target_work_directory(&target).unwrap();
assert!(test_project.relative_path("qg/work/dep-test/a").exists());
}
#[test]
fn simple_dependencies() {
let test_project = TestProject::new("./src/test_projects/dependency_test").unwrap();
let project = &test_project.project;
assert_eq!(project.name, "dep-test");
let registry = project.registry();
let a_id = registry.get_node_id("dep-test:a").unwrap();
let b_id = registry.get_node_id("dep-test:b").unwrap();
let c_id = registry.get_node_id("dep-test:c").unwrap();
let d_id = registry.get_node_id("dep-test:d").unwrap();
let e_id = registry.get_node_id("dep-test:e").unwrap();
for (name, id, duration_ticks, children) in [
("a", a_id, 1, vec![b_id, c_id]),
("b", b_id, 2, vec![]),
("c", c_id, 3, vec![d_id, e_id]),
("d", d_id, 4, vec![]),
("e", e_id, 5, vec![]),
] {
let target = registry.get_target_by_id(id).unwrap();
let Metadata::Fake(fake) = target.metadata() else {
panic!("{name} is not of the target type Fake: {target:?}.");
};
assert_eq!(
fake.duration_ticks,
duration_ticks,
"Node {name}'s durations ticks ({}) do not match expected value ({duration_ticks}).",
fake.duration_ticks);
let deps: HashSet<_> = registry.node_deps(id).collect();
let expected: HashSet<_> = children.into_iter().collect();
assert_eq!(
deps, expected,
"Node {name}'s dependencies ({deps:?}) do not match expected value ({expected:?})."
);
}
}
}