How can I make running a Cargo build script optional? - build

I have a Rust project that generates a dynamic (cdylib) library. The project uses a cbindgen build script to create an according C header file, matching the exported functions of the library. Cargo.toml looks like this:
[package]
name = "example"
version = "0.1.0"
authors = ["Me <me#foo.bar>"]
build = "build.rs"
[lib]
name = "example"
crate-type = ["cdylib"]
[dependencies]
[build-dependencies]
cbindgen = "0.6.2"
Unfortunately RLS (Rust Language Server) doesn't work very well when the build script is active which makes editing in VS Code rather unpleasant. Is there a way to make running the build script optional, having it disabled by default and only manually enable it when requested on the command-line (i.e. something like cargo build --release --enable-build-scripts)?

You can't conditionally disable build scripts or pass variables to them via cargo build, but you can make use of environment variables instead.
Inside your build.rs:
use std::env;
fn main() {
let build_enabled = env::var("BUILD_ENABLED")
.map(|v| v == "1")
.unwrap_or(true); // run by default
if build_enabled {
// do your build
}
}
Build with your build script:
BUILD_ENABLED=1 cargo build
Build without your build script:
BUILD_ENABLED=0 cargo build

To extend the answer from #PeterHall one can use a Cargo "features" section to pass information on to the build script.
Insert the following lines into Cargo.toml:
[features]
headers = []
Then check for environment variable CARGO_FEATURE_HEADERS in build.rs:
use std::env;
fn write_headers() {
// call cbindgen ...
}
fn main() {
let headers_enabled = env::var_os("CARGO_FEATURE_HEADERS").is_some();
if headers_enabled {
write_headers();
}
}
To make a release build run cargo build --features=headers --release.
Now this solution still compiles the build script and all cbindgen dependencies when RLS updates its status or when manually running cargo test. But cbindgen run-time errors do not hamper RLS anymore.

Related

How can I implement a command line option in Bazel to switch between which dependency version is used for building?

For some background, the C++ program I am working on has the possibility to interoperate with some other applications that use various Protobuf versions. In the source code for my program, I have the compiled .pb.cc files from these other applications for the Protobuf interface. These .pb.cc files were compiled with a particular version of Protobuf, and I don't have any control over this. I am using Bazel to build, and I want to be able to specify a Bazel build configuration for my program, which will use a particular version of Protobuf which matches that of one of the possible other applications.
Originally, I wanted to put something in the .bazelrc file so that I can specify a particular version of Protobuf depending on the config, for example:
# in .bazelrc:
build:my_config --protobuf_version=3_20_1
build:my_other_config --protobuf_version=3_21_6
Then from the terminal, I could build with the command
bazel build --config=my_config //path/to/target:target
which would build as if I had typed
bazel build --protobuf_version=3_20_1 //path/to/target:target
At this point, I wanted to use the select() function, as detailed in the Bazel docs for Configurable Build Attributes, to use a particular Protobuf version during building. But, the Protobuf dependencies are all specified in the WORKSPACE file, which is more limited than a BUILD file, and this select() function cannot be used there. So then my idea was to pull in every version of the Protobuf library that I would possibly need, and give them different names in the WORKSPACE file, and then in the BUILD files, use a select() function to choose the correct version. But, the Bazel rule for compiling the proto_library is used as such:
proto_library(
name = "foo",
srcs = ["foo.proto"],
strip_import_prefix = "/foo/bar/baz",
)
I don't see of any opportunity to use a select() function here to specify which Protobuf version's proto_library rule should be used. The proto_library rule is also defined in from the WORKSPACE file with:
load("#rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")
rules_proto_dependencies()
rules_proto_toolchains()
Now, I would say that I am stuck. I don't see a way to specify on the command line which version of Protobuf should be used with the proto_library rule.
In the end, I would like a way to do the equivalent in the WORKSPACE file of
# in WORKSPACE
if my_config:
# specific protobuf version:
http_archive(
name = "com_google_protobuf",
sha256 = "8b28fdd45bab62d15db232ec404248901842e5340299a57765e48abe8a80d930",
strip_prefix = "protobuf-3.20.1",
urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.20.1.tar.gz"],
)
elif my_other_config:
# same as above, but with different version
else:
# same as above, but with default version
According to some google groups discussion, this doesn't seem to be possible in the WORKSPACE file, so I would need to do it in a BUILD file, but the dependencies are specified in the WORKSPACE.
I figured out a way that works that seems to go against Bazel's philosophy, but most importantly does what I want.
The repository dependencies are loaded in the first of two steps, the first involving the WORKSPACE file, and the second involving the BUILD file. Command line flags for the build cannot be normally be directly passed to the WORKSPACE, but it is possible to get some information to the WORKSPACE by setting an environment variable and creating a repository_rule. In the WORKSPACE, this environment variable can be used, for example, to change the url argument to http_archive which specifies the dependency version.
This repository rule is created in a separate file .bzl file, which is then loaded in the WORKSPACE. As a generalized example of how get environment variable values into the WORKSPACE, the following file my_repository_rule.bzl could be created:
# in file my_repository_rule.bzl
def _my_repository_rule_impl(repository_ctx):
# read the particular environment variable we are interested in
config = repository_ctx.os.environ.get("MY_CONFIG_ENV_VAR", "")
# necessary to create empty BUILD file for this rule
# which will be located somewhere in the Bazel build files
repository_ctx.file("BUILD")
# some logic to do something based on the value of the environment variable passed in:
if config.lower() == "example_config_1":
ADDITIONAL_INFO = "foo"
elif config.lower() == "example_config_2":
ADDITIONAL_INFO = "bar"
else:
ADDITIONAL_INFO = "baz"
# create a temporary file called config.bzl to be loaded into WORKSPACE
# passing in any desired information from this rule implementation
repository_ctx.file("config.bzl", content = """
MY_CONFIG = {}
ADDITIONAL_INFO = {}
""".format(repr(config), repr(ADDITIONAL_INFO ))
)
my_repository_rule = repository_rule(
implementation=_my_repository_rule_impl,
environ = ["MY_CONFIG_ENV_VAR"]
)
This can be used in the WORKSPACE as such:
# in file WORKSPACE
load("//:my_repository_rule.bzl", "my_repository_rule ")
my_repository_rule(name = "local_my_repository_rule ")
load("#local_my_repository_rule //:config.bzl", "MY_CONFIG", "ADDITIONAL_INFO")
print("MY_CONFIG = {}".format(MY_CONFIG))
print("ADDITIONAL_INFO = {}".format(ADDITIONAL_INFO))
When a target is built with bazel build, the WORKSPACE will receive the value of the MY_CONFIG_ENV_VAR from the terminal and store it in the Starlark variable MY_CONFIG, and any other additional information determined in the implementation.
The environment variable can be passed by normal means, such as typing in a bash shell, for example:
MY_CONFIG_ENV_VAR=example_config_1 bazel build //path/to/target:target
It can also be passed as a flag with the --repo_env flag. This flag sends an extra environment variable to be available to the repository rules, meaning the following is equivalent:
bazel build --repo_env=MY_CONFIG_ENV_VAR=example_config_1 //path/to/target:target
This can be made easier to switch between by including the following in the .bazelrc file:
# in file .bazelrc
build:my_config_1 --repo_env=MY_CONFIG_ENV_VAR=example_config_1
build:my_config_2 --repo_env=MY_CONFIG_ENV_VAR=example_config_2
So running bazel build --config=my_config_1 //path/to/target:target will show the debug output from the print statements in WORKSPACE as the following:
MY_CONFIG = example_config_1
ADDITIONAL_INFO = foo
If ADDITIONAL_INFO in the rule implementation (in the file my_repository_rule.bzl) were set to a version number such as "3.20.1", then the WORKSPACE could, for example, use this in an http_archive call to pull the desired version of the dependency.
# in file WORKSPACE
if ADDITIONAL_INFO == "3.20.1":
sha256 = "8b28fdd45bab62d15db232ec404248901842e5340299a57765e48abe8a80d930"
http_archive(
name = "com_google_protobuf",
sha256 = sha256,
strip_prefix = "protobuf-{}".format(ADDITIONAL_INFO),
urls = ["https://github.com/protocolbuffers/protobuf/archive/v{}.tar.gz".format(ADDITIONAL_INFO)],
)
Of course, the value of the sha256 kwarg could also be passed in from the repository rule as a separate string variable, or as part of a dictionary, for example.

CLI app test fails if `cargo build` is not run first

I have a pure Rust CLI app that I'm unit testing, and if I run
cargo clean
cargo test --release
it fails to find the binary, but if I run
cargo clean
cargo build --release
cargo test --release
it works. Is there a way to tell cargo not to run the test until the binary app has been built?
Here's my test harness:
#[cfg(test)]
pub mod tests {
use std::process::Command;
use assert_cmd::prelude::{CommandCargoExt, OutputAssertExt};
use predicates::prelude::predicate;
#[test]
fn test_that_cli_app_produces_result() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("fastsim-cli")?;
let mut cyc_file = project_root::get_project_root().unwrap();
cyc_file.push("../fastsim/resources/cycles/udds.csv");
assert!(cyc_file.exists());
let mut veh_file = project_root::get_project_root().unwrap();
veh_file.push("../fastsim/resources/vehdb/2012_Ford_Fusion.yaml");
assert!(veh_file.exists());
cmd.args([
"--cyc-file",
cyc_file.to_str().unwrap(),
"--veh-file",
veh_file.to_str().unwrap(),
]);
cmd.assert()
.success()
.stdout(predicate::str::contains("32.4"));
Ok(())
}
}
From the #[cfg(test)] mod tests { I am guessing that this is code that exists inside of your library or binary source files. That is not the correct location for a test which needs to run a built binary. Instead, you should write a so-called “integration test” which are located in the tests/ directory next to src/, and are separate compilation targets (so that they can be built after everything else). Cargo specifically provides for testing binaries via integration tests:
Binary targets are automatically built if there is an integration test. This allows an integration test to execute the binary to exercise and test its behavior. The CARGO_BIN_EXE_<name> environment variable is set when the integration test is built so that it can use the env macro to locate the executable.
If you're already using an integration test, then something else has gone wrong that isn't obvious from your description, because Cargo should be building the binary, as per the documentation.

Statically linking a C library with a Haskell library

I have a Haskell project that aims to create some C++ bindings. I've written the C wrappers and compiled them into a stand-alone statically linked library.
I'd like to write the Haskell bindings to link statically to the C wrappers so that I don't have to distribute the C wrappers separately but I can't seem to get it working and would appreciate some help.
I specify the C library as an extra library but my cabal build step doesn't seem to add it to compile command.
I've created a small project to illustrate this (http://github.com/deech/CPlusPlusBindings).
It contains a small C++ class (https://github.com/deech/CPlusPlusBindings/tree/master/cpp-src), the C wrapper (https://github.com/deech/CPlusPlusBindings/tree/master/c-src), a working C test routine (https://github.com/deech/CPlusPlusBindings/tree/master/c-test) and the Haskell file (https://github.com/deech/CPlusPlusBindings/blob/master/src/BindingTest.chs).
The C library is added in Setup.hs not in the Cabal file because that's how I have it my real project which builds the C library using "make" through Cabal just before the build stepf. I have verified that at the build step the extraLibs part of BuildInfo contains the library name and extraLibDirs contains the right directory.
The output of my cabal build is:
creating dist/setup
./dist/setup/setup build --verbose=2
creating dist/build
creating dist/build/autogen
Building CPlusPlusBinding-0.1.0.0...
Preprocessing library CPlusPlusBinding-0.1.0.0...
Building library...
creating dist/build
/usr/local/bin/ghc --make -fbuilding-cabal-package -O -odir dist/build -hidir dist/build -stubdir dist/build -i -idist/build -isrc -idist/build/autogen -Idist/build/autogen -Idist/build -I/home/deech/Old/Haskell/CPlusPlusBinding/c-src -I/home/deech/Old/Haskell/CPlusPlusBinding/cpp-includes -optP-include -optPdist/build/autogen/cabal_macros.h -package-name CPlusPlusBinding-0.1.0.0 -hide-all-packages -package-db dist/package.conf.inplace -package-id base-4.6.0.1-8aa5d403c45ea59dcd2c39f123e27d57 -XHaskell98 -XForeignFunctionInterface BindingTest
Linking...
/usr/bin/ar -r dist/build/libHSCPlusPlusBinding-0.1.0.0.a dist/build/BindingTest.o
/usr/bin/ar: creating dist/build/libHSCPlusPlusBinding-0.1.0.0.a
/usr/bin/ld -x --hash-size=31 --reduce-memory-overheads -r -o dist/build/HSCPlusPlusBinding-0.1.0.0.o dist/build/BindingTest.o
In-place registering CPlusPlusBinding-0.1.0.0...
/usr/local/bin/ghc-pkg update - --global --user --package-db=dist/package.conf.inplace
Unfortunately neither the compilation nor the linking step uses the C library. There are no other warnings or errors.
To solve this problem I had to:
re-link the Haskell library with the C bindings' object files and
use the ghc-options tag in my Cabal file to make sure they linked in the right order.
All the changes are in the test project (http://github.com/deech/CPlusPlusBindings).
Below the process of creating a new archive that includes both the C and Haskell objects is explained in detail and it is not simple. The complexity occurs because there is no way (as of Cabal 1.16.0.2) to hook into the linker part of the build process.
Setting the flags in the Cabal file is trivial so it is not described here.
Relinking The Haskell Library
Set the build type to custom by adding:
build-type: custom
to the cabal file.
Insert customized build logic by replacing the main method in Setup.hs with:
main = defaultMainWithHooks simpleUserHooks {
buildHook = myBuildHook,
...
}
This tells the build process that instead of going with the default build process defined in simpleUserHooks it should use the myBuildHook function which is defined below. Similarly the clean up process is overridden with the custom function myCleanHook.
Define the build hook. This build hook will run make on the command line to build the C++, and C portions and then use the C object files when creating linking the Haskell bindings.
We start off myBuildHook:
myBuildHook pkg_descr local_bld_info user_hooks bld_flags = do
by first running make with no arguments:
rawSystemExit normal "make" []
Then add the locations of the header files and library directories and the library itself to the PackageDescription record and update the LocalBuildInfo with the new package description:
let new_pkg_descr = (addLib . addLibDirs . addIncludeDirs $ pkg_descr)
new_local_bld_info = local_bld_info {localPkgDescr = new_pkg_descr}
Before the buildHook fired the configureHook stored the order of compilation in the compBuildOrder (component build order) key of the LocalBuildInfo record. We need to isolate the building of the library so we separate the library building and executable building parts of the build process.
The build order is just a list and we know the build component is a library if it's just a plain CLibName type constructor so we isolate those elements from the list and update the LocalBuildInfo record with only them:
let (libs, nonlibs) = partition
(\c -> case c of
CLibName -> True
_ -> False)
(compBuildOrder new_local_bld_info)
lib_lbi = new_local_bld_info {compBuildOrder = libs}
Now we run the default build hook with the updated records:
buildHook simpleUserHooks new_pkg_descr lib_lbi user_hooks bld_flags
Once it's done building an archive has been created but we have to re-create it to include the C objects generated by the make command in step 1. So we grab some settings and a list of the C object file paths:
let verbosity = fromFlag (buildVerbosity bld_flags)
info verbosity "Relinking archive ..."
let pref = buildDir local_bld_info
verbosity = fromFlag (buildVerbosity bld_flags)
cobjs <- getLibDirContents >>= return . map (\f -> combine clibdir f)
. filter (\f -> takeExtension f == ".o")
And then hand it off to withComponentsLBI which acts on each component of the build. In this case since we're only dealing with the library part there is only one component. Cabal provides getHaskellObjects for getting a list of the Haskell object files and createArLibArchive for creating an archive so we can re-run the linker:
withComponentsLBI pkg_descr local_bld_info $ \comp clbi ->
case comp of
(CLib lib) -> do
hobjs <- getHaskellObjects lib local_bld_info pref objExtension True
let staticObjectFiles = hobjs ++ cobjs
(arProg, _) <- requireProgram verbosity arProgram (withPrograms local_bld_info)
let pkgid = packageId pkg_descr
vanillaLibFilePath = pref </> mkLibName pkgid
Ar.createArLibArchive verbosity arProg vanillaLibFilePath staticObjectFiles
_ -> return ()
The default buildHook which was run in Step 4 created a temporary package database file named "package.conf.inplace" which holds the description of the library that was built so that executable can link against it without the library needing to be installed to the default system package file. Unfortunately every buildHook run blanks it out so we need to hold on to a temporary copy:
let distPref = fromFlag (buildDistPref bld_flags)
dbFile = distPref </> "package.conf.inplace"
(tempFilePath, tempFileHandle) <- openTempFile distPref "package.conf"
hClose tempFileHandle
copyFile dbFile tempFilePath
Now we store a path to that copy into the LocalBuildInfo structure along with the executable parts of the build process which were filtered out in Step 3.
let exe_lbi = new_local_bld_info {
withPackageDB = withPackageDB
new_local_bld_info ++
[SpecificPackageDB tempFilePath],
compBuildOrder = nonlibs
}
and store the path again in the extraTmpFiles part of the PackageDescription so it can be removed by the default clean up hook.
exe_pkg_descr = new_pkg_descr {extraTmpFiles = extraTmpFiles new_pkg_descr ++ [tempFilePath]}
Now we finally run the default buildHook again with the updated records (which now know about the new archive) on just the executable components:
buildHook simpleUserHooks exe_pkg_descr exe_lbi user_hooks bld_flags

aggregating gradle multiproject test results using TestReport

I have a project structure that looks like the below. I want to use the TestReport functionality in Gradle to aggregate all the test results to a single directory.
Then I can access all the test results through a single index.html file for ALL subprojects.
How can I accomplish this?
.
|--ProjectA
|--src/test/...
|--build
|--reports
|--tests
|--index.html (testresults)
|--..
|--..
|--ProjectB
|--src/test/...
|--build
|--reports
|--tests
|--index.html (testresults)
|--..
|--..
From Example 4. Creating a unit test report for subprojects in the Gradle User Guide:
subprojects {
apply plugin: 'java'
// Disable the test report for the individual test task
test {
reports.html.enabled = false
}
}
task testReport(type: TestReport) {
destinationDir = file("$buildDir/reports/allTests")
// Include the results from the `test` task in all subprojects
reportOn subprojects*.test
}
Fully working sample is available from samples/testing/testReport in the full Gradle distribution.
In addition to the subprojects block and testReport task suggested by #peter-niederwieser above, I would add another line to the build below those:
tasks('test').finalizedBy(testReport)
That way if you run gradle test (or even gradle build), the testReport task will run after the subproject tests complete. Note that you have to use tasks('test') rather than just test.finalizedBy(...) because the test task doesn't exist in the root project.
If using kotlin Gradle DSL
val testReport = tasks.register<TestReport>("testReport") {
destinationDir = file("$buildDir/reports/tests/test")
reportOn(subprojects.map { it.tasks.findByPath("test") })
subprojects {
tasks.withType<Test> {
useJUnitPlatform()
finalizedBy(testReport)
ignoreFailures = true
testLogging {
events("passed", "skipped", "failed")
}
}
}
And execute gradle testReport. Source How to generate an aggregated test report for all Gradle subprojects
I am posting updated answer on this topic. I am using Gradle 7.5.1.
TestReport task
In short I'm using following script to set up test aggregation form subprojects (based on #Peter's answer):
subprojects {
apply plugin: 'java'
}
task testReport(type: TestReport) {
destinationDir = file("$buildDir/reports/allTests")
// Include the results from the `test` task in all subprojects
testResults.from = subprojects*.test
}
Note that reportOn method is "deprecated" or will be soon and replaced with testResults, while at the same time testResults is still incubating as of 7.5.1.
I got following warning in IDE
The TestReport.reportOn(Object...) method has been deprecated. This is scheduled to be removed in Gradle 8.0.
Hint: subproject*.test is example of star dot notation in groovy that invokes test task on a list of subprojects. Equally would be invocation of subprojects.collect{it.test}
TestReport#reportOn (Gradle API documentation)
TestReport#testResults (Gradle API documentation)
reportOn replacement for gradle 8 (Gradle Forum)
test-report-aggregation plugin
There is also alternative option for aggregating tests (Since Gradle 7.4). One can apply test-report-aggregation plugin.
If your projects already apply java plugin, this means they will come with jvm-test-suite, all you have to do is apply the plugin.
plugins {
id 'test-report-aggregation'
}
Then you will be able to invoke test reports through testSuiteAggregateTestReport task. Personally didn't use the plugin, but I think it makes sense to use it if you have multiple test suites configured with jvm-test-suite.
Example project can be found in https://github.com/gradle-samples/Aggregating-test-results-using-a-standalone-utility-project-Groovy
For 'connectedAndroidTest's there is a approach published by google.(https://developer.android.com/studio/test/command-line.html#RunTestsDevice (Multi-module reports section))
Add the 'android-reporting' Plugin to your projects build.gradle.
apply plugin: 'android-reporting'
Execute the android tests with additional 'mergeAndroidReports' argument. It will merge all test results of the project modules into one report.
./gradlew connectedAndroidTest mergeAndroidReports
FYI, I've solved this problem using the following subprojects config in my root project build.gradle file. This way no extra tasks are needed.
Note: this places each module's output in its own reports/<module_name> folder, so subproject builds don't overwrite each other's results.
subprojects {
// Combine all build results
java {
reporting.baseDir = "${rootProject.buildDir.path}/reports/${project.name}"
}
}
For a default Gradle project, this would result in a folder structure like
build/reports/module_a/tests/test/index.html
build/reports/module_b/tests/test/index.html
build/reports/module_c/tests/test/index.html

scons - how to run something /after/ all targets have been built

I've recently picked up scons to implement a multi-platform build framework for a medium sized C++ project. The build generates a bunch of unit-tests which should be invoked at the end of it all. How does one achieve that sort of thing?
For example in my top level sconstruct, I have
subdirs=['list', 'of', 'my', 'subprojects']
for subdir in subdirs:
SConscript(dirs=subdir, exports='env', name='sconscript',
variant_dir=subdir+os.sep+'build'+os.sep+mode, duplicate=0)
Each of the subdir has its unit-tests, however, since there are dependencies between the dlls and executables built inside them - i want to hold the running of tests until all the subdirs have been built and installed (I mean, using env.Install).
Where should I write the loop to iterate through the built tests and execute them? I tried putting it just after this loop - but since scons doesn't let you control the order of execution - it gets executed well before I want it to.
Please help a scons newbie. :)
thanks,
SCons, like Make, uses a declarative method to solving the build problem. You don't want to tell SCons how to do its job. You want to document all the dependencies and then let SCons solve how it builds everything.
If something is being executed before something else, you need to create and hook up the dependencies.
If you want to create dmy touch files, you can create a custom builder like:
import time
def action(target, source, env):
os.system('echo here I am running other build')
dmy_fh = open('dmy_file','w')
dmy_fh.write( 'Dummy dependency file created at %4d.%02d.%02d %02dh%02dm%02ds\n'%time.localtime()[0:6])
dmy_fh.close()
bldr = Builder(action=action)
env.Append( BUILDERS = {'SubBuild' : bldr } )
env.SubBuild(srcs,tgts)
It is very important to put the timestamp into the dummy file, because scons uses md5 hashes. If you have an empty file, the md5 will always be the same and it may decide to not do subsequent build steps. If you need to generate different tweaks on a basic command, you can use function factories to modify a template. e.g.
def gen_a_echo_cmd_func(echo_str):
def cmd_func(target,source,env):
cmd = 'echo %s'%echo_str
print cmd
os.system(cmd)
return cmd_fun
bldr = Builder(action = gen_a_echo_cmd_func('hi'))
env.Append(BUILDERS = {'Hi': bldr})
env.Hi(srcs,tgts)
bldr = Builder(action = gen_a_echo_cmd_func('bye'))
env.Append(BUILDERS = {'Bye': bldr})
env.Bye(srcs,tgts)
If you have something that you want to automatically inject into the scons build flow ( e.g. something that compresses all your build log files after everything else has run ), see my question here.
The solution should be as simple as this.
Make the result of the Test builders depend on the result of the Install builder
In pseudo:
test = Test(dlls)
result = Install(dlls)
Depends(test,result)
The best way would be if the Test builder actually worked out the dll dependencies for you, but there may be all kinds of reasons it doesn't do that.
In terms of dependencies, what you want is for all the test actions to depend on all the program-built actions. A way of doing this is to create and export a dummy-target to all the subdirectories' sconscript files, and in the sconscript files, make the dummy-target Depends on the main targets, and have the test targets Depends on the dummy-target.
I'm having a bit of trouble figuring out how to set up the dummy target, but this basically works:
(in top-level SConstruct)
dummy = env.Command('.all_built', 'SConstruct', 'echo Targets built. > $TARGET')
Export('dummy')
(in each sub-directory's SConscript)
Import('dummy')
for target in target_list:
Depends(dummy, targe)
for test in test_list:
Depends(test, dummy)
I'm sure further refinements are possible, but maybe this'll get you started.
EDIT: also worth pointing out this page on the subject.
Just have each SConscript return a value on which you will build dependencies.
SConscript file:
test = debug_environment.Program('myTest', src_files)
Return('test')
SConstruct file:
dep1 = SConscript([...])
dep2 = SConscript([...])
Depends(dep1, dep2)
Now dep1 build will complete after dep2 build has completed.