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

Conversation

staticfloat
Copy link
Member

It's better to invoke curl directly (e.g. via curl --help) in order to see if it exists than to use which curl and see if that successfully passes. This fixes test errors on e.g. our CentOS 7.3 buildbots, which do not have which available by default.

@iamed2
Copy link
Contributor

iamed2 commented Mar 21, 2018

Maybe someone with FreeBSD can confirm, but I don't think fetch supports --help or any other no-op argument.

@ararslan
Copy link
Member

Can confirm, it does not.

@stevengj
Copy link
Member

Might be nice to have a portable which, analogous to Python's distutils.spawn.find_executable or shutils.which, if we don't have one already.

@ararslan
Copy link
Member

@stevengj, Elliot and I were just talking about that offline. He intends to add such a function based off of https://github.com/ararslan/BuildUtils.jl/blob/master/src/BuildUtils.jl#L46-L74. 🙂

@staticfloat
Copy link
Member Author

Yep, I'll tack it onto this PR, just writing up the tests now. It'll be called Sys.which(), and I expect a full course of bikeshedding. Don't let me down guys.

base/sysinfo.jl Outdated
# If prog has a slash, we know the user wants to determine whether the given
# file exists and is executable, and to not search the `PATH`. Note that
# Windows can have either `\\` or `/` in its paths:
dirseps = @static if is_windows() ["/", "\\"] else ["/"] end
Copy link
Member

Choose a reason for hiding this comment

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

is_windows is deprecated to iswindows

base/sysinfo.jl Outdated
# Windows can have either `\\` or `/` in its paths:
dirseps = @static if is_windows() ["/", "\\"] else ["/"] end

if any(contains.(program_name, dirseps))
Copy link
Member

Choose a reason for hiding this comment

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

contains(a, b) is deprecated to occursin(b, a)

base/sysinfo.jl Outdated
# 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 = 1 << 0
can_execute(program_name) = @static if is_windows()
Copy link
Member

Choose a reason for hiding this comment

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

This could be moved out of the function

base/sysinfo.jl Outdated
["/", "\\"]
else
["/"]
end
Copy link
Member

Choose a reason for hiding this comment

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

Probably a tuple of chars here rather than an array of strings?

Copy link
Member Author

Choose a reason for hiding this comment

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

Are tuples preferred here for performance considerations?

base/sysinfo.jl Outdated
# If we have been given just a program name (not a relative or absolute
# path) then we should search `PATH` for it here:
path = get(ENV, "PATH", "")
pathsep = @static if iswindows()
Copy link
Member

@stevengj stevengj Mar 21, 2018

Choose a reason for hiding this comment

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

pathsep = @static iswindows() ? ';' : ':' would be shorter and probably more readable for conditionals like this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, for some reason I thought @static didn't work with the ternary operator. Nice.

Copy link
Member

Choose a reason for hiding this comment

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

The @static isn't entirely necessary

base/sysinfo.jl Outdated

# On windows, we need to check both $(program_name) and $(program_name).exe
program_matches(file, program_name) = @static if iswindows()
file == program_name || file == "$(program_name).exe"
Copy link
Member

@stevengj stevengj Mar 21, 2018

Choose a reason for hiding this comment

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

Don't you need to check everything in get(ENV, "PATHEXT", "exe")?

Copy link
Member

@stevengj stevengj Mar 21, 2018

Choose a reason for hiding this comment

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

Also, you presumably only want to check appending an extension if program_name doesn't already have an extension (i.e. if it doesn't contain .).

Copy link
Member

Choose a reason for hiding this comment

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

No to the first (also .cmd), yes to the second (with qualifications). We should probably try to return the same answer as our underlying spawn API (https://github.com/libuv/libuv/blob/edf05b97f03472def2837ceb2d6bf61a0d0dc248/src/win/process.c#L299)

base/sysinfo.jl Outdated
file == program_name
end

for dir in split(path, pathsep), file in readdir(dir)
Copy link
Member

Choose a reason for hiding this comment

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

Why walk the directory tree? This seems very inefficient if the directories contain lots of entries, as will be common. Instead, you could do something like:

for dir in split(path, pathsep)
     file = joinpath(path, program_name)
     isfile(file) && _can_execute(file) && return abspath(file)
     .... check extensions on windows ....
end

right?

base/sysinfo.jl Outdated

Returns `true` if the given `path` has executable permissions.
"""
function _can_execute(path::AbstractString)
Copy link
Member

Choose a reason for hiding this comment

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

isexecutable seems more idiomatic.

base/sysinfo.jl Outdated
["/"]
end

if any(occursin.(dirseps, program_name))
Copy link
Member

Choose a reason for hiding this comment

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

or just

if occursin(@static(iswindows() ? r"[/\\]" : '/'), program_name)

base/sysinfo.jl Outdated
# by the current user. `X_OK` comes from `unistd.h`.
X_OK = 1 << 0
@static if iswindows()
ccall(:_access, Cint, (Ptr{UInt8}, Cint), path, X_OK) == 0
Copy link
Member

Choose a reason for hiding this comment

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

access on windows doesn't have an X_OK parameter

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, that's an excellent point. I just spent half an hour trying to parse MSDN and figure out how GetNamedSecurityInfo can help me figure out the right ACL values to see if something is executable by the current user, but it defeated me. I'm just going to return true on Windows.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, the which function on Windows probably shares fairly little with the same function on posix. I expect they likely should just be completely independent implementations

Copy link
Member Author

Choose a reason for hiding this comment

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

.........busybox-w32 uses access(path, X_OK). Sigh.

Copy link
Member

Choose a reason for hiding this comment

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

I’m not too surprised. It’s a posix emulation later, probably intended to approximate the result you’d get on busybox sh or cygwin (similar to how the real which program on posix approximates the likely answer)

base/sysinfo.jl Outdated
pathsep = @static iswindows() ? ';' : ':'

# On windows, if the program name does not already have an extension,
# we need to tack all the values in PATHEXT onto the end and check for
Copy link
Member

Choose a reason for hiding this comment

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

Nope, see how libuv implements this function. IIRC, the distinction is that PATHEXT is for DOS, not the Shell32.

Copy link
Member Author

Choose a reason for hiding this comment

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

Which function are you referring to? What should I look for in libuv?

I'm not sure what you mean in stating that PATHEXT is only for DOS; when I try to run() a command, I expect it to work like I've typed it into cmd.exe, and cmd.exe does pay attention to PATHEXT.

Copy link
Member

Choose a reason for hiding this comment

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

See my libuv link in the other comment thread

Copy link
Member

@stevengj stevengj Mar 22, 2018

Choose a reason for hiding this comment

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

@vtjnash means that we want to match the semantics of search_path in libuv's src/win/process.c, I guess, since this is what run uses.

Unfortunately, the search_path function is not exported. But since we use our own libuv fork anyway, maybe we should just modify it to export a uv_win_search_path function on Windows? But it doesn't look that complicated to implement libuv's semantics — it's actually somewhat simpler than what you are doing now.

Copy link
Member

Choose a reason for hiding this comment

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

Libuv says:

Furthermore, it tries to follow the semantics that cmd.exe, with this exception that PATHEXT environment variable isn't used. Since CreateProcess can start only .com and .exe files, only those extensions are tried. This behavior equals that of msvcrt's spawn functions.

Copy link
Member

@stevengj stevengj Mar 22, 2018

Choose a reason for hiding this comment

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

Note also some differences with what you are doing now:

  • If there's really only a filename, check the current directory for file, then search all path directories.
  • If filename specified has any extension, search for the file with the specified extension first.
  • If the literal filename is not found in a directory (even if the file already had an extension), try appending (not replacing) .com first and then .exe.

Copy link
Member

Choose a reason for hiding this comment

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

Note also that libuv writes CMD does not trim leading/trailing whitespace from path/pathex entries

base/sysinfo.jl Outdated
program_names = [program_name]
@static if iswindows()
if !isempty(splitext(program_name)[2])
pathexts = get(ENV, "PATHEXT", ".com; .exe; .bat; .cmd")
Copy link
Member

Choose a reason for hiding this comment

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

no spaces in the default

base/sysinfo.jl Outdated
@static if iswindows()
if !isempty(splitext(program_name)[2])
pathexts = get(ENV, "PATHEXT", ".com; .exe; .bat; .cmd")
pathexts = strip.(split(pathexts, ';'))
Copy link
Member

Choose a reason for hiding this comment

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

Is the strip necessary? Can PATHEXT contain whitespace?

@staticfloat
Copy link
Member Author

So looking through this, I guess we have to make the decision; do we want to behave like cmd here, or do something else? Since the most logical use of this function is to be used with run(), which I assume is going to eventually sub off to CreateProcess, we should probably ignore PATHEXT completely just like libuv does, and only search for .exe and .com files.

@stevengj
Copy link
Member

I think which("foo") should return the same program that run(`foo`) would execute, which means it needs to follow libuv.

base/sysinfo.jl Outdated
"""
function isexecutable(path::AbstractString)
@static if iswindows()
return true
Copy link
Member

Choose a reason for hiding this comment

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

isfile(path)?

base/sysinfo.jl Outdated
for program_name in program_names
program_path = joinpath(path_dir, program_name)
# If we find something that matches our name and we can execute
if isfile(program_path) && isexecutable(program_path)
Copy link
Member

Choose a reason for hiding this comment

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

If you change isexecutable to isfile on Windows as suggested above, then you can omit the isfile check here.

@staticfloat staticfloat changed the title Don't use which, just invoke curl directly Add Sys.which(), use that to find curl in download() Mar 30, 2018
@staticfloat
Copy link
Member Author

Okay, I think I've addressed all the comments so far.

base/sysinfo.jl Outdated
"""
function isexecutable(path::AbstractString)
@static if iswindows()
return isfile(path)
Copy link
Member

Choose a reason for hiding this comment

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

Can't you determine whether it has executable permissions using _access on Windows?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope. Windows doesn't have an X_OK bit. :(

https://msdn.microsoft.com/en-us/library/1w06ktdy.aspx

Copy link
Member

Choose a reason for hiding this comment

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

Ah okay, sorry for the noise then.

base/sysinfo.jl Outdated
# If this file does not even exist, fail out
if !isfile(program_name)
throw(ArgumentError("$program_name does not exist"))
end
Copy link
Member

Choose a reason for hiding this comment

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

this check is now redundant with isexecutable

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.

test/spawn.jl Outdated
end

mktempdir() do dir
pathsep = @static if Sys.iswindows() ";" else ":" end
Copy link
Member

Choose a reason for hiding this comment

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

A ternary would be less weird here

Copy link
Member Author

Choose a reason for hiding this comment

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

pathsep = Sys.iswindows() ? ";" : ":" is too much punctuation for me.

test/spawn.jl Outdated
bar_path = joinpath(dir, "bin1", "bar.exe")
touch(bar_path)
chmod(bar_path, 0o777)
cd("$(dir)") do
Copy link
Member

Choose a reason for hiding this comment

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

dir is already a String; it doesn't need to be interpolated.

test/spawn.jl Outdated

@static if Sys.iswindows()
# On windows, check that pwd() takes precedence, except when we provide a path
cd("$(dir)/bin2") do
Copy link
Member

Choose a reason for hiding this comment

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

joinpath(dir, "bin2")

base/sysinfo.jl Outdated
"""
Sys.isexecutable(path::String)

Returns `true` if the given `path` has executable permissions.
Copy link
Member

Choose a reason for hiding this comment

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

We word our docs in the imperative, i.e. this should be "return" rather than "returns." Same for the which docstring below.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hah, I was basing this off of the Sys.windows_version() docstring above it. I'll fix that too.

base/sysinfo.jl Outdated
@@ -23,7 +23,8 @@ export BINDIR,
isbsd,
islinux,
isunix,
iswindows
iswindows,
which
Copy link
Member

Choose a reason for hiding this comment

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

Maybe export isexecutable as well?

This acts as a julia-native version of the `which` command found on most
*nix systems; it searches the path for an executable of the given name,
returning the absolute path if it exists, and throwing an
`ArgumentError()` if it does not.
This fixes issues on systems that don't have `which` available
@staticfloat
Copy link
Member Author

This passes all CI now. Win32 build failure is unrelated, the spawn test passes, which is the only thing that matters for this PR.

Absent any other comments, I'm going to merge this tomorrow.

@staticfloat staticfloat merged commit 33fd977 into master May 22, 2018
@ararslan ararslan deleted the sf/very_small_rocks branch May 22, 2018 23:22
@ararslan
Copy link
Member

Probably should have been squash-merged.

@staticfloat
Copy link
Member Author

I intentionally didn't; each commit is intended to stand alone.

vtjnash pushed a commit that referenced this pull request Apr 6, 2021
As [discussed recently on discourse](https://discourse.julialang.org/t/weird-sys-which-function/58070), some programs require a certain executable name to function correctly, and behave badly if you call `realpath` to expand symbolic links (potentially changing the program name).

c.f. discussion in #26559, implemented `Sys.which`
ElOceanografo pushed a commit to ElOceanografo/julia that referenced this pull request May 4, 2021
As [discussed recently on discourse](https://discourse.julialang.org/t/weird-sys-which-function/58070), some programs require a certain executable name to function correctly, and behave badly if you call `realpath` to expand symbolic links (potentially changing the program name).

c.f. discussion in JuliaLang#26559, implemented `Sys.which`
antoine-levitt pushed a commit to antoine-levitt/julia that referenced this pull request May 9, 2021
As [discussed recently on discourse](https://discourse.julialang.org/t/weird-sys-which-function/58070), some programs require a certain executable name to function correctly, and behave badly if you call `realpath` to expand symbolic links (potentially changing the program name).

c.f. discussion in JuliaLang#26559, implemented `Sys.which`
johanmon pushed a commit to johanmon/julia that referenced this pull request Jul 5, 2021
As [discussed recently on discourse](https://discourse.julialang.org/t/weird-sys-which-function/58070), some programs require a certain executable name to function correctly, and behave badly if you call `realpath` to expand symbolic links (potentially changing the program name).

c.f. discussion in JuliaLang#26559, implemented `Sys.which`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants