Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions e2e-win/task.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -423,4 +423,92 @@ file = "scripts/thing"
}
}
}

Context 'a task-directory file Windows will not run' {
BeforeAll {
# Deliberately outside $TestDrive: the config at the TestDrive root includes a tasks
# directory, and a project that inherits those tasks never reaches the two diagnostics
# that only fire when nothing resolved at all.
$script:skipRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
$script:aloneDir = Join-Path $script:skipRoot 'alone'
$script:besideDir = Join-Path $script:skipRoot 'beside'
New-Item -ItemType Directory -Path (Join-Path $script:aloneDir 'mise-tasks') -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $script:besideDir 'mise-tasks') -Force | Out-Null

# No shebang and no known extension. There is no permission bit for `is_executable` to
# consult on this platform, so this file is simply not a task.
Set-Content (Join-Path $script:aloneDir 'mise-tasks\skipped') 'echo hi' -NoNewline
New-Item -ItemType File -Path (Join-Path $script:aloneDir 'mise.toml') | Out-Null

# The same file with a working task beside it, which sends `mise run` down its other
# branch -- the one that has a task list to suggest from. The warning is the only
# thing that names the file there.
Set-Content (Join-Path $script:besideDir 'mise-tasks\skipped') 'echo hi' -NoNewline
Set-Content (Join-Path $script:besideDir 'mise-tasks\works.bat') "@echo off`r`necho works"
New-Item -ItemType File -Path (Join-Path $script:besideDir 'mise.toml') | Out-Null

# Global tasks would also keep these projects from being empty, and this suite does
# not otherwise isolate them.
$script:originalConfigDir = [Environment]::GetEnvironmentVariable('MISE_CONFIG_DIR', 'Process')
$env:MISE_CONFIG_DIR = Join-Path $script:skipRoot 'config'
New-Item -ItemType Directory -Path $env:MISE_CONFIG_DIR | Out-Null
$script:describeTrusted = $env:MISE_TRUSTED_CONFIG_PATHS
$env:MISE_TRUSTED_CONFIG_PATHS = $script:skipRoot
}

AfterAll {
$env:MISE_TRUSTED_CONFIG_PATHS = $script:describeTrusted
if ($null -eq $script:originalConfigDir) {
Remove-Item -Path Env:\MISE_CONFIG_DIR -ErrorAction SilentlyContinue
} else {
$env:MISE_CONFIG_DIR = $script:originalConfigDir
}
Remove-Item -Recurse -Force $script:skipRoot -ErrorAction SilentlyContinue
}

It 'says why `mise tasks ls` found nothing' {
Push-Location $script:aloneDir
try {
$out = mise tasks ls 2>&1 | Out-String
$LASTEXITCODE | Should -Be 0
$out | Should -BeLike '*non-executable*'
$out | Should -BeLike '*shebang*'
# Being told to run chmod is why this diagnostic was switched off here at all.
$out | Should -Not -BeLike '*chmod*'
} finally {
Pop-Location
}
}

It 'names the file when the project has no other tasks' {
Push-Location $script:aloneDir
try {
$out = mise run skipped 2>&1 | Out-String
$LASTEXITCODE | Should -Not -Be 0
$out | Should -BeLike '*non-executable*'
$out | Should -BeLike '*mise-tasks*'
$out | Should -BeLike '*shebang*'
$out | Should -Not -BeLike '*chmod*'
} finally {
Pop-Location
}
}

It 'names the file when the project has other tasks' {
Push-Location $script:besideDir
try {
# The control: the sibling really is a task here, so the run below fails over the
# skipped file rather than over an empty project.
mise run works | Select -Last 1 | Should -Be 'works'
$out = mise run skipped 2>&1 | Out-String
$LASTEXITCODE | Should -Not -Be 0
$out | Should -BeLike '*non-executable*'
$out | Should -BeLike '*mise-tasks*'
$out | Should -BeLike '*shebang*'
$out | Should -Not -BeLike '*chmod*'
} finally {
Pop-Location
}
}
}
}
11 changes: 8 additions & 3 deletions src/cli/tasks/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::sync::Arc;

use crate::config::{self, Config};
use crate::dirs;
use crate::file;
use crate::file::display_rel_path;
use crate::task::Task;
use crate::task::task_fetcher::TaskFetcher;
Expand Down Expand Up @@ -164,13 +165,17 @@ impl TasksLs {
// Warn about non-executable files only when there are truly no tasks at all
// (not just filtered out by --hidden/--local/--global)
if all_tasks.is_empty()
&& !cfg!(windows)
&& let Some(cwd) = &*dirs::CWD
{
let includes = config::task_includes_for_dir(cwd, &config.config_files)?;
if !find_non_executable_task_files(&includes).is_empty() {
// One file is enough to act on, and `make_executable_hint` is the only thing that
// knows what "make it executable" means on this platform. Bound to a local because
// under edition 2024 an `if let` scrutinee temporary is dropped before the body runs.
let non_executable = find_non_executable_task_files(&includes);
if let Some(path) = non_executable.first() {
warn!(
"no tasks found, but non-executable files exist in task directories.\nFiles must be executable to be detected as tasks. Run `chmod +x` on the task files to fix this."
"no tasks found, but non-executable files exist in task directories.\nFiles must be executable to be detected as tasks. {}",
file::make_executable_hint(path)
);
}
}
Expand Down
36 changes: 21 additions & 15 deletions src/task/task_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,6 @@ async fn make_task_executable(
name: &str,
task_context: Option<&TaskLoadContext>,
) -> Result<Option<Task>> {
if cfg!(windows) {
return Ok(None);
}
let Some(cwd) = &*dirs::CWD else {
return Ok(None);
};
Expand Down Expand Up @@ -217,11 +214,24 @@ async fn make_task_executable(
return Ok(None);
};

// The prompt below is the only part Windows cannot use: `make_executable` does not change
// whether Windows will run a file, so a "yes" would be taken and change nothing. The warning
// itself is still wanted -- when the project has other tasks this is the only place that names
// the file at all -- so on Windows it carries the remedy instead of handing off to a prompt.
let windows_has_no_prompt = cfg!(windows);
let remedy = if windows_has_no_prompt {
format!(" {}", file::make_executable_hint(&path))
} else {
String::new()
};
warn!(
"no task {} found, but a non-executable file exists at {}",
"no task {} found, but a non-executable file exists at {}{remedy}",
style::ered(name),
display_path(&path)
);
if windows_has_no_prompt {
return Ok(None);
}
let confirmed = config::Settings::get().yes
|| prompt::confirm("Mark this file as executable to allow it to be run as a task?")?
.is_yes();
Expand Down Expand Up @@ -307,12 +317,13 @@ async fn err_no_task(
}

// Check if there are non-executable files in task include directories
if !cfg!(windows)
&& let Some(cwd) = &*dirs::CWD
{
if let Some(cwd) = &*dirs::CWD {
let includes = config::task_includes_for_dir(cwd, &config.config_files)?;
let non_exec_files = find_non_executable_task_files(&includes);
if !non_exec_files.is_empty() {
// The remedy differs by platform and `make_executable_hint` is the only thing that
// knows how, so it gets one file to name rather than the list. The count and the
// directories below still say how much is affected.
if let Some(first) = non_exec_files.first() {
let dirs_with_files: Vec<String> = includes
.iter()
.filter(|d| d.is_dir())
Expand All @@ -321,16 +332,11 @@ async fn err_no_task(
bail!(
"no tasks defined in {}, but found {} non-executable file(s) in {}.\n\
Files must be executable to be detected as tasks.\n\
Run `chmod +x` on the task files to fix this, e.g.:\n chmod +x {}",
{}",
display_path(dirs::CWD.clone().unwrap_or_default()),
non_exec_files.len(),
dirs_with_files.join(", "),
non_exec_files
.iter()
.take(5)
.map(display_path)
.collect::<Vec<_>>()
.join(" "),
file::make_executable_hint(first),
);
}
}
Expand Down
Loading