Skip to content

Commit d4b3f6d

Browse files
committed
Add coursier language support
Improve
1 parent e479cb6 commit d4b3f6d

11 files changed

Lines changed: 428 additions & 12 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,12 @@ jobs:
578578
with:
579579
bun-version: ${{ env.BUN_VERSION }}
580580

581+
- name: "Install Coursier"
582+
if: ${{ contains(format(' {0} ', matrix.languages), ' coursier ') }}
583+
uses: coursier/setup-action@fd1707a76b027efdfb66ca79318b4d29b72e5a02 # v3.0.0
584+
with:
585+
apps: ""
586+
581587
- name: "Install Deno"
582588
if: ${{ contains(format(' {0} ', matrix.languages), ' deno ') }}
583589
uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4

crates/prek-consts/src/env_vars.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ impl EnvVars {
8787
// Dart related
8888
pub const PUB_CACHE: &'static str = "PUB_CACHE";
8989

90+
// Coursier related
91+
pub const COURSIER_CACHE: &'static str = "COURSIER_CACHE";
92+
9093
// Ruby related
9194
pub const PREK_RUBY_MIRROR: &'static str = "PREK_RUBY_MIRROR";
9295
pub const GEM_HOME: &'static str = "GEM_HOME";
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
use std::ffi::OsStr;
2+
use std::io::ErrorKind;
3+
use std::path::Path;
4+
use std::process::Stdio;
5+
use std::sync::Arc;
6+
7+
use anyhow::{Context, Result};
8+
use prek_consts::env_vars::EnvVars;
9+
use prek_consts::prepend_paths;
10+
use tracing::debug;
11+
12+
use crate::cli::reporter::HookInstallReporter;
13+
use crate::cli::run::HookRunReporter;
14+
use crate::hook::{Hook, InstallInfo, InstalledHook};
15+
use crate::languages::LanguageImpl;
16+
use crate::process::Cmd;
17+
use crate::run::run_by_batch;
18+
use crate::store::{CacheBucket, Store};
19+
20+
const PRE_COMMIT_CHANNEL_DIR: &str = ".pre-commit-channel";
21+
22+
#[derive(Debug, Copy, Clone)]
23+
pub(crate) struct Coursier;
24+
25+
fn channel_app_name(file_name: &str) -> &str {
26+
match file_name.rfind('.') {
27+
Some(0) | None => file_name,
28+
Some(index) if index + 1 == file_name.len() => file_name,
29+
Some(index) => &file_name[..index],
30+
}
31+
}
32+
33+
fn collect_channel_apps(channel_dir: &Path) -> Result<Option<Vec<String>>> {
34+
let entries = match fs_err::read_dir(channel_dir) {
35+
Ok(entries) => entries,
36+
Err(err) if matches!(err.kind(), ErrorKind::NotFound | ErrorKind::NotADirectory) => {
37+
return Ok(None);
38+
}
39+
Err(err) => {
40+
return Err(err).with_context(|| format!("Failed to read `{}`", channel_dir.display()));
41+
}
42+
};
43+
44+
let mut apps = entries
45+
.map(|entry| {
46+
let file_name = entry?.file_name();
47+
let file_name = file_name.to_string_lossy();
48+
Ok(channel_app_name(&file_name).to_string())
49+
})
50+
.collect::<Result<Vec<_>>>()?;
51+
apps.sort_unstable();
52+
Ok(Some(apps))
53+
}
54+
55+
async fn install_coursier_app(
56+
cs: &Path,
57+
env_path: &Path,
58+
cache_path: &Path,
59+
source_path: &Path,
60+
path_env: &OsStr,
61+
args: &[String],
62+
) -> Result<()> {
63+
Cmd::new(cs, "coursier install")
64+
.current_dir(source_path)
65+
.arg("install")
66+
.arg("--dir")
67+
.arg(env_path)
68+
.args(args)
69+
.env(EnvVars::PATH, path_env)
70+
.env(EnvVars::COURSIER_CACHE, cache_path)
71+
.check(true)
72+
.output()
73+
.await
74+
.with_context(|| format!("Failed to install coursier app `{}`", args.join(" ")))?;
75+
76+
Ok(())
77+
}
78+
79+
impl LanguageImpl for Coursier {
80+
async fn install(
81+
&self,
82+
hook: Arc<Hook>,
83+
store: &Store,
84+
reporter: &HookInstallReporter,
85+
) -> Result<InstalledHook> {
86+
let progress = reporter.on_install_start(&hook);
87+
88+
let source_path = hook.repo_path().unwrap_or_else(|| hook.work_dir());
89+
let channel_dir = source_path.join(PRE_COMMIT_CHANNEL_DIR);
90+
let channel_apps = collect_channel_apps(&channel_dir)?;
91+
92+
let mut dependencies = hook
93+
.additional_dependencies
94+
.iter()
95+
.cloned()
96+
.collect::<Vec<_>>();
97+
dependencies.sort_unstable();
98+
99+
if channel_apps.is_none() && dependencies.is_empty() {
100+
anyhow::bail!("expected .pre-commit-channel dir or additional_dependencies");
101+
}
102+
103+
let cs = which::which("cs")
104+
.or_else(|_| which::which("coursier"))
105+
.context(
106+
"Coursier hooks require system-installed `cs` or `coursier` executables in PATH",
107+
)?;
108+
let mut info = InstallInfo::new(
109+
hook.language,
110+
hook.env_key_dependencies().clone(),
111+
&store.hooks_dir(),
112+
)?;
113+
114+
debug!(%hook, target = %info.env_path.display(), "Installing Coursier environment");
115+
116+
fs_err::tokio::create_dir_all(&info.env_path).await?;
117+
let coursier_cache = store.cache_path(CacheBucket::Coursier);
118+
fs_err::tokio::create_dir_all(&coursier_cache).await?;
119+
120+
let path_env = prepend_paths(&[&info.env_path]).context("Failed to join PATH")?;
121+
122+
if let Some(channel_apps) = channel_apps {
123+
for app in channel_apps {
124+
let args = vec![
125+
"--default-channels=false".to_string(),
126+
"--channel".to_string(),
127+
channel_dir.to_string_lossy().into_owned(),
128+
app,
129+
];
130+
install_coursier_app(
131+
&cs,
132+
&info.env_path,
133+
&coursier_cache,
134+
source_path,
135+
&path_env,
136+
&args,
137+
)
138+
.await?;
139+
}
140+
}
141+
142+
if !dependencies.is_empty() {
143+
Cmd::new(&cs, "coursier fetch")
144+
.current_dir(source_path)
145+
.arg("fetch")
146+
.args(&dependencies)
147+
.env(EnvVars::PATH, &path_env)
148+
.env(EnvVars::COURSIER_CACHE, &coursier_cache)
149+
.check(true)
150+
.output()
151+
.await
152+
.with_context(|| {
153+
format!("Failed to fetch coursier app `{}`", dependencies.join(" "))
154+
})?;
155+
install_coursier_app(
156+
&cs,
157+
&info.env_path,
158+
&coursier_cache,
159+
source_path,
160+
&path_env,
161+
&dependencies,
162+
)
163+
.await?;
164+
}
165+
166+
info.with_toolchain(cs);
167+
info.persist_env_path();
168+
169+
reporter.on_install_complete(progress);
170+
171+
Ok(InstalledHook::Installed {
172+
hook,
173+
info: Arc::new(info),
174+
})
175+
}
176+
177+
async fn check_health(&self, _info: &InstallInfo) -> Result<()> {
178+
Ok(())
179+
}
180+
181+
async fn run(
182+
&self,
183+
hook: &InstalledHook,
184+
filenames: &[&Path],
185+
store: &Store,
186+
reporter: &HookRunReporter,
187+
) -> Result<(i32, Vec<u8>)> {
188+
let progress = reporter.on_run_start(hook, filenames.len());
189+
190+
let env_path = hook.env_path().expect("Coursier must have env path");
191+
let coursier_cache = store.cache_path(CacheBucket::Coursier);
192+
let path_env = prepend_paths(&[env_path]).context("Failed to join PATH")?;
193+
let entry = hook.entry.resolve(Some(&path_env), store)?;
194+
195+
let run = async |batch: &[&Path]| {
196+
let mut output = Cmd::new(&entry[0], "run coursier hook")
197+
.current_dir(hook.work_dir())
198+
.args(&entry[1..])
199+
.envs(&hook.env)
200+
.args(&hook.args)
201+
.args(batch)
202+
.check(false)
203+
.stdin(Stdio::null())
204+
.env(EnvVars::PATH, &path_env)
205+
.env(EnvVars::COURSIER_CACHE, &coursier_cache)
206+
.pty_output_with_sink(reporter.output_sink(progress))
207+
.await?;
208+
209+
reporter.on_run_progress(progress, batch.len() as u64);
210+
211+
output.stdout.extend(output.stderr);
212+
let code = output.status.code().unwrap_or(1);
213+
anyhow::Ok((code, output.stdout))
214+
};
215+
216+
let results = run_by_batch(hook, filenames, entry.argv(), run).await?;
217+
218+
let mut combined_status = 0;
219+
let mut combined_output = Vec::new();
220+
221+
for (code, output) in results {
222+
combined_status |= code;
223+
combined_output.extend(output);
224+
}
225+
226+
reporter.on_run_complete(progress);
227+
228+
Ok((combined_status, combined_output))
229+
}
230+
}
231+
232+
#[cfg(test)]
233+
mod tests {
234+
use super::channel_app_name;
235+
236+
#[test]
237+
fn channel_app_name_drops_descriptor_extension() {
238+
assert_eq!(channel_app_name("scalafmt.json"), "scalafmt");
239+
assert_eq!(channel_app_name("foo.bar.json"), "foo.bar");
240+
}
241+
242+
#[test]
243+
fn channel_app_name_keeps_dotfiles_and_trailing_dots() {
244+
assert_eq!(channel_app_name(".scalafmt"), ".scalafmt");
245+
assert_eq!(channel_app_name("scalafmt."), "scalafmt.");
246+
}
247+
}

crates/prek/src/languages/mod.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::hooks;
1717
use crate::store::{CacheBucket, Store, ToolBucket};
1818

1919
mod bun;
20+
mod coursier;
2021
mod dart;
2122
mod deno;
2223
mod docker;
@@ -38,6 +39,7 @@ mod system;
3839
pub(crate) mod version;
3940

4041
static BUN: bun::Bun = bun::Bun;
42+
static COURSIER: coursier::Coursier = coursier::Coursier;
4143
static DART: dart::Dart = dart::Dart;
4244
static DENO: deno::Deno = deno::Deno;
4345
static DOCKER: docker::Docker = docker::Docker;
@@ -141,6 +143,7 @@ impl Language {
141143
pub(crate) fn supported(self) -> bool {
142144
match self {
143145
Self::Bun
146+
| Self::Coursier
144147
| Self::Dart
145148
| Self::Deno
146149
| Self::Docker
@@ -159,7 +162,7 @@ impl Language {
159162
| Self::Script
160163
| Self::Swift
161164
| Self::System => true,
162-
Self::Conda | Self::Coursier | Self::Perl | Self::R => false,
165+
Self::Conda | Self::Perl | Self::R => false,
163166
}
164167
}
165168

@@ -191,6 +194,7 @@ impl Language {
191194
pub(crate) fn shell_support(self) -> ShellSupport {
192195
match self {
193196
Self::Bun
197+
| Self::Coursier
194198
| Self::Deno
195199
| Self::Dotnet
196200
| Self::Golang
@@ -202,7 +206,7 @@ impl Language {
202206
| Self::Script
203207
| Self::Swift
204208
| Self::System => ShellSupport::Supported,
205-
Self::Conda | Self::Coursier | Self::Perl | Self::R => {
209+
Self::Conda | Self::Perl | Self::R => {
206210
ShellSupport::Unsupported("no runner is implemented yet")
207211
}
208212
Self::Dart => ShellSupport::Unsupported(
@@ -248,14 +252,14 @@ impl Language {
248252

249253
pub(crate) fn cache_buckets(self) -> &'static [CacheBucket] {
250254
match self {
255+
Self::Coursier => &[CacheBucket::Coursier],
251256
Self::Deno => &[CacheBucket::Deno],
252257
Self::Golang => &[CacheBucket::Go],
253258
Self::Node => &[CacheBucket::Npm],
254259
Self::Python | Self::Pygrep => &[CacheBucket::Uv, CacheBucket::Python],
255260
Self::Rust => &[CacheBucket::Cargo],
256261
Self::Bun
257262
| Self::Conda
258-
| Self::Coursier
259263
| Self::Dart
260264
| Self::Docker
261265
| Self::DockerImage
@@ -345,6 +349,7 @@ impl Language {
345349
match self {
346350
Self::Dart => DART.install(hook, store, reporter).await,
347351
Self::Bun => BUN.install(hook, store, reporter).await,
352+
Self::Coursier => COURSIER.install(hook, store, reporter).await,
348353
Self::Deno => DENO.install(hook, store, reporter).await,
349354
Self::Docker => DOCKER.install(hook, store, reporter).await,
350355
Self::DockerImage => DOCKER_IMAGE.install(hook, store, reporter).await,
@@ -362,7 +367,7 @@ impl Language {
362367
Self::Script => SCRIPT.install(hook, store, reporter).await,
363368
Self::Swift => SWIFT.install(hook, store, reporter).await,
364369
Self::System => SYSTEM.install(hook, store, reporter).await,
365-
Self::Conda | Self::Coursier | Self::Perl | Self::R => {
370+
Self::Conda | Self::Perl | Self::R => {
366371
UNIMPLEMENTED.install(hook, store, reporter).await
367372
}
368373
}
@@ -372,6 +377,7 @@ impl Language {
372377
match self {
373378
Self::Dart => DART.check_health(info).await,
374379
Self::Bun => BUN.check_health(info).await,
380+
Self::Coursier => COURSIER.check_health(info).await,
375381
Self::Deno => DENO.check_health(info).await,
376382
Self::Docker => DOCKER.check_health(info).await,
377383
Self::DockerImage => DOCKER_IMAGE.check_health(info).await,
@@ -389,9 +395,7 @@ impl Language {
389395
Self::Script => SCRIPT.check_health(info).await,
390396
Self::Swift => SWIFT.check_health(info).await,
391397
Self::System => SYSTEM.check_health(info).await,
392-
Self::Conda | Self::Coursier | Self::Perl | Self::R => {
393-
UNIMPLEMENTED.check_health(info).await
394-
}
398+
Self::Conda | Self::Perl | Self::R => UNIMPLEMENTED.check_health(info).await,
395399
}
396400
}
397401

@@ -428,6 +432,7 @@ impl Language {
428432
match self {
429433
Self::Dart => DART.run(hook, filenames, store, reporter).await,
430434
Self::Bun => BUN.run(hook, filenames, store, reporter).await,
435+
Self::Coursier => COURSIER.run(hook, filenames, store, reporter).await,
431436
Self::Deno => DENO.run(hook, filenames, store, reporter).await,
432437
Self::Docker => DOCKER.run(hook, filenames, store, reporter).await,
433438
Self::DockerImage => DOCKER_IMAGE.run(hook, filenames, store, reporter).await,
@@ -445,7 +450,7 @@ impl Language {
445450
Self::Script => SCRIPT.run(hook, filenames, store, reporter).await,
446451
Self::Swift => SWIFT.run(hook, filenames, store, reporter).await,
447452
Self::System => SYSTEM.run(hook, filenames, store, reporter).await,
448-
Self::Conda | Self::Coursier | Self::Perl | Self::R => {
453+
Self::Conda | Self::Perl | Self::R => {
449454
UNIMPLEMENTED.run(hook, filenames, store, reporter).await
450455
}
451456
}

crates/prek/src/store.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ pub(crate) enum CacheBucket {
416416
Cargo,
417417
Deno,
418418
Npm,
419+
Coursier,
419420
Prek,
420421
}
421422

0 commit comments

Comments
 (0)