use anyhow::{anyhow, bail, Context as _, Result};
use futures::{stream, StreamExt as _};
use log::info;
use std::{
collections::BTreeSet,
env,
path::{Path, PathBuf},
};
use wasmtime::{
component::{Component, Linker},
Engine, Store,
};
use wasmtime_wasi::{
bindings::Command, pipe::MemoryOutputPipe, DirPerms, FilePerms, IoView, ResourceTable, WasiCtx, WasiCtxBuilder, WasiView
};
use crate::{
config::{ConfigLinter, LinterLocation},
file_matching::matching_files,
git::FileInfo,
metadata::{read_metadata, ArgBlock},
wasi_cache,
};
pub fn get_cache_dir() -> Option<PathBuf> {
if let Ok(cache_dir) = env::var("NIT_CACHE_DIR") {
Some(cache_dir.into())
} else {
dirs::cache_dir()
.or_else(|| dirs::home_dir())
.map(|d| d.join("nit"))
}
}
pub fn get_linter_path(top_level: &PathBuf, cache_dir: &Path, linter: &ConfigLinter) -> PathBuf {
match &linter.location {
LinterLocation::Local(path) => top_level.join(path),
LinterLocation::Remote(remote) => get_url_linter_path(cache_dir, &remote.url),
}
}
pub fn get_url_linter_path(cache_dir: &Path, url: &str) -> PathBuf {
let mut hasher = blake3::Hasher::new();
hasher.update(url.as_bytes());
let hash = hasher.finalize();
let hash_str = format!("{}.wasm", hash.to_hex());
cache_dir.join(hash_str)
}
struct ComponentRunStates {
wasi_ctx: WasiCtx,
resource_table: ResourceTable,
}
impl WasiView for ComponentRunStates {
fn ctx(&mut self) -> &mut WasiCtx {
&mut self.wasi_ctx
}
}
impl IoView for ComponentRunStates {
fn table(&mut self) -> &mut ResourceTable {
&mut self.resource_table
}
}
pub async fn run_single_linter(
files: &[FileInfo],
cache_dir: &PathBuf,
top_level: &PathBuf,
linter: ConfigLinter,
) -> Result<bool> {
let linter_path = get_linter_path(top_level, cache_dir, &linter);
let metadata = read_metadata(&linter_path)?;
log::info!("Running linter: {} ({})", linter.name, metadata.repo);
let files = matching_files(
files,
if let Some(m) = &linter.override_match {
m
} else {
&metadata.default_match
},
);
let mut full_args: Vec<&str> = vec![metadata.argv0.as_str()];
if let Some(override_args) = &linter.override_args {
let all_metadata_arg_names: BTreeSet<&str> =
metadata.args.iter().map(|a| a.name.as_str()).collect();
for (arg, _) in override_args {
if !all_metadata_arg_names.contains(arg.as_str()) {
bail!(
"Override arg '{}' isn't valid for linter '{}'. Valid options are {:?}.",
arg,
linter.name,
all_metadata_arg_names
);
}
}
}
for ArgBlock { name, args } in metadata.args.iter() {
let args = linter
.override_args
.as_ref()
.and_then(|a| a.get(name))
.unwrap_or(args);
for s in args.iter() {
full_args.push(s.as_str());
}
}
info!("Loading component");
let engine =
Engine::new(wasmtime::Config::new().async_support(true)).context("creating WASM engine")?;
let component = wasi_cache::load_component_cached(&engine, &linter_path).await?;
if metadata.max_filenames == 0 {
run_linter_command(top_level, &full_args, &engine, &component).await
} else {
let all_filenames = files
.iter()
.map(|f| {
f.path
.to_str()
.ok_or_else(|| anyhow!("Couldn't convert path to UTF-8: {:?}", f.path))
})
.collect::<Result<Vec<_>>>()?;
let tasks = all_filenames
.chunks(metadata.max_filenames as usize)
.map(|chunk| {
let mut full_args = full_args.clone();
full_args.extend_from_slice(&chunk);
let component = &component;
let engine = &engine;
async move { run_linter_command(top_level, &full_args, engine, component).await }
});
let max_parallelism = if metadata.require_serial { 1 } else { 4 };
let results: Vec<_> = stream::iter(tasks)
.buffered(max_parallelism)
.collect()
.await;
for result in results.into_iter() {
if !result? {
return Ok(false);
}
}
Ok(true)
}
}
async fn run_linter_command(
top_level: &Path,
args: &[&str],
engine: &Engine,
component: &Component,
) -> Result<bool> {
let mut linker = Linker::new(&engine);
wasmtime_wasi::add_to_linker_async(&mut linker)?;
let stdout = MemoryOutputPipe::new(10 * 1024 * 1024);
let stderr = MemoryOutputPipe::new(10 * 1024 * 1024);
let wasi = WasiCtxBuilder::new()
.allow_tcp(false)
.allow_udp(false)
.allow_ip_name_lookup(false)
.preopened_dir(
top_level,
".",
DirPerms::all(),
FilePerms::all(),
)?
.stdout(stdout)
.stderr(stderr)
.args(args)
.build();
let state = ComponentRunStates {
wasi_ctx: wasi,
resource_table: ResourceTable::new(),
};
let mut store = Store::new(&engine, state);
info!("Instantiating");
let command = Command::instantiate_async(&mut store, &component, &linker).await?;
info!("Starting call");
let program_result = command.wasi_cli_run().call_run(&mut store).await?;
info!("Call finished");
Ok(program_result.is_ok())
}