Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sys.which(), use that to find curl in download() #26559

Merged
merged 4 commits into from
May 22, 2018
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
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,8 @@ Library improvements
* `trunc`, `floor`, `ceil`, and `round` specify `digits`, `sigdigits` and `base` using
keyword arguments. ([#26156], [#26670])

* `Sys.which()` provides a cross-platform method to find executable files, similar to
the Unix `which` command. ([#26559])

Compiler/Runtime improvements
-----------------------------
Expand Down
14 changes: 8 additions & 6 deletions base/download.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

downloadcmd = nothing
if Sys.iswindows()
downloadcmd = :powershell
downloadcmd = "powershell"
function download(url::AbstractString, filename::AbstractString)
ps = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
tls12 = "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12"
Expand All @@ -27,23 +27,25 @@ else
function download(url::AbstractString, filename::AbstractString)
global downloadcmd
if downloadcmd === nothing
for checkcmd in (:curl, :wget, :fetch)
if success(pipeline(`which $checkcmd`, devnull))
for checkcmd in ("curl", "wget", "fetch")
try
# Sys.which() will throw() if it can't find `checkcmd`
Sys.which(checkcmd)
downloadcmd = checkcmd
break
end
end
end
if downloadcmd == :wget
if downloadcmd == "wget"
try
run(`wget -O $filename $url`)
catch
rm(filename) # wget always creates a file
rethrow()
end
elseif downloadcmd == :curl
elseif downloadcmd == "curl"
run(`curl -g -L -f -o $filename $url`)
elseif downloadcmd == :fetch
elseif downloadcmd == "fetch"
run(`fetch -f $filename $url`)
else
error("no download agent available; install curl, wget, or fetch")
Expand Down
88 changes: 86 additions & 2 deletions base/sysinfo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export BINDIR,
isbsd,
islinux,
isunix,
iswindows
iswindows,
isexecutable,
which

import ..Base: show

Expand Down Expand Up @@ -312,14 +314,96 @@ if iswindows()
else
windows_version() = v"0.0"
end

"""
Sys.windows_version()

Returns the version number for the Windows NT Kernel as a `VersionNumber`,
Return the version number for the Windows NT Kernel as a `VersionNumber`,
i.e. `v"major.minor.build"`, or `v"0.0.0"` if this is not running on Windows.
"""
windows_version

const WINDOWS_VISTA_VER = v"6.0"

"""
Sys.isexecutable(path::String)

Return `true` if the given `path` has executable permissions.
"""
function isexecutable(path::String)
if iswindows()
return isfile(path)
else
# We use `access()` and `X_OK` to determine if a given path is
# executable by the current user. `X_OK` comes from `unistd.h`.
X_OK = 0x01
ccall(:access, Cint, (Ptr{UInt8}, Cint), path, X_OK) == 0
end
end
isexecutable(path::AbstractString) = isexecutable(String(path))

"""
Sys.which(program_name::String)

Given a program name, search the current `PATH` to find the first binary with
the proper executable permissions that can be run, and return an absolute
path. Raise `ErrorException` if no such program is available. If a path with
a directory in it is passed in for `program_name`, tests that exact path
for executable permissions only (with `.exe` and `.com` extensions added on
Windows platforms); no searching of `PATH` is performed.
"""
function which(program_name::String)
# Build a list of program names that we're going to try
program_names = String[]
base_pname = basename(program_name)
if iswindows()
# If the file already has an extension, try that name first
if !isempty(splitext(base_pname)[2])
push!(program_names, base_pname)
end

# But also try appending .exe and .com`
for pe in (".exe", ".com")
push!(program_names, string(base_pname, pe))
end
else
# On non-windows, we just always search for what we've been given
push!(program_names, base_pname)
end

path_dirs = String[]
program_dirname = dirname(program_name)
# If we've been given a path that has a directory name in it, then we
# check to see if that path exists. Otherwise, we search the PATH.
if isempty(program_dirname)
# If we have been given just a program name (not a relative or absolute
# path) then we should search `PATH` for it here:
pathsep = iswindows() ? ';' : ':'
path_dirs = abspath.(split(get(ENV, "PATH", ""), pathsep))

# On windows we always check the current directory as well
if iswindows()
pushfirst!(path_dirs, pwd())
end
else
push!(path_dirs, abspath(program_dirname))
end

# Here we combine our directories with our program names, searching for the
# first match among all combinations.
for path_dir in path_dirs
for pname in program_names
program_path = joinpath(path_dir, pname)
# If we find something that matches our name and we can execute
if isexecutable(program_path)
return realpath(program_path)
end
end
end

# If we couldn't find anything, complain
error("$program_name not found")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be an ArgumentError.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see it originally was but Jameson said it shouldn't be. Just using error here seems unfortunately general.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but I don't have a good feel for what errors should be what, so in the absence of a better idea, I'm going to keep it as error() for now.

end
which(program_name::AbstractString) = which(String(program_name))

end # module Sys
82 changes: 82 additions & 0 deletions test/spawn.jl
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,85 @@ let
output = readchomp(pipeline(cmd, stderr=catcmd))
@test occursin("Info: Hello", output)
end

# Sys.which() testing
psep = if Sys.iswindows() ";" else ":" end
withenv("PATH" => "$(Sys.BINDIR)$(psep)$(ENV["PATH"])") do
julia_exe = joinpath(Sys.BINDIR, "julia")
if Sys.iswindows()
julia_exe *= ".exe"
end

@test Sys.which("julia") == realpath(julia_exe)
@test Sys.which(julia_exe) == realpath(julia_exe)
end

mktempdir() do dir
withenv("PATH" => "$(dir)$(psep)$(ENV["PATH"])") do
# Test that files lacking executable permissions fail Sys.which
# but only on non-Windows systems, as Windows doesn't care...
foo_path = joinpath(dir, "foo")
touch(foo_path)
chmod(foo_path, 0o777)
if !Sys.iswindows()
@test Sys.which("foo") == realpath(foo_path)
@test Sys.which(foo_path) == realpath(foo_path)

chmod(foo_path, 0o666)
@test_throws ErrorException Sys.which("foo")
@test_throws ErrorException Sys.which(foo_path)
end

# Test that completely missing files also fail
@test_throws ErrorException Sys.which("this_is_not_a_command")
end
end

mktempdir() do dir
withenv("PATH" => "$(joinpath(dir, "bin1"))$(psep)$(joinpath(dir, "bin2"))$(psep)$(ENV["PATH"])") do
# Test that we have proper priorities
mkpath(joinpath(dir, "bin1"))
mkpath(joinpath(dir, "bin2"))
foo1_path = joinpath(dir, "bin1", "foo")
foo2_path = joinpath(dir, "bin2", "foo")

# On windows, we find things with ".exe" and ".com"
if Sys.iswindows()
foo1_path *= ".exe"
foo2_path *= ".com"
end

touch(foo1_path)
touch(foo2_path)
chmod(foo1_path, 0o777)
chmod(foo2_path, 0o777)
@test Sys.which("foo") == realpath(foo1_path)

# chmod() doesn't change which() on Windows, so don't bother to test that
if !Sys.iswindows()
chmod(foo1_path, 0o666)
@test Sys.which("foo") == realpath(foo2_path)
chmod(foo1_path, 0o777)
end

if Sys.iswindows()
# On windows, check that pwd() takes precedence, except when we provide a path
cd(joinpath(dir, "bin2")) do
@test Sys.which("foo") == realpath(foo2_path)
@test Sys.which(foo1_path) == realpath(foo1_path)
end
end

# Check that "bin1/bar" will actually run "bin1/bar"
bar_path = joinpath(dir, "bin1", "bar")
if Sys.iswindows()
bar_path *= ".exe"
end

touch(bar_path)
chmod(bar_path, 0o777)
cd(dir) do
@test Sys.which(joinpath("bin1", "bar")) == realpath(bar_path)
end
end
end