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
27 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 std::{ 18 collections::HashSet, 19 fmt::Debug, 20 fs::{File, exists}, 21 io::{self, BufRead, BufReader, Write}, 22 sync::{Arc, Mutex, OnceLock}, 23 time::SystemTime, 24}; 25 26use camino::{ReadDirUtf8, Utf8Path, Utf8PathBuf}; 27 28use crate::{beam_compiler::BeamCompilerInstance, dependencies, lsp::LspLocker}; 29 30#[cfg(test)] 31mod tests; 32 33/// Return the current directory as a UTF-8 Path 34pub fn get_current_directory() -> Result<Utf8PathBuf, Error> { 35 let curr_dir = std::env::current_dir().map_err(|e| Error::FileIo { 36 kind: FileKind::Directory, 37 action: FileIoAction::Open, 38 path: ".".into(), 39 err: Some(e.to_string()), 40 })?; 41 Utf8PathBuf::from_path_buf(curr_dir.clone()).map_err(|_| Error::NonUtf8Path { path: curr_dir }) 42} 43 44// Return the first directory with a gleam.toml as a UTF-8 Path 45pub fn get_project_root(original_path: Utf8PathBuf) -> Result<Utf8PathBuf, Error> { 46 let mut path = original_path.clone(); 47 loop { 48 if path.join("gleam.toml").is_file() { 49 return Ok(path); 50 } 51 52 path = match path.parent() { 53 Some(path) => path.into(), 54 None => { 55 return Err(Error::UnableToFindProjectRoot { 56 path: original_path.to_string(), 57 }); 58 } 59 } 60 } 61} 62 63pub fn get_os() -> OS { 64 parse_os(std::env::consts::OS, get_distro_str().as_str()) 65} 66 67// try to extract the distro id from /etc/os-release 68pub fn extract_distro_id(os_release: String) -> String { 69 let distro = os_release.lines().find(|line| line.starts_with("ID=")); 70 if let Some(distro) = distro { 71 let id = distro.split('=').nth(1).unwrap_or("").replace("\"", ""); 72 return id; 73 } 74 "".to_string() 75} 76 77pub fn get_distro_str() -> String { 78 let path = Utf8Path::new("/etc/os-release"); 79 if std::env::consts::OS != "linux" || !path.exists() { 80 return "other".to_string(); 81 } 82 let os_release = read(path); 83 match os_release { 84 Ok(os_release) => extract_distro_id(os_release), 85 Err(_) => "other".to_string(), 86 } 87} 88 89/// A `FileWriter` implementation that writes to the file system. 90#[derive(Debug, Clone, Default)] 91pub struct ProjectIO { 92 beam_compiler: Arc<Mutex<Option<BeamCompilerInstance>>>, 93} 94 95impl ProjectIO { 96 pub fn new() -> Self { 97 Self { 98 beam_compiler: Default::default(), 99 } 100 } 101 102 pub fn boxed() -> Box<Self> { 103 Box::new(Self::new()) 104 } 105 106 pub(crate) fn initialise_beam_compiler(&self) -> Result<(), Error> { 107 let mut guard = self 108 .beam_compiler 109 .lock() 110 .expect("could not lock beam_compiler"); 111 *guard = Some(BeamCompilerInstance::new(self)?); 112 Ok(()) 113 } 114} 115 116impl FileSystemReader for ProjectIO { 117 fn read(&self, path: &Utf8Path) -> Result<String, Error> { 118 read(path) 119 } 120 121 fn read_bytes(&self, path: &Utf8Path) -> Result<Vec<u8>, Error> { 122 read_bytes(path) 123 } 124 125 fn is_file(&self, path: &Utf8Path) -> bool { 126 path.is_file() 127 } 128 129 fn is_directory(&self, path: &Utf8Path) -> bool { 130 path.is_dir() 131 } 132 133 fn reader(&self, path: &Utf8Path) -> Result<WrappedReader, Error> { 134 reader(path) 135 } 136 137 fn read_dir(&self, path: &Utf8Path) -> Result<ReadDir> { 138 read_dir(path).map(|entries| { 139 entries 140 .map(|result| result.map(|entry| DirEntry::from_path(entry.path()))) 141 .collect() 142 }) 143 } 144 145 fn modification_time(&self, path: &Utf8Path) -> Result<SystemTime, Error> { 146 modification_time(path) 147 } 148 149 fn canonicalise(&self, path: &Utf8Path) -> Result<Utf8PathBuf, Error> { 150 canonicalise(path) 151 } 152 153 fn is_same_file(&self, left: &Utf8Path, right: &Utf8Path) -> Result<bool, Error> { 154 is_same_file(left, right) 155 } 156} 157 158fn is_same_file(left: &Utf8Path, right: &Utf8Path) -> Result<bool, Error> { 159 same_file::is_same_file(left, right).map_err(|e| Error::FileIo { 160 action: FileIoAction::ReadMetadata, 161 kind: FileKind::File, 162 path: left.to_path_buf(), 163 err: Some(e.to_string()), 164 }) 165} 166 167pub fn modification_time(path: &Utf8Path) -> std::result::Result<SystemTime, Error> { 168 path.metadata() 169 .map(|m| m.modified().unwrap_or_else(|_| SystemTime::now())) 170 .map_err(|e| Error::FileIo { 171 action: FileIoAction::ReadMetadata, 172 kind: FileKind::File, 173 path: path.to_path_buf(), 174 err: Some(e.to_string()), 175 }) 176} 177 178impl FileSystemWriter for ProjectIO { 179 fn delete_directory(&self, path: &Utf8Path) -> Result<()> { 180 delete_directory(path) 181 } 182 183 fn copy(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> { 184 copy(from, to) 185 } 186 187 fn copy_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> { 188 copy_dir(from, to) 189 } 190 191 fn mkdir(&self, path: &Utf8Path) -> Result<(), Error> { 192 mkdir(path) 193 } 194 195 fn hardlink(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error> { 196 hardlink(from, to) 197 } 198 199 fn symlink_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error> { 200 symlink_dir(from, to) 201 } 202 203 fn delete_file(&self, path: &Utf8Path) -> Result<()> { 204 delete_file(path) 205 } 206 207 fn write(&self, path: &Utf8Path, content: &str) -> Result<(), Error> { 208 write(path, content) 209 } 210 211 fn write_bytes(&self, path: &Utf8Path, content: &[u8]) -> Result<(), Error> { 212 write_bytes(path, content) 213 } 214 215 fn exists(&self, path: &Utf8Path) -> bool { 216 path.exists() 217 } 218} 219 220impl CommandExecutor for ProjectIO { 221 fn exec(&self, command: Command) -> Result<i32, Error> { 222 let Command { 223 program, 224 args, 225 env, 226 cwd, 227 stdio, 228 } = command; 229 tracing::debug!(program=program, args=?args.join(" "), env=?env, cwd=?cwd, "command_exec"); 230 let result = std::process::Command::new(&program) 231 .args(args) 232 .stdin(stdio.get_process_stdio()) 233 .stdout(stdio.get_process_stdio()) 234 .envs(env.iter().map(|pair| (&pair.0, &pair.1))) 235 .current_dir(cwd.unwrap_or_else(|| Utf8Path::new("./").to_path_buf())) 236 .status(); 237 238 match result { 239 Ok(status) => Ok(status.code().unwrap_or_default()), 240 241 Err(error) => Err(match error.kind() { 242 io::ErrorKind::NotFound => Error::ShellProgramNotFound { 243 program, 244 os: get_os(), 245 }, 246 247 other => Error::ShellCommand { 248 program, 249 reason: ShellCommandFailureReason::IoError(other), 250 }, 251 }), 252 } 253 } 254} 255 256impl BeamCompilerIO for ProjectIO { 257 fn compile_beam( 258 &self, 259 out: &Utf8Path, 260 lib: &Utf8Path, 261 modules: &HashSet<Utf8PathBuf>, 262 stdio: Stdio, 263 ) -> Result<Vec<String>, Error> { 264 let mut guard = self 265 .beam_compiler 266 .lock() 267 .expect("could not lock beam_compiler"); 268 269 match &mut *guard { 270 Some(compiler) => compiler.compile(out, lib, modules, stdio), 271 None => { 272 let mut compiler = BeamCompilerInstance::new(self)?; 273 let result = compiler.compile(out, lib, modules, stdio); 274 *guard = Some(compiler); 275 result 276 } 277 } 278 } 279} 280 281impl MakeLocker for ProjectIO { 282 fn make_locker(&self, paths: &ProjectPaths, target: Target) -> Result<Box<dyn Locker>> { 283 let locker = LspLocker::new(paths, target)?; 284 Ok(Box::new(locker)) 285 } 286} 287 288impl DownloadDependencies for ProjectIO { 289 fn download_dependencies(&self, paths: &ProjectPaths) -> Result<Manifest> { 290 dependencies::resolve_and_download( 291 paths, 292 NullTelemetry, 293 None, 294 Vec::new(), 295 dependencies::DependencyManagerConfig { 296 use_manifest: dependencies::UseManifest::Yes, 297 check_major_versions: dependencies::CheckMajorVersions::No, 298 }, 299 ) 300 } 301} 302 303pub fn delete_directory(dir: &Utf8Path) -> Result<(), Error> { 304 tracing::debug!(path=?dir, "deleting_directory"); 305 if dir.exists() { 306 std::fs::remove_dir_all(dir).map_err(|e| Error::FileIo { 307 action: FileIoAction::Delete, 308 kind: FileKind::Directory, 309 path: dir.to_path_buf(), 310 err: Some(e.to_string()), 311 })?; 312 } else { 313 tracing::debug!(path=?dir, "directory_did_not_exist_for_deletion"); 314 } 315 Ok(()) 316} 317 318pub fn delete_file(file: &Utf8Path) -> Result<(), Error> { 319 tracing::debug!("Deleting file {:?}", file); 320 if file.exists() { 321 std::fs::remove_file(file).map_err(|e| Error::FileIo { 322 action: FileIoAction::Delete, 323 kind: FileKind::File, 324 path: file.to_path_buf(), 325 err: Some(e.to_string()), 326 })?; 327 } else { 328 tracing::debug!("Did not exist for deletion: {:?}", file); 329 } 330 Ok(()) 331} 332 333pub fn write_outputs_under(outputs: &[OutputFile], base: &Utf8Path) -> Result<(), Error> { 334 for file in outputs { 335 let path = base.join(&file.path); 336 match &file.content { 337 Content::Binary(buffer) => write_bytes(&path, buffer), 338 Content::Text(buffer) => write(&path, buffer), 339 }?; 340 } 341 Ok(()) 342} 343 344pub fn write_output(file: &OutputFile) -> Result<(), Error> { 345 let OutputFile { path, content } = file; 346 match content { 347 Content::Binary(buffer) => write_bytes(path, buffer), 348 Content::Text(buffer) => write(path, buffer), 349 } 350} 351 352pub fn write(path: &Utf8Path, text: &str) -> Result<(), Error> { 353 write_bytes(path, text.as_bytes()) 354} 355 356#[cfg(target_family = "unix")] 357pub fn make_executable(path: impl AsRef<Utf8Path>) -> Result<(), Error> { 358 use std::os::unix::fs::PermissionsExt; 359 tracing::debug!(path = ?path.as_ref(), "setting_permissions"); 360 361 std::fs::set_permissions(path.as_ref(), std::fs::Permissions::from_mode(0o755)).map_err( 362 |e| Error::FileIo { 363 action: FileIoAction::UpdatePermissions, 364 kind: FileKind::File, 365 path: path.as_ref().to_path_buf(), 366 err: Some(e.to_string()), 367 }, 368 )?; 369 Ok(()) 370} 371 372#[cfg(not(target_family = "unix"))] 373pub fn make_executable(_path: impl AsRef<Utf8Path>) -> Result<(), Error> { 374 Ok(()) 375} 376 377pub fn write_bytes(path: &Utf8Path, bytes: &[u8]) -> Result<(), Error> { 378 tracing::debug!(path=?path, "writing_file"); 379 380 let dir_path = path.parent().ok_or_else(|| Error::FileIo { 381 action: FileIoAction::FindParent, 382 kind: FileKind::Directory, 383 path: path.to_path_buf(), 384 err: None, 385 })?; 386 387 std::fs::create_dir_all(dir_path).map_err(|e| Error::FileIo { 388 action: FileIoAction::Create, 389 kind: FileKind::Directory, 390 path: dir_path.to_path_buf(), 391 err: Some(e.to_string()), 392 })?; 393 394 let mut f = File::create(path).map_err(|e| Error::FileIo { 395 action: FileIoAction::Create, 396 kind: FileKind::File, 397 path: path.to_path_buf(), 398 err: Some(e.to_string()), 399 })?; 400 401 f.write_all(bytes).map_err(|e| Error::FileIo { 402 action: FileIoAction::WriteTo, 403 kind: FileKind::File, 404 path: path.to_path_buf(), 405 err: Some(e.to_string()), 406 })?; 407 Ok(()) 408} 409 410pub fn write_to_open_file( 411 file: &mut File, 412 path: &Utf8PathBuf, 413 data: impl AsRef<[u8]>, 414) -> Result<()> { 415 file.write_all(data.as_ref()).map_err(|e| Error::FileIo { 416 action: FileIoAction::WriteTo, 417 kind: FileKind::File, 418 path: path.clone(), 419 err: Some(e.to_string()), 420 }) 421} 422 423fn is_gleam_path(path: &Utf8Path, dir: impl AsRef<Utf8Path>) -> bool { 424 use regex::Regex; 425 426 static RE: OnceLock<Regex> = OnceLock::new(); 427 428 RE.get_or_init(|| { 429 Regex::new(&format!( 430 "^({module}{slash})*{module}\\.gleam$", 431 module = "[a-z][_a-z0-9]*", 432 slash = "(/|\\\\)", 433 )) 434 .expect("is_gleam_path() RE regex") 435 }) 436 .is_match( 437 path.strip_prefix(dir.as_ref()) 438 .expect("is_gleam_path(): strip_prefix") 439 .as_str(), 440 ) 441} 442 443fn is_gleam_build_dir(e: &ignore::DirEntry) -> bool { 444 if !e.path().is_dir() || !e.path().ends_with("build") { 445 return false; 446 } 447 448 let Some(parent_path) = e.path().parent() else { 449 return false; 450 }; 451 452 parent_path.join("gleam.toml").exists() 453} 454 455/// Walks through all Gleam module files in the directory, even if ignored, 456/// except for those in the `build/` directory. Excludes any Gleam files within 457/// invalid module paths, for example if they or a folder they're in contain a 458/// dot or a hyphen within their names. 459pub fn gleam_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ { 460 ignore::WalkBuilder::new(dir) 461 .follow_links(true) 462 .standard_filters(false) 463 .filter_entry(|entry| !is_gleam_build_dir(entry)) 464 .build() 465 .filter_map(Result::ok) 466 .filter(|entry| { 467 entry 468 .file_type() 469 .map(|type_| type_.is_file()) 470 .unwrap_or(false) 471 }) 472 .map(ignore::DirEntry::into_path) 473 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path")) 474 .filter(move |d| is_gleam_path(d, dir)) 475} 476 477/// Walks through all native files in the directory, such as `.mjs` and `.erl`, 478/// even if ignored. 479pub fn native_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ { 480 ignore::WalkBuilder::new(dir) 481 .follow_links(true) 482 .standard_filters(false) 483 .filter_entry(|entry| !is_gleam_build_dir(entry)) 484 .build() 485 .filter_map(Result::ok) 486 .filter(|entry| { 487 entry 488 .file_type() 489 .map(|type_| type_.is_file()) 490 .unwrap_or(false) 491 }) 492 .map(ignore::DirEntry::into_path) 493 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path")) 494 .filter(|path| { 495 let extension = path.extension().unwrap_or_default(); 496 is_native_file_extension(extension) 497 }) 498} 499 500/// Walks through all files in the directory, even if ignored. 501pub fn priv_directory_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ { 502 ignore::WalkBuilder::new(dir) 503 .follow_links(true) 504 .standard_filters(false) 505 .build() 506 .filter_map(Result::ok) 507 .filter(|entry| { 508 entry 509 .file_type() 510 .map(|type_| type_.is_file()) 511 .unwrap_or(false) 512 }) 513 .map(ignore::DirEntry::into_path) 514 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path")) 515} 516 517/// Walks through all `.erl` and `.hrl` files in the directory, even if ignored. 518pub fn erlang_files(dir: &Utf8Path) -> impl Iterator<Item = Utf8PathBuf> + '_ { 519 ignore::WalkBuilder::new(dir) 520 .follow_links(true) 521 .standard_filters(false) 522 .build() 523 .filter_map(Result::ok) 524 .filter(|entry| { 525 entry 526 .file_type() 527 .map(|type_| type_.is_file()) 528 .unwrap_or(false) 529 }) 530 .map(ignore::DirEntry::into_path) 531 .map(|path| Utf8PathBuf::from_path_buf(path).expect("Non Utf-8 Path")) 532 .filter(|path| { 533 let extension = path.extension().unwrap_or_default(); 534 extension == "erl" || extension == "hrl" 535 }) 536} 537 538pub fn create_tar_archive(outputs: Vec<OutputFile>) -> Result<Vec<u8>, Error> { 539 tracing::debug!("creating_tar_archive"); 540 541 let encoder = flate2::write::GzEncoder::new(vec![], flate2::Compression::default()); 542 let mut builder = tar::Builder::new(encoder); 543 544 for file in outputs { 545 let mut header = tar::Header::new_gnu(); 546 header.set_path(&file.path).map_err(|e| Error::AddTar { 547 path: file.path.clone(), 548 err: e.to_string(), 549 })?; 550 header.set_size(file.content.as_bytes().len() as u64); 551 header.set_cksum(); 552 builder 553 .append(&header, file.content.as_bytes()) 554 .map_err(|e| Error::AddTar { 555 path: file.path.clone(), 556 err: e.to_string(), 557 })?; 558 } 559 560 builder 561 .into_inner() 562 .map_err(|e| Error::TarFinish(e.to_string()))? 563 .finish() 564 .map_err(|e| Error::Gzip(e.to_string())) 565} 566 567pub fn mkdir(path: impl AsRef<Utf8Path> + Debug) -> Result<(), Error> { 568 if path.as_ref().exists() { 569 return Ok(()); 570 } 571 572 tracing::debug!(path=?path, "creating_directory"); 573 574 std::fs::create_dir_all(path.as_ref()).map_err(|err| Error::FileIo { 575 kind: FileKind::Directory, 576 path: Utf8PathBuf::from(path.as_ref()), 577 action: FileIoAction::Create, 578 err: Some(err.to_string()), 579 }) 580} 581 582pub fn read_dir(path: impl AsRef<Utf8Path> + Debug) -> Result<ReadDirUtf8, Error> { 583 tracing::debug!(path=?path,"reading_directory"); 584 585 Utf8Path::read_dir_utf8(path.as_ref()).map_err(|e| Error::FileIo { 586 action: FileIoAction::Read, 587 kind: FileKind::Directory, 588 path: Utf8PathBuf::from(path.as_ref()), 589 err: Some(e.to_string()), 590 }) 591} 592 593pub fn module_caches_paths( 594 path: impl AsRef<Utf8Path> + Debug, 595) -> Result<impl Iterator<Item = Utf8PathBuf>, Error> { 596 Ok(read_dir(path)? 597 .filter_map(Result::ok) 598 .map(|f| f.into_path()) 599 .filter(|p| p.extension() == Some("cache"))) 600} 601 602pub fn read(path: impl AsRef<Utf8Path>) -> Result<String, Error> { 603 let path = path.as_ref(); 604 tracing::debug!(path=?path,"reading_file"); 605 606 std::fs::read_to_string(path).map_err(|err| Error::FileIo { 607 action: FileIoAction::Read, 608 kind: FileKind::File, 609 path: Utf8PathBuf::from(path), 610 err: Some(err.to_string()), 611 }) 612} 613 614pub fn open_file(path: impl AsRef<Utf8Path>) -> Result<File, Error> { 615 let path = path.as_ref(); 616 tracing::debug!(path=?path,"opening_file"); 617 618 File::create(path).map_err(|err| Error::FileIo { 619 action: FileIoAction::Open, 620 kind: FileKind::File, 621 path: Utf8PathBuf::from(path), 622 err: Some(err.to_string()), 623 }) 624} 625 626pub fn read_bytes(path: impl AsRef<Utf8Path>) -> Result<Vec<u8>, Error> { 627 let path = path.as_ref(); 628 tracing::debug!(path=?path,"reading_file"); 629 630 std::fs::read(path).map_err(|err| Error::FileIo { 631 action: FileIoAction::Read, 632 kind: FileKind::File, 633 path: Utf8PathBuf::from(path), 634 err: Some(err.to_string()), 635 }) 636} 637 638pub fn reader(path: impl AsRef<Utf8Path>) -> Result<WrappedReader, Error> { 639 let path = path.as_ref(); 640 tracing::debug!(path=?path,"opening_file_reader"); 641 642 let reader = File::open(path).map_err(|err| Error::FileIo { 643 action: FileIoAction::Open, 644 kind: FileKind::File, 645 path: Utf8PathBuf::from(path), 646 err: Some(err.to_string()), 647 })?; 648 649 Ok(WrappedReader::new(path, Box::new(reader))) 650} 651 652pub fn buffered_reader<P: AsRef<Utf8Path>>(path: P) -> Result<impl BufRead, Error> { 653 let path = path.as_ref(); 654 tracing::debug!(path=?path,"opening_file_buffered_reader"); 655 let reader = File::open(path).map_err(|err| Error::FileIo { 656 action: FileIoAction::Open, 657 kind: FileKind::File, 658 path: Utf8PathBuf::from(path), 659 err: Some(err.to_string()), 660 })?; 661 Ok(BufReader::new(reader)) 662} 663 664pub fn copy(path: impl AsRef<Utf8Path>, to: impl AsRef<Utf8Path>) -> Result<(), Error> { 665 let path = path.as_ref(); 666 let to = to.as_ref(); 667 tracing::debug!(from=?path, to=?to, "copying_file"); 668 669 // TODO: include the destination in the error message 670 std::fs::copy(path, to) 671 .map_err(|err| Error::FileIo { 672 action: FileIoAction::Copy, 673 kind: FileKind::File, 674 path: Utf8PathBuf::from(path), 675 err: Some(err.to_string()), 676 }) 677 .map(|_| ()) 678} 679 680pub fn copy_dir(path: impl AsRef<Utf8Path>, to: impl AsRef<Utf8Path>) -> Result<(), Error> { 681 let path = path.as_ref(); 682 let to = to.as_ref(); 683 tracing::debug!(from=?path, to=?to, "copying_directory"); 684 685 // TODO: include the destination in the error message 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, 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, 715 kind: FileKind::File, 716 path: Utf8PathBuf::from(dest), 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, 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, 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}