use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
};
use lazy_static::lazy_static;
use miette::IntoDiagnostic;
use regex::{Match, Regex, RegexBuilder};
use tokio::{fs, sync::RwLock, time};
use watchexec_filterer_globset::GlobsetFilterer;
use crate::{
command::WCommand,
constants::{dirname, extension, filename},
error::WatchError,
glob::glob,
toml::read_cargo_toml,
};
#[derive(Default)]
pub struct ProjectMap(Arc<RwLock<HashMap<String, PathBuf>>>);
impl ProjectMap {
pub async fn get_program_path<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
let program_name = match path.as_ref().extension().map(|ext| ext.to_str()) {
Some(Some(ext)) => match ext {
extension::JSON => ProgramName::from_keypair_path(path),
extension::SO => ProgramName::from_elf_path(path),
_ => None,
},
_ => None,
};
match program_name {
Some(program_name) => match self
.get_program_path_from_name(program_name.original())
.await
{
Some(program_path) => Some(program_path),
None => {
self.get_program_path_from_name(program_name.kebab_case())
.await
}
},
None => None,
}
}
pub async fn set_program_path<S, P>(&self, name: S, path: P)
where
S: Into<String>,
P: Into<PathBuf>,
{
let mut program_hm = self.0.write().await;
program_hm.insert(name.into(), path.into());
}
async fn get_program_path_from_name<S: AsRef<str>>(&self, name: S) -> Option<PathBuf> {
self.0
.read()
.await
.get(name.as_ref())
.map(|path| path.to_owned())
}
}
#[derive(Debug)]
pub struct ProgramName(String);
impl ProgramName {
pub fn new<S: Into<String>>(name: S) -> Self {
Self(name.into())
}
pub fn from_keypair_path<P: AsRef<Path>>(program_keypair_path: P) -> Option<Self> {
Self::from_path(program_keypair_path, "-keypair.json")
}
pub fn from_elf_path<P: AsRef<Path>>(program_elf_path: P) -> Option<Self> {
Self::from_path(program_elf_path, ".so")
}
fn from_path<P, S>(path: P, suffix: S) -> Option<Self>
where
P: AsRef<Path>,
S: AsRef<str>,
{
path.as_ref()
.file_name()
.and_then(|name| name.to_str())
.filter(|name| name.ends_with(suffix.as_ref()))
.map(|name| Self::new(name.trim_end_matches(suffix.as_ref())))
}
pub fn original(&self) -> &str {
&self.0
}
pub fn kebab_case(&self) -> String {
self.0.replace('_', "-")
}
}
pub async fn start_test_validator<P: Into<PathBuf>>(origin: P) -> miette::Result<()> {
let origin = origin.into();
tokio::spawn(async {
let _ = WCommand::new("solana-test-validator")
.current_dir(origin)
.output()
.await;
});
time::sleep(time::Duration::from_secs(2)).await;
Ok(())
}
pub async fn get_watch_pathset<P: AsRef<Path>>(origin: P) -> miette::Result<Vec<PathBuf>> {
let mut paths = vec![Path::new(dirname::TARGET).join(dirname::DEPLOY)];
match filter_workspace_programs(origin).await? {
Some(filtered_paths) => paths.extend(filtered_paths),
None => paths.push(PathBuf::from(dirname::SRC)),
}
Ok(paths)
}
async fn filter_workspace_programs<P: AsRef<Path>>(
origin: P,
) -> miette::Result<Option<Vec<PathBuf>>> {
let manifest = read_cargo_toml(&origin).await?;
match manifest.workspace {
Some(workspace) => {
let paths = glob(origin.as_ref(), workspace.members, workspace.exclude, true).await?;
Ok(Some(paths))
}
None => Ok(None),
}
}
pub async fn get_program_name_path_hashmap<P: AsRef<Path>>(
origin: P,
) -> miette::Result<HashMap<String, PathBuf>> {
let mut program_name_path_hm = HashMap::new();
let program_paths = filter_workspace_programs(&origin)
.await?
.unwrap_or(vec![origin.as_ref().to_path_buf()]);
for program_path in program_paths {
if let Ok(manifest) = read_cargo_toml(&program_path).await {
if let Some(package) = manifest.package {
program_name_path_hm.insert(package.name, program_path);
}
}
}
Ok(program_name_path_hm)
}
pub async fn get_program_path<P: AsRef<Path>>(modified_file_path: P) -> miette::Result<PathBuf> {
let output = WCommand::new("cargo locate-project --message-format plain")
.current_dir(modified_file_path.as_ref().parent().unwrap())
.output()
.await?;
if output.status().success() {
Ok(Path::new(output.stdout().trim_end_matches('\n'))
.parent()
.unwrap()
.to_path_buf())
} else {
Err(WatchError::CommandNotFound("cargo locate-project"))?
}
}
pub async fn get_pubkey_from_keypair_path<P: AsRef<Path>>(
keypair_path: P,
) -> miette::Result<String> {
let keypair_output = WCommand::new(format!(
"solana address -k {}",
keypair_path.as_ref().display()
))
.output()
.await?;
if !keypair_output.status().success() {
return Err(WatchError::CouldNotGetKeypair(
keypair_output.stderr().into(),
))?;
}
let program_id = keypair_output.stdout().trim_end_matches('\n');
Ok(program_id.to_owned())
}
pub async fn find_and_update_program_id<P1, P2>(
program_path: P1,
program_keypair_path: P2,
) -> miette::Result<()>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
let program_id = get_pubkey_from_keypair_path(program_keypair_path).await?;
let src_path = program_path.as_ref().join(dirname::SRC);
let lib_rs_path = src_path.join(filename::LIB_RS);
if update_rust_program_id(lib_rs_path, &program_id).await? {
return Ok(());
}
let rust_src_paths = glob(src_path, [format!("*.{}", extension::RS)], [], false).await?;
for path in rust_src_paths {
if update_rust_program_id(path, &program_id).await? {
break;
}
}
Ok(())
}
async fn update_rust_program_id<P, S>(path: P, program_id: S) -> miette::Result<bool>
where
P: AsRef<Path>,
S: AsRef<str>,
{
lazy_static! {
static ref REGEX: Regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#)
.multi_line(true)
.build()
.unwrap();
};
update_file_program_id_with(path, &program_id, |content| {
REGEX.captures(content).and_then(|captures| captures.get(3))
})
.await
}
pub async fn update_file_program_id_with<P, S, F>(
path: P,
program_id: S,
cb: F,
) -> miette::Result<bool>
where
P: AsRef<Path>,
S: AsRef<str>,
F: Fn(&str) -> Option<Match<'_>>,
{
let mut content = fs::read_to_string(&path).await.into_diagnostic()?;
if let Some(program_id_match) =
cb(&content).filter(|program_id_match| program_id_match.as_str() != program_id.as_ref())
{
content.replace_range(program_id_match.range(), program_id.as_ref());
fs::write(&path, content).await.into_diagnostic()?;
return Ok(true);
}
Ok(false)
}
pub async fn get_bpf_or_sbf() -> miette::Result<&'static str> {
const BUILD_SBF: &str = "cargo build-sbf";
const BUILD_BPF: &str = "cargo build-bpf";
let build_cmd = if WCommand::exists(BUILD_SBF).await {
BUILD_SBF
} else if WCommand::exists(BUILD_BPF).await {
BUILD_BPF
} else {
return Err(WatchError::CommandNotFound("solana"))?;
};
Ok(build_cmd)
}
pub async fn create_globset_filterer<P: AsRef<Path>>(
origin: P,
filters: &[&str],
ignores: &[&str],
extensions: &[&str],
) -> Arc<GlobsetFilterer> {
let filters = filters
.iter()
.map(|glob| (glob.to_string(), None))
.collect::<Vec<(String, Option<PathBuf>)>>();
let ignores = [
&[
"**/*/target/**/*",
"**/*/test-ledger/**/*",
"**/*/node_modules/**/*",
],
ignores,
]
.concat()
.iter()
.map(|glob| (glob.to_string(), None))
.collect::<Vec<(String, Option<PathBuf>)>>();
let ignore_files = [];
let extensions = extensions.iter().map(|ext| ext.into());
Arc::new(
GlobsetFilterer::new(origin, filters, ignores, ignore_files, extensions)
.await
.unwrap(),
)
}