Copy files to the target directory after build - rust

Let's assume I have a game with the following directory structure:
/src
/resources
Cargo.toml
I would like cargo build to copy the files in the resources directory and paste them in the same directory as the executable file.
I know it is possible to do this using a custom build script, but this seems to be a common case that deserves special treatment. So the question is: does cargo provide a standard way of copying files to the target directory (using just Cargo.toml)?

No, it doesn't.
You can move files around with build scripts, but these are run before your crate is built because their sole purpose is to prepare the environment (e.g. compile C libraries and shims).
If you think this is an important feature, you can open a feature request in Cargo issue tracker.
Alternatively, you can write a makefile or a shell script which would forward all arguments to cargo and then copy the directory manually:
#!/bin/bash
DIR="$(dirname "$0")"
if cargo "$#"; then
[ -d "$DIR/target/debug" ] && cp -r "$DIR/resources" "$DIR/target/debug/resources"
[ -d "$DIR/target/release" ] && cp -r "$DIR/resources" "$DIR/target/release/resources"
fi
Now you can run cargo like
% ./make.sh build

I can't solve this for crates (as the accepted answer says) but for a "single" binary that needed a file to run correctly, this works for me.
use std::env;
use std::path::Path;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=config.json");
println!("cargo:warning=Hello from build.rs");
println!("cargo:warning=CWD is {:?}", env::current_dir().unwrap());
println!("cargo:warning=OUT_DIR is {:?}", env::var("OUT_DIR").unwrap());
println!("cargo:warning=CARGO_MANIFEST_DIR is {:?}", env::var("CARGO_MANIFEST_DIR").unwrap());
println!("cargo:warning=PROFILE is {:?}", env::var("PROFILE").unwrap());
let output_path = get_output_path();
println!("cargo:warning=Calculated build path: {}", output_path.to_str().unwrap());
let input_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("config.json");
let output_path = Path::new(&output_path).join("config.json");
let res = std::fs::copy(input_path, output_path);
println!("cargo:warning={:#?}",res)
}
fn get_output_path() -> PathBuf {
//<root or manifest path>/target/<profile>/
let manifest_dir_string = env::var("CARGO_MANIFEST_DIR").unwrap();
let build_type = env::var("PROFILE").unwrap();
let path = Path::new(&manifest_dir_string).join("target").join(build_type);
return PathBuf::from(path);
}
This is a mess and I'm very new to Rust. Improvements welcome.

Related

Use ~ in std::process::Command

I am trying to use std::process::Command to run webdrivers programmatically installed at
$HOME/.webdrivers. I don't want users to have to add the directory to their path, so I was hoping to be able to use something like
let geckodriver = Command::new("~/.webdrivers/geckodriver")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
But this doesn't seem to work. I'm getting the error for the executable not being there.
thread 'main' panicked at 'Could not start geckodriver: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/lib.rs:49:10
Anything helps!
Tilde expansion is a feature of the shell (bash, etc.) - the OS facilities that std::process::Command use do not expand tildes. So you will have to do it yourself.
Get the HOME envvar using std::env::var_os, convert it to a Path, then join your executable path onto that.
As stargateur mentioned ~ is expanded by the shell. bash is a shell that can be used to perform the expansion. Instead of spawning geckodriver directly spawn bash to spawn geckodriver
let geckodriver = Command::new("bash")
.arg("-c")
.arg("~/.webdrivers/geckodriver")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
Beware:
Packing the args into the string passed to Command::new("bash -c ~/.webdrivers/geckodriver") will not work since that will look for an executable with filename bash -c ~/.webdrivers/geckodriver (with spaces in the filename).

Rust not printing to terminal

rustc is not outputting anything to terminal when using println!.
Code:
fn main() {
println!("Hello, world!");
}
Running it:
me#mclaptop:~
> rustc helloworld.rs
me#mclaptop:~
>
Why does it not print anything?
rustc is the compiler of the Rust language, it just produces an executable to be run. If you want to actually see the output you must run the ./helloworld command.
You can read about it here.
rustc only compiles your code. You need to call the output binary to get it working.
Try ./helloworld or whatever the name of the output file is.
On Linux and Mac:
rustrc helloworld.rs && ./helloworld
You can simply use:
cargo run
This will compile current project and run it just with one command.

How to execute Rust code directly on Unix systems? (using the shebang)

From reading this thread, it looks like its possible to use the shebang to run Rust *.
#!/usr/bin/env rustc
fn main() {
println!("Hello World!");
}
Making this executable and running does compile, but not run the code.
chmod +x hello_world.rs
./hello_world.rs
However this only compiles the code into hello_world.
Can *.rs files be executed directly, similar to a shell script?
* This references rustx, I looked into this, but its a bash script which compiles the script every time (without caching) and never removes the file from the temp directory, although this could be improved. Also it has the significant limitation that it can't use crates.
There's cargo-script. That also lets you use dependencies.
After installing cargo-script via cargo install cargo-script, you can create your script file (hello.rs) like this:
#!/usr/bin/env run-cargo-script
fn main() {
println!("Hello World!");
}
To execute it, you need to:
$ chmod +x hello.rs
$ ./hello.rs
Compiling hello v0.1.0 (file://~/.cargo/.cargo/script-cache/file-hello-d746fc676c0590b)
Finished release [optimized] target(s) in 0.80 secs
Hello World!
To use crates from crates.io, please see the tutorial in the README linked above.
This seems to work:
#!/bin/sh
//usr/bin/env rustc $0 -o a.out && ./a.out && rm ./a.out ; exit
fn main() {
println!("Hello World!");
}
I have written a tool just for that: Scriptisto. It is a fully language agnostic tool and it works with other compiled languages or languages that have expensive validation steps (Python with mypy).
For Rust it can also fetch dependencies behind the scenes or build entirely in Docker without having a Rust compiler installed. scriptisto embeds those templates into the binary so you can bootstrap easily:
$ scriptisto new rust > ./script.rs
$ chmod +x ./script.rs
$ ./script.rs
Instead of new rust you can do new docker-rust and the build will not require Rust compiler on your host system.
#!/bin/sh
#![allow()] /*
exec cargo-play --cached --release $0 -- "$#"
*/
Needs cargo-play. You can see a solution that doesn't need anything here:
#!/bin/sh
#![allow()] /*
# rust self-compiler by Mahmoud Al-Qudsi, Copyright NeoSmart Technologies 2020
# See <https://neosmart.net/blog/self-compiling-rust-code/> for info & updates.
#
# This code is freely released to the public domain. In case a public domain
# license is insufficient for your legal department, this code is also licensed
# under the MIT license.
# Get an output path that is derived from the complete path to this self script.
# - `realpath` makes sure if you have two separate `script.rs` files in two
# different directories, they get mapped to different binaries.
# - `which` makes that work even if you store this script in $PATH and execute
# it by its filename alone.
# - `cut` is used to print only the hash and not the filename, which `md5sum`
# always includes in its output.
OUT=/tmp/$(printf "%s" $(realpath $(which "$0")) | md5sum | cut -d' ' -f1)
# Calculate hash of the current contents of the script, so we can avoid
# recompiling if it hasn't changed.
MD5=$(md5sum "$0" | cut -d' ' -f1)
# Check if we have a previously compiled output for this exact source code.
if !(test -f "${OUT}.md5" && test "${MD5}" = "$(cat ${OUT}.md5)"); then
# The script has been modified or is otherwise not cached.
# Check if the script already contains an `fn main()` entry point.
if grep -Eq '^\s*(\[.*?\])*\s*fn\s*main\b*' "$0"; then
# Compile the input script as-is to the previously determined location.
rustc "$0" -o ${OUT}
# Save rustc's exit code so we can compare against it later.
RUSTC_STATUS=$?
else
# The script does not contain an `fn main()` entry point, so add one.
# We don't use `printf 'fn main() { %s }' because the shebang must
# come at the beginning of the line, and we don't use `tail` to skip
# it because that would result in incorrect line numbers in any errors
# reported by rustc, instead we just comment out the shebang but leave
# it on the same line as `fn main() {`.
printf "fn main() {//%s\n}" "$(cat $0)" | rustc - -o ${OUT}
# Save rustc's exit code so we can compare against it later.
RUSTC_STATUS=$?
fi
# Check if we compiled the script OK, or exit bubbling up the return code.
if test "${RUSTC_STATUS}" -ne 0; then
exit ${RUSTC_STATUS}
fi
# Save the MD5 of the current version of the script so we can compare
# against it next time.
printf "%s" ${MD5} > ${OUT}.md5
fi
# Execute the compiled output. This also ends execution of the shell script,
# as it actually replaces its process with ours; see exec(3) for more on this.
exec ${OUT} $#
# At this point, it's OK to write raw rust code as the shell interpreter
# never gets this far. But we're actually still in the rust comment we opened
# on line 2, so close that: */

Is there a way to include multiple c-archive packages in a single binary

I'm trying to include multiple Go c-archive packages in a single C binary, but I'm getting multiple definition errors due to the full runtime being included in each c-archive.
I've tried putting multiple packages in the same c-archive but go build does not allow this.
I've also tried removing go.o from all the archives except one, but it seems my own Go code is also in that object file so that doesn't work, and it's even the reason I get multiple defines instead of the linker ignoring go.o from subsequent archives.
It would probably work to use c-shared instead of c-archive, but I don't wish to do that as I then have to put the shared libraries on my target machine, which is more complicated compared to just putting the final program binary there. I'd like everything to be statically linked if possible.
Is there a way to get this working? I can accept a linux only solution if that matters (some GNU ld trickery probably in that case).
Putting everything in a single Go package is not really an option, since it's a fairly large code base and there would be different programs wanting different parts. It would have to be an auto-generated package in that case.
Full steps to reproduce the problem:
cd $GOPATH/src
mkdir a b
cat > a/a.go <<EOT
package main
import "C"
//export a
func a() {
println("a")
}
func main() {}
EOT
cat > b/b.go <<EOT
package main
import "C"
//export b
func b() {
println("b")
}
func main() {}
EOT
cat > test.c <<EOT
#include "a.h"
#include "b.h"
int
main(int argc, char *argv[]) {
a();
b();
}
EOT
go build -buildmode=c-archive -o a.a a
go build -buildmode=c-archive -o b.a b
gcc test.c a.a b.a
I fumbled my way through this today after coming across your question.
The key is to define a single main package that imports the packages that you need and build them all together with a single "go install" command. I was unable to get this to work with "go build".
package main //import golib
import (
_ "golib/operations/bar"
_ "golib/foo"
)
func main() {}
go install -buildmode=c-archive golib
This will place your .a and .h files under a pkg/arch/golib directory. You can include the .h files as usual, but you only need to link against golib.a
aaron#aaron-laptop:~/code/pkg/linux_amd64$ ls
github.com golang.org golib golib.a
aaron#aaron-laptop:~/code/pkg/linux_amd64$ ls golib
foo.a foo.h operations
aaron#aaron-laptop:~/code/pkg/linux_amd64$ ls golib/operations
bar.a bar.h
Note that go will complain about unused packages if you omit the underscore in the import.

scons help for alternate build tools

I'm using a compiler for TI DSPs, so the default CC and LINK and AS tools make no sense. Below is an SConstruct file that works for me, I'm wondering if anyone has suggestions to make it better. Some problems:
I'd like to somehow tell it that my .obj files should go in a different directory than the source .c files. (it needs to know where, in order to figure out the SOURCES for the link step, and the dependencies for compile/linking) It would be nice to tie this in with the "-fr" and "-fs" arguments to the compiler, but I don't mind doing that manually.
There are some stock C files in the SConstruct file below, all start with a prefix of DSP2804x_. Right now scons can't figure out the dependencies for these, because I guess it's expecting the .obj files to live in the same directory, whereas my use of "-fr" and "-fs" for the compiler means those .obj files end up in the same directory as the SConstruct file. Is there a better way to do this? I'm guessing I should probably have a build step that copies these reference files into a local directory: if I change them, I want the changes to propagate to all projects that use them.
sigh....
env = Environment(
CC = 'C:/appl/ti/ccs/3.3/C2000/cgtools/bin/cl2000',
CCCOM = '$CC $CFLAGS $CCFLAGS $SOURCES',
CCFLAGS = Split('-g -q -pdr -d"_DEBUG" -d"LARGE_MODEL" -ml -mt -v28'),
LINKCOM = '$LINK $LINKFLAGS ${SOURCES.file} -o ${TARGET.base}.out',
LINK = 'C:/appl/ti/ccs/3.3/C2000/cgtools/bin/cl2000',
LINKFLAGS = Split('-z -q -c -ecode_start -stack0x200 -w -x'),
ASCOM = '$CC $CFLAGS $CCFLAGS $SOURCES',
#Bizarre but true. assembly is just like compiling C.
);
includes = {'CCFLAGS' : [
'-i../common/headers/include',
'-i../common/include',
'-fr.',
'-fs.'
]};
env.MergeFlags(includes);
links = {'LINKFLAGS' : [
'-m./Debug/Example_2804xGpioToggle.map',
'-i../common/headers/include',
'-iC:/appl/ti/ccs/3.3/C2000/xdais/lib',
'-iC:/appl/ti/ccs/3.3/C2000/cgtools/lib',
'-lrts2800_ml.lib',
'../common/cmd/28044_RAM_lnk.cmd',
'../common/headers/cmd/DSP2804x_Headers_nonBIOS.cmd'
]};
env.MergeFlags(links);
print "CCCOM is:", env['CCCOM'], "\n", env['LINKCOM'], '\n', env['ASCOM'];
env.Program('blink_gpio', [
'Example_2804xGpioToggle.c',
'../common/headers/source/DSP2804x_GlobalVariableDefs.c',
'../common/source/DSP2804x_CodeStartBranch.asm',
'../common/source/DSP2804x_DefaultIsr.c',
'../common/source/DSP2804x_PieCtrl.c',
'../common/source/DSP2804x_PieVect.c',
'../common/source/DSP2804x_SysCtrl.c'
]);
I solved both problems by doing a hierarchical build and using -fr=${TARGET.dir} in my compiler flags.

Resources