Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-cli / src / fs.rs
28 kB 893 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2020 The Gleam contributors 3 4use gleam_core::{ 5 Result, Warning, 6 build::{NullTelemetry, Target}, 7 error::{Error, FileIoAction, FileKind, OS, ShellCommandFailureReason, parse_os}, 8 io::{ 9 BeamCompilerIO, Command, CommandExecutor, Content, DirEntry, FileSystemReader, 10 FileSystemWriter, OutputFile, ReadDir, Stdio, WrappedReader, is_native_file_extension, 11 }, 12 manifest::Manifest, 13 paths::ProjectPaths, 14 warning::WarningEmitterIO, 15}; 16use gleam_language_server::{DownloadDependencies, Locker, MakeLocker}; 17use regex::Regex; 18use std::{ 19 collections::HashSet, 20 fmt::Debug, 21 fs::{File, exists}, 22 io::{self, BufRead, BufReader, Write}, 23 sync::{Arc, Mutex, OnceLock}, 24 time::SystemTime, 25}; 26 27use camino::{ReadDirUtf8, Utf8Path, Utf8PathBuf}; 28 29use crate::{beam_compiler::BeamCompilerInstance, dependencies, lsp::LspLocker}; 30 31#[cfg(test)] 32mod tests; 33 34/// Return the current directory as a UTF-8 Path 35pub fn get_current_directory() -> Result<Utf8PathBuf, Error> { 36 let curr_dir = std::env::current_dir().map_err(|e| Error::FileIo { 37 kind: FileKind::Directory, 38 action: FileIoAction::Open, 39 path: ".".into(), 40 err: Some(e.to_string()), 41 })?; 42 Utf8PathBuf::from_path_buf(curr_dir.clone()).map_err(|_| Error::NonUtf8Path { path: curr_dir }) 43} 44 45// Return the first directory with a gleam.toml as a UTF-8 Path 46pub fn get_project_root(original_path: Utf8PathBuf) -> Result<Utf8PathBuf, Error> { 47 let mut path = original_path.clone(); 48 loop { 49 if path.join("gleam.toml").is_file() { 50 return Ok(path); 51 } 52 53 path = match path.parent() { 54 Some(path) => path.into(), 55 None => { 56 return Err(Error::UnableToFindProjectRoot { 57 path: original_path.to_string(), 58 }); 59 } 60 } 61 } 62} 63 64pub fn get_os() -> OS { 65 parse_os(std::env::consts::OS, get_distro_str().as_str()) 66} 67 68// try to extract the distro id from /etc/os-release 69pub fn extract_distro_id(os_release: String) -> String { 70 let distro = os_release.lines().find(|line| line.starts_with("ID=")); 71 if let Some(distro) = distro { 72 let id = distro.split('=').nth(1).unwrap_or("").replace("\"", ""); 73 return id; 74 } 75 "".to_string() 76} 77 78pub fn get_distro_str() -> String { 79 let path = Utf8Path::new("/etc/os-release"); 80 if std::env::consts::OS != "linux" || !path.exists() { 81 return "other".to_string(); 82 } 83 let os_release = read(path); 84 match os_release { 85 Ok(os_release) => extract_distro_id(os_release), 86 Err(_) => "other".to_string(), 87 } 88} 89 90/// A `FileWriter` implementation that writes to the file system. 91#[derive(Debug, Clone, Default)] 92pub struct ProjectIO { 93 beam_compiler: Arc<Mutex<Option<BeamCompilerInstance>>>, 94} 95 96impl ProjectIO { 97 pub fn new() -> Self { 98 Self { 99 beam_compiler: Arc::new(Mutex::new(None)), 100 } 101 } 102 103 pub fn boxed() -> Box<Self> { 104 Box::new(Self::new()) 105 } 106 107 pub(crate) fn initialise_beam_compiler(&self) -> Result<(), Error> { 108 let mut guard = self 109 .beam_compiler 110 .lock() 111 .expect("could not lock beam_compiler"); 112 if guard.is_none() { 113 *guard = Some(BeamCompilerInstance::new(self)?); 114 } 115 Ok(()) 116 } 117} 118 119impl FileSystemReader for ProjectIO { 120 fn read(&self, path: &Utf8Path) -> Result<String, Error> { 121 read(path) 122 } 123 124 fn read_bytes(&self, path: &Utf8Path) -> Result<Vec<u8>, Error> { 125 read_bytes(path) 126 } 127 128 fn is_file(&self, path: &Utf8Path) -> bool { 129 path.is_file() 130 } 131 132 fn is_directory(&self, path: &Utf8Path) -> bool { 133 path.is_dir() 134 } 135 136 fn reader(&self, path: &Utf8Path) -> Result<WrappedReader, Error> { 137 reader(path) 138 } 139 140 fn read_dir(&self, path: &Utf8Path) -> Result<ReadDir> { 141 read_dir(path).map(|entries| { 142 entries 143 .map(|result| result.map(|entry| DirEntry::from_path(entry.path()))) 144 .collect() 145 }) 146 } 147 148 fn modification_time(&self, path: &Utf8Path) -> Result<SystemTime, Error> { 149 modification_time(path) 150 } 151 152 fn canonicalise(&self, path: &Utf8Path) -> Result<Utf8PathBuf, Error> { 153 canonicalise(path) 154 } 155 156 fn is_same_file(&self, left: &Utf8Path, right: &Utf8Path) -> Result<bool, Error> { 157 is_same_file(left, right) 158 } 159} 160 161fn is_same_file(left: &Utf8Path, right: &Utf8Path) -> Result<bool, Error> { 162 same_file::is_same_file(left, right).map_err(|e| Error::FileIo { 163 action: FileIoAction::ReadMetadata, 164 kind: FileKind::File, 165 path: left.to_path_buf(), 166 err: Some(e.to_string()), 167 }) 168} 169 170pub fn modification_time(path: &Utf8Path) -> std::result::Result<SystemTime, Error> { 171 path.metadata() 172 .map(|m| m.modified().unwrap_or_else(|_| SystemTime::now())) 173 .map_err(|e| Error::FileIo { 174 action: FileIoAction::ReadMetadata, 175 kind: FileKind::File, 176 path: path.to_path_buf(), 177 err: Some(e.to_string()), 178 }) 179} 180 181impl FileSystemWriter for ProjectIO { 182 fn delete_directory(&self, path: &Utf8Path) -> Result<()> { 183 delete_directory(path) 184 } 185 186 fn copy(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> { 187 copy(from, to) 188 } 189 190 fn copy_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> { 191 copy_dir(from, to) 192 } 193 194 fn mkdir(&self, path: &Utf8Path) -> Result<(), Error> { 195 mkdir(path) 196 } 197 198 fn hardlink(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error> { 199 hardlink(from, to) 200 } 201 202 fn symlink_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error> { 203 symlink_dir(from, to) 204 } 205 206 fn delete_file(&self, path: &Utf8Path) -> Result<()> { 207 delete_file(path) 208 } 209 210 fn write(&self, path: &Utf8Path, content: &str) -> Result<(), Error> { 211 write(path, content) 212 } 213 214 fn write_bytes(&self, path: &Utf8Path, content: &[u8]) -> Result<(), Error> { 215 write_bytes(path, content) 216 } 217 218 fn exists(&self, path: &Utf8Path) -> bool { 219 path.exists() 220 } 221} 222 223impl CommandExecutor for ProjectIO { 224 fn exec(&self, command: Command) -> Result<i32, Error> { 225 let Command { 226 program, 227 args, 228 env, 229 cwd, 230 stdio, 231 } = command; 232 tracing::debug!(program=program, args=?args.join(" "), env=?env, cwd=?cwd, "command_exec"); 233 let result = std::process::Command::new(&program) 234 .args(args) 235 .stdin(stdio.get_process_stdio()) 236 .stdout(stdio.get_process_stdio()) 237 .envs(env.iter().map(|pair| (&pair.0, &pair.1))) 238 .current_dir(cwd.unwrap_or_else(|| Utf8Path::new("./").to_path_buf())) 239 .status(); 240 241 match result { 242 Ok(status) => Ok(status.code().unwrap_or_default()), 243 244 Err(error) => Err(match error.kind() { 245 io::ErrorKind::NotFound => Error::ShellProgramNotFound { 246 program, 247 os: get_os(), 248 }, 249 250 other => Error::ShellCommand { 251 program, 252 reason: ShellCommandFailureReason::IoError(other), 253 }, 254 }), 255 } 256 } 257} 258 259impl BeamCompilerIO for ProjectIO { 260 fn compile_beam( 261 &self, 262 out: &Utf8Path, 263 lib: &Utf8Path, 264 modules: &HashSet<Utf8PathBuf>, 265 stdio: Stdio, 266 ) -> Result<Vec<String>, Error> { 267 let mut guard = self 268 .beam_compiler 269 .lock() 270 .expect("could not lock beam_compiler"); 271 272 match &mut *guard { 273 Some(compiler) => compiler.compile(out, lib, modules, stdio), 274 None => { 275 let mut compiler = BeamCompilerInstance::new(self)?; 276 let result = compiler.compile(out, lib, modules, stdio); 277 *guard = Some(compiler); 278 result 279 } 280 } 281 } 282} 283 284impl MakeLocker for ProjectIO { 285 fn make_locker(&self, paths: &ProjectPaths, target: Target) -> Result<Box<dyn Locker>> { 286 let locker = LspLocker::new(paths, target)?; 287 Ok(Box::new(locker)) 288 } 289} 290 291impl DownloadDependencies for ProjectIO { 292 fn download_dependencies(&self, paths: &ProjectPaths) -> Result<Manifest> { 293 dependencies::resolve_and_download( 294 paths, 295 NullTelemetry, 296 None, 297 Vec::new(), 298 dependencies::DependencyManagerConfig { 299 use_manifest: dependencies::UseManifest::Yes, 300 check_major_versions: dependencies::CheckMajorVersions::No, 301 }, 302 ) 303 } 304} 305 306pub fn delete_directory(dir: &Utf8Path) -> Result<(), Error> { 307 tracing::debug!(path=?dir, "deleting_directory"); 308 if dir.exists() { 309 std::fs::remove_dir_all(dir).map_err(|e| Error::FileIo { 310 action: FileIoAction::Delete, 311 kind: FileKind::Directory, 312 path: dir.to_path_buf(), 313 err: Some(e.to_string()), 314 })?; 315 } else { 316 tracing::debug!(path=?dir, "directory_did_not_exist_for_deletion"); 317 } 318 Ok(()) 319} 320 321pub fn delete_file(file: &Utf8Path) -> Result<(), Error> { 322 tracing::debug!("Deleting file {:?}", file); 323 if file.exists() { 324 std::fs::remove_file(file).map_err(|e| Error::FileIo { 325 action: FileIoAction::Delete, 326 kind: FileKind::File, 327 path: file.to_path_buf(), 328 err: Some(e.to_string()), 329 })?; 330 } else { 331 tracing::debug!("Did not exist for deletion: {:?}", file); 332 } 333 Ok(()) 334} 335 336pub fn write_outputs_under(outputs: &[OutputFile], base: &Utf8Path) -> Result<(), Error> { 337 for file in outputs { 338 let path = base.join(&file.path); 339 match &file.content { 340 Content::Binary(buffer) => write_bytes(&path, buffer), 341 Content::Text(buffer) => write(&path, buffer), 342 }?; 343 } 344 Ok(()) 345} 346 347pub fn write_output(file: &OutputFile) -> Result<(), Error> { 348 let OutputFile { path, content } = file; 349 match content { 350 Content::Binary(buffer) => write_bytes(path, buffer), 351 Content::Text(buffer) => write(path, buffer), 352 } 353} 354 355pub fn write(path: &Utf8Path, text: &str) -> Result<(), Error> { 356 write_bytes(path, text.as_bytes()) 357} 358 359#[cfg(target_family = "unix")] 360pub fn make_executable(path: impl AsRef<Utf8Path>) -> Result<(), Error> { 361 use std::os::unix::fs::PermissionsExt; 362 tracing::debug!(path = ?path.as_ref(), "setting_permissions"); 363 364 std::fs::set_permissions(path.as_ref(), std::fs::Permissions::from_mode(0o755)).map_err( 365 |e| Error::FileIo { 366 action: FileIoAction::UpdatePermissions, 367 kind: FileKind::File, 368 path: path.as_ref().to_path_buf(), 369 err: Some(e.to_string()), 370 }, 371 )?; 372 Ok(()) 373} 374 375#[cfg(not(target_family = "unix"))] 376pub fn make_executable(_path: impl AsRef<Utf8Path>) -> Result<(), Error> { 377 Ok(()) 378} 379 380pub fn write_bytes(path: &Utf8Path, bytes: &[u8]) -> Result<(), Error> { 381 tracing::debug!(path=?path, "writing_file"); 382 383 let dir_path = path.parent().ok_or_else(|| Error::FileIo { 384 action: FileIoAction::FindParent, 385 kind: FileKind::Directory, 386 path: path.to_path_buf(), 387 err: None, 388 })?; 389 390 std::fs::create_dir_all(dir_path).map_err(|e| Error::FileIo { 391 action: FileIoAction::Create, 392 kind: FileKind::Directory, 393 path: dir_path.to_path_buf(), 394 err: Some(e.to_string()), 395 })?; 396 397 let mut f = File::create(path).map_err(|e| Error::FileIo { 398 action: FileIoAction::Create, 399 kind: FileKind::File, 400 path: path.to_path_buf(), 401 err: Some(e.to_string()), 402 })?; 403 404 f.write_all(bytes).map_err(|e| Error::FileIo { 405 action: FileIoAction::WriteTo, 406 kind: FileKind::File, 407 path: path.to_path_buf(), 408 err: Some(e.to_string()), 409 })?; 410 Ok(()) 411} 412 413pub fn write_to_open_file( 414 file: &mut File, 415 path: &Utf8PathBuf, 416 data: impl AsRef<[u8]>, 417) -> Result<()> { 418 file.write_all(data.as_ref()).map_err(|e| Error::FileIo { 419 action: FileIoAction::WriteTo, 420 kind: FileKind::File, 421 path: path.clone(), 422 err: Some(e.to_string()), 423 }) 424} 425 426static IS_GLEAM_PATH_PATTERN: OnceLock<Regex> = OnceLock::new(); 427 428fn is_gleam_path(path: &Utf8Path, dir: impl AsRef<Utf8Path>) -> bool { 429 IS_GLEAM_PATH_PATTERN 430 .get_or_init(|| { 431 Regex::new(&format!( 432 "^({module}{slash})*{module}\\.gleam$", 433 module = "[a-z][_a-z0-9]*", 434 slash = "(/|\\\\)", 435 )) 436 .expect("is_gleam_path() RE regex") 437 }) 438 .is_match( 439 path.strip_prefix(dir.as_ref()) 440 .expect("is_gleam_path(): strip_prefix") 441 .as_str(), 442 ) 443} 444 445fn is_gleam_build_dir(e: &ignore::DirEntry) -> bool { 446 if !e.path().is_dir() || !e.path().ends_with("build") { 447 return false; 448 } 449 450 let Some(parent_path) = e.path().parent() else { 451 return false; 452 }; 453 454 parent_path.join("gleam.toml").exists() 455} 456 457/// Walks through all Gleam module files in the directory, even if ignored, 458/// except for those in the `build/` directory. Excludes any Gleam files within 459/// invalid module paths, for example if they or a folder they're in contain a 460/// dot or a hyphen within their names. 461pub fn gleam_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ { 462 ignore::WalkBuilder::new(dir) 463 .follow_links(true) 464 .standard_filters(false) 465 .filter_entry(|entry| !is_gleam_build_dir(entry)) 466 .build() 467 .filter_map(Result::ok) 468 .filter(|entry| { 469 entry 470 .file_type() 471 .map(|type_| type_.is_file()) 472 .unwrap_or(false) 473 }) 474 .map(ignore::DirEntry::into_path) 475 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path")) 476 .filter(move |d| is_gleam_path(d, dir)) 477} 478 479/// Walks through all native files in the directory, such as `.mjs` and `.erl`, 480/// even if ignored. 481pub fn native_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ { 482 ignore::WalkBuilder::new(dir) 483 .follow_links(true) 484 .standard_filters(false) 485 .filter_entry(|entry| !is_gleam_build_dir(entry)) 486 .build() 487 .filter_map(Result::ok) 488 .filter(|entry| { 489 entry 490 .file_type() 491 .map(|type_| type_.is_file()) 492 .unwrap_or(false) 493 }) 494 .map(ignore::DirEntry::into_path) 495 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path")) 496 .filter(|path| { 497 let extension = path.extension().unwrap_or_default(); 498 is_native_file_extension(extension) 499 }) 500} 501 502/// Walks through all files in the directory, even if ignored. 503pub fn priv_directory_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ { 504 ignore::WalkBuilder::new(dir) 505 .follow_links(true) 506 .standard_filters(false) 507 .build() 508 .filter_map(Result::ok) 509 .filter(|entry| { 510 entry 511 .file_type() 512 .map(|type_| type_.is_file()) 513 .unwrap_or(false) 514 }) 515 .map(ignore::DirEntry::into_path) 516 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path")) 517} 518 519/// Walks through all `.erl` and `.hrl` files in the directory, even if ignored. 520pub fn erlang_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ { 521 ignore::WalkBuilder::new(dir) 522 .follow_links(true) 523 .standard_filters(false) 524 .build() 525 .filter_map(Result::ok) 526 .filter(|entry| { 527 entry 528 .file_type() 529 .map(|type_| type_.is_file()) 530 .unwrap_or(false) 531 }) 532 .map(ignore::DirEntry::into_path) 533 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path")) 534 .filter(|path| { 535 let extension = path.extension().unwrap_or_default(); 536 extension == "erl" || extension == "hrl" 537 }) 538} 539 540pub fn create_tar_archive(outputs: Vec<OutputFile>) -> Result<Vec<u8>, Error> { 541 tracing::debug!("creating_tar_archive"); 542 543 let encoder = flate2::write::GzEncoder::new(vec![], flate2::Compression::default()); 544 let mut builder = tar::Builder::new(encoder); 545 546 for file in outputs { 547 let mut header = tar::Header::new_gnu(); 548 header.set_path(&file.path).map_err(|e| Error::AddTar { 549 path: file.path.clone(), 550 err: e.to_string(), 551 })?; 552 header.set_size(file.content.as_bytes().len() as u64); 553 header.set_cksum(); 554 builder 555 .append(&header, file.content.as_bytes()) 556 .map_err(|e| Error::AddTar { 557 path: file.path.clone(), 558 err: e.to_string(), 559 })?; 560 } 561 562 builder 563 .into_inner() 564 .map_err(|e| Error::TarFinish(e.to_string()))? 565 .finish() 566 .map_err(|e| Error::Gzip(e.to_string())) 567} 568 569pub fn mkdir(path: impl AsRef<Utf8Path> + Debug) -> Result<(), Error> { 570 if path.as_ref().exists() { 571 return Ok(()); 572 } 573 574 tracing::debug!(path=?path, "creating_directory"); 575 576 std::fs::create_dir_all(path.as_ref()).map_err(|err| Error::FileIo { 577 kind: FileKind::Directory, 578 path: Utf8PathBuf::from(path.as_ref()), 579 action: FileIoAction::Create, 580 err: Some(err.to_string()), 581 }) 582} 583 584pub fn read_dir(path: impl AsRef<Utf8Path> + Debug) -> Result<ReadDirUtf8, Error> { 585 tracing::debug!(path=?path,"reading_directory"); 586 587 Utf8Path::read_dir_utf8(path.as_ref()).map_err(|e| Error::FileIo { 588 action: FileIoAction::Read, 589 kind: FileKind::Directory, 590 path: Utf8PathBuf::from(path.as_ref()), 591 err: Some(e.to_string()), 592 }) 593} 594 595pub fn module_caches_paths( 596 path: impl AsRef<Utf8Path> + Debug, 597) -> Result<impl Iterator<Item = Utf8PathBuf>, Error> { 598 Ok(read_dir(path)? 599 .filter_map(Result::ok) 600 .map(|f| f.into_path()) 601 .filter(|p| p.extension() == Some("cache"))) 602} 603 604pub fn read(path: impl AsRef<Utf8Path>) -> Result<String, Error> { 605 let path = path.as_ref(); 606 tracing::debug!(path=?path,"reading_file"); 607 608 std::fs::read_to_string(path).map_err(|err| Error::FileIo { 609 action: FileIoAction::Read, 610 kind: FileKind::File, 611 path: Utf8PathBuf::from(path), 612 err: Some(err.to_string()), 613 }) 614} 615 616pub fn open_file(path: impl AsRef<Utf8Path>) -> Result<File, Error> { 617 let path = path.as_ref(); 618 tracing::debug!(path=?path,"opening_file"); 619 620 File::create(path).map_err(|err| Error::FileIo { 621 action: FileIoAction::Open, 622 kind: FileKind::File, 623 path: Utf8PathBuf::from(path), 624 err: Some(err.to_string()), 625 }) 626} 627 628pub fn read_bytes(path: impl AsRef<Utf8Path>) -> Result<Vec<u8>, Error> { 629 let path = path.as_ref(); 630 tracing::debug!(path=?path,"reading_file"); 631 632 std::fs::read(path).map_err(|err| Error::FileIo { 633 action: FileIoAction::Read, 634 kind: FileKind::File, 635 path: Utf8PathBuf::from(path), 636 err: Some(err.to_string()), 637 }) 638} 639 640pub fn reader(path: impl AsRef<Utf8Path>) -> Result<WrappedReader, Error> { 641 let path = path.as_ref(); 642 tracing::debug!(path=?path,"opening_file_reader"); 643 644 let reader = File::open(path).map_err(|err| Error::FileIo { 645 action: FileIoAction::Open, 646 kind: FileKind::File, 647 path: Utf8PathBuf::from(path), 648 err: Some(err.to_string()), 649 })?; 650 651 Ok(WrappedReader::new(path, Box::new(reader))) 652} 653 654pub fn buffered_reader<P: AsRef<Utf8Path>>(path: P) -> Result<impl BufRead, Error> { 655 let path = path.as_ref(); 656 tracing::debug!(path=?path,"opening_file_buffered_reader"); 657 let reader = File::open(path).map_err(|err| Error::FileIo { 658 action: FileIoAction::Open, 659 kind: FileKind::File, 660 path: Utf8PathBuf::from(path), 661 err: Some(err.to_string()), 662 })?; 663 Ok(BufReader::new(reader)) 664} 665 666pub fn copy(path: impl AsRef<Utf8Path>, to: impl AsRef<Utf8Path>) -> Result<(), Error> { 667 let path = path.as_ref(); 668 let to = to.as_ref(); 669 tracing::debug!(from=?path, to=?to, "copying_file"); 670 671 std::fs::copy(path, to) 672 .map_err(|err| Error::FileIo { 673 action: FileIoAction::Copy(Some(to.to_path_buf())), 674 kind: FileKind::File, 675 path: Utf8PathBuf::from(path), 676 err: Some(err.to_string()), 677 }) 678 .map(|_| ()) 679} 680 681pub fn copy_dir(path: impl AsRef<Utf8Path>, to: impl AsRef<Utf8Path>) -> Result<(), Error> { 682 let path = path.as_ref(); 683 let to = to.as_ref(); 684 tracing::debug!(from=?path, to=?to, "copying_directory"); 685 686 fs_extra::dir::copy( 687 path, 688 to, 689 &fs_extra::dir::CopyOptions::new() 690 .copy_inside(false) 691 .content_only(true), 692 ) 693 .map_err(|err| Error::FileIo { 694 action: FileIoAction::Copy(Some(to.to_path_buf())), 695 kind: FileKind::Directory, 696 path: Utf8PathBuf::from(path), 697 err: Some(err.to_string()), 698 }) 699 .map(|_| ()) 700} 701 702pub fn symlink_dir(src: impl AsRef<Utf8Path>, dest: impl AsRef<Utf8Path>) -> Result<(), Error> { 703 let src = src.as_ref(); 704 let dest = dest.as_ref(); 705 tracing::debug!(src=?src, dest=?dest, "symlinking"); 706 let src = canonicalise(src)?; 707 708 #[cfg(target_family = "windows")] 709 let result = std::os::windows::fs::symlink_dir(&src, dest); 710 #[cfg(not(target_family = "windows"))] 711 let result = std::os::unix::fs::symlink(&src, dest); 712 713 result.map_err(|err| Error::FileIo { 714 action: FileIoAction::Link(dest.to_path_buf()), 715 kind: FileKind::File, 716 path: src, 717 err: Some(err.to_string()), 718 })?; 719 Ok(()) 720} 721 722pub fn hardlink(from: impl AsRef<Utf8Path>, to: impl AsRef<Utf8Path>) -> Result<(), Error> { 723 let from = from.as_ref(); 724 let to = to.as_ref(); 725 tracing::debug!(from=?from, to=?to, "hardlinking"); 726 std::fs::hard_link(from, to) 727 .map_err(|err| Error::FileIo { 728 action: FileIoAction::Link(to.to_path_buf()), 729 kind: FileKind::File, 730 path: Utf8PathBuf::from(from), 731 err: Some(err.to_string()), 732 }) 733 .map(|_| ()) 734} 735 736/// Check if the given path is inside a git work tree. 737/// This is done by running `git rev-parse --is-inside-work-tree --quiet` in the 738/// given path. If git is not installed then we assume we're not in a git work 739/// tree. 740/// 741fn is_inside_git_work_tree(path: &Utf8Path) -> Result<bool, Error> { 742 tracing::debug!(path=?path, "checking_for_git_repo"); 743 744 let args: Vec<&str> = vec!["rev-parse", "--is-inside-work-tree", "--quiet"]; 745 746 // Ignore all output, rely on the exit code instead. 747 // git will display a fatal error on stderr if rev-parse isn't run inside of a git work tree, 748 // so send stderr to /dev/null 749 let result = std::process::Command::new("git") 750 .args(args) 751 .stdin(std::process::Stdio::null()) 752 .stdout(std::process::Stdio::null()) 753 .stderr(std::process::Stdio::null()) 754 .current_dir(path) 755 .status(); 756 757 match result { 758 Ok(status) => Ok(status.success()), 759 Err(error) => match error.kind() { 760 io::ErrorKind::NotFound => Ok(false), 761 762 other => Err(Error::ShellCommand { 763 program: "git".into(), 764 reason: ShellCommandFailureReason::IoError(other), 765 }), 766 }, 767 } 768} 769 770pub(crate) fn is_git_work_tree_root(path: &Utf8Path) -> bool { 771 tracing::debug!(path=?path, "checking_for_git_repo_root"); 772 exists(path.join(".git")).unwrap_or(false) 773} 774 775/// Run `git init` in the given path. 776/// If git is not installed then we do nothing. 777pub fn git_init(path: &Utf8Path) -> Result<(), Error> { 778 tracing::debug!(path=?path, "initializing git"); 779 780 if is_inside_git_work_tree(path)? { 781 tracing::debug!(path=?path, "git_repo_already_exists"); 782 return Ok(()); 783 } 784 785 let args = vec!["init".into(), "--quiet".into(), path.to_string()]; 786 787 let command = Command { 788 program: "git".to_string(), 789 args, 790 env: vec![], 791 cwd: None, 792 stdio: Stdio::Inherit, 793 }; 794 match ProjectIO::new().exec(command) { 795 Ok(_) => Ok(()), 796 Err(err) => match err { 797 Error::ShellProgramNotFound { .. } => Ok(()), 798 _ => Err(Error::GitInitialization { 799 error: err.to_string(), 800 }), 801 }, 802 } 803} 804 805pub fn canonicalise(path: &Utf8Path) -> Result<Utf8PathBuf, Error> { 806 std::fs::canonicalize(path) 807 .map_err(|err| Error::FileIo { 808 action: FileIoAction::Canonicalise, 809 kind: FileKind::File, 810 path: Utf8PathBuf::from(path), 811 err: Some(err.to_string()), 812 }) 813 .map(|pb| Utf8PathBuf::from_path_buf(pb).expect("Non Utf8 Path")) 814} 815 816#[derive(Debug, Clone, Copy)] 817pub struct ConsoleWarningEmitter; 818 819impl WarningEmitterIO for ConsoleWarningEmitter { 820 fn emit_warning(&self, warning: Warning) { 821 let buffer_writer = crate::cli::stderr_buffer_writer(); 822 let mut buffer = buffer_writer.buffer(); 823 warning.pretty(&mut buffer); 824 buffer_writer 825 .print(&buffer) 826 .expect("Writing warning to stderr"); 827 } 828} 829 830/// Returns root of Git repository in base path if it is initialised. 831pub fn get_git_repository_root(mut path: Utf8PathBuf) -> Option<Utf8PathBuf> { 832 loop { 833 if path.join(".git").is_dir() { 834 return Some(path); 835 } 836 837 path = match path.parent() { 838 Some(path) => path.into(), 839 None => return None, 840 } 841 } 842} 843 844#[derive(Debug)] 845pub struct ZipArchive<W: Write + io::Seek> { 846 zip: zip::ZipWriter<W>, 847} 848 849impl<W: Write + io::Seek> ZipArchive<W> { 850 pub fn new(writer: W) -> Self { 851 Self { 852 zip: zip::ZipWriter::new(writer), 853 } 854 } 855 856 pub fn finish(self) -> Result<W> { 857 self.zip 858 .finish() 859 .map_err(|e| Error::ZipFinish(e.to_string())) 860 } 861 862 pub fn add_file_from_disc( 863 &mut self, 864 disc_path: impl AsRef<Utf8Path>, 865 zip_path: impl Into<String>, 866 ) -> Result<()> { 867 let disc_path = disc_path.as_ref(); 868 let zip_path = zip_path.into(); 869 self.zip 870 .start_file(zip_path.clone(), self.options()) 871 .map_err(|e| Error::ZipAdd { 872 path: zip_path, 873 error: e.to_string(), 874 })?; 875 let mut file = File::open(disc_path).map_err(|e| Error::FileIo { 876 kind: FileKind::File, 877 action: FileIoAction::Open, 878 path: disc_path.to_path_buf(), 879 err: Some(e.to_string()), 880 })?; 881 let _: u64 = io::copy(&mut file, &mut self.zip).map_err(|e| Error::FileIo { 882 kind: FileKind::File, 883 action: FileIoAction::Copy(None), 884 path: disc_path.to_path_buf(), 885 err: Some(e.to_string()), 886 })?; 887 Ok(()) 888 } 889 890 fn options(&self) -> zip::write::FileOptions<'static, ()> { 891 zip::write::SimpleFileOptions::default() 892 } 893}