One stupid shell scripting trick everyone should know

Posted on April 11, 2015 by Tommy McGuire
Labels: software development, rust, linux, random, unix, shell, programming language, tip

Let's say that I am installing Rust 1.0.0.beta, which comes as a pre-compiled binary package with an install script. Now, this is probably a good thing, because building rustc from source was slow and the rustc source doesn't include cargo, and thus probably other nifty things, but it does have a downside: if you use the --prefix argument to set a local installation directory, say in your home directory, you get an error message running rustc.


error while loading shared libraries: librustc_driver-4e7c5e5c.so: cannot open shared object file: No such file or directory

The rustc executable cannot find the shared libraries that it needs because their custom location is not built into the executable. Nor, if you didn't run install.sh as root, has their location been added to the system's search path; that is the point of the error message at the end of the installation.


install: running ldconfig
/sbin/ldconfig.real: Can't create temporary cache file /etc/ld.so.cache~: Permission denied
install: WARNING: failed to run ldconfig. this may happen when not installing as root. run with --verbose to see the error

So, what to do?

You could fix the ldconfig situation, but fooling with the system configuration irritates my lazy bump.

The obvious solution would be to set the LD_LIBRARY_PATH variable to include the Rust lib directory, but LD_LIBRARY_PATH has been the source of weird problems and frequently complicates debugging, so setting it globally or in your shell initialization script is an enterprise fraught with peril. But there is a simple trick that is handy for these sorts of situations.

First, in the Rust bin directory, move all of the executables to a new name, say rustc to rustc.bin, rustdoc to rustdoc.bin, etc. The cargo program doesn't have any links to the Rust libraries and the rust-gdb program is already a script, but doing this to all of them won't hurt anything.

Second, in the bin directory or any other location of your choice, create another short shell script.


#!/bin/sh

RUST_HOME=/home/mcguire/soft/rust

export LD_RUN_PATH=$RUST_HOME/lib

LD_LIBRARY_PATH=$RUST_HOME/lib exec $0.bin "$@"

I named it environment-script. Make sure it is marked as executable.

This script does a little more than necessary, but the basic idea should be clear: it sets the LD_LIBRARY_PATH environment variable in the last line to the location of the Rust libraries, then uses exec (a small optimization) to call another program ("$0.bin"), passing along all of the command line arguments ('"$@"'). I will explain the details here after the final step.

Third, in the rust bin directory next to the newly-named rustc.bin, create a symbolic link to the environment-script named with the original name of the Rust program. Do this for all of the Rust programs.


ln -s environment-script rustc
ln -s environment-script cargo
...

After doing this, you should be able to run rustc --version and get the version string without an error message.


$ rustc --version
rustc 1.0.0-beta (9854143cb 2015-04-02) (built 2015-04-02)

What is happening here? Running rustc invokes the environment-script, which uses the shell's syntax for setting the LD_LIBRARY_PATH variable for the execution of the exec command. The exec command takes the rest of its arguments and calls them, as a normal shell command. However, with exec, the process running the environment-script becomes the next command rather than running it in a sub-process. (Like I said, a small optimization.)

The program that gets run is given by $0.bin. $0 evaluates to the name the script was called as; in this case that will be "rustc". As a result, it invokes rustc.bin, the renamed executable. The script uses the shell syntax "$@" (the quotation marks are important) to pass any command line arguments along unchanged. In the end, rustc.bin is executed with an argument of --version and an environment setting necessary for it to find its shared libraries.

The LD_RUN_PATH environment variable is similar to LD_LIBRARY_PATH, except that its value should be added to the library search path compiled into any programs that rustc produces, allowing them to find the Rust libraries if they need them.

Tl;dr: Wrapper scripts! Learn them! Love them! Live them! Is there nothing they cannot do?

Comments



Is there a reason you used a wrapper script instead of setting rpath at compile time?

Anonymous
'2015-07-19T14:32:10.352-05:00'

Yes.

Oh, what is it?

Ok, there's two compile times here: for the rustc compiler itself, and for the programs built using rustc. For the first, I'm installing a pre-compiled binary so I have to use LD_LIBRARY_PATH (as in the script) to set the path for the compiler's use.

For the second, I am setting rpath using the LD_RUN_PATH environment variable. As a result, programs compiled with rustc include my weird, abnormal library path in their binaries.

Tommy McGuire
'2015-07-19T18:23:02.980-05:00'
active directory applied formal logic ashurbanipal authentication books c c++ comics conference continuations coq data structure digital humanities Dijkstra eclipse virgo electronics emacs goodreads haskell http java job Knuth ldap link linux lisp math naming nimrod notation OpenAM osgi parsing pony programming language protocols python quote quotes R random REST ruby rust SAML scala scheme shell software development system administration theory tip toy problems unix vmware yeti
Member of The Internet Defense League
Site proudly generated by Hakyll.