forked from lix-project/lix
Merge pull request #4618 from NixOS/ca/sign-drvoutputs
Sign the derivation outputs
This commit is contained in:
commit
a5e21aa13c
|
@ -925,6 +925,8 @@ void DerivationGoal::resolvedFinished() {
|
||||||
if (realisation) {
|
if (realisation) {
|
||||||
auto newRealisation = *realisation;
|
auto newRealisation = *realisation;
|
||||||
newRealisation.id = DrvOutput{initialOutputs.at(wantedOutput).outputHash, wantedOutput};
|
newRealisation.id = DrvOutput{initialOutputs.at(wantedOutput).outputHash, wantedOutput};
|
||||||
|
newRealisation.signatures.clear();
|
||||||
|
signRealisation(newRealisation);
|
||||||
worker.store.registerDrvOutput(newRealisation);
|
worker.store.registerDrvOutput(newRealisation);
|
||||||
} else {
|
} else {
|
||||||
// If we don't have a realisation, then it must mean that something
|
// If we don't have a realisation, then it must mean that something
|
||||||
|
|
|
@ -180,6 +180,9 @@ struct DerivationGoal : public Goal
|
||||||
/* Open a log file and a pipe to it. */
|
/* Open a log file and a pipe to it. */
|
||||||
Path openLogFile();
|
Path openLogFile();
|
||||||
|
|
||||||
|
/* Sign the newly built realisation if the store allows it */
|
||||||
|
virtual void signRealisation(Realisation&) {}
|
||||||
|
|
||||||
/* Close the log file. */
|
/* Close the log file. */
|
||||||
void closeLogFile();
|
void closeLogFile();
|
||||||
|
|
||||||
|
|
|
@ -2615,11 +2615,20 @@ void LocalDerivationGoal::registerOutputs()
|
||||||
but it's fine to do in all cases. */
|
but it's fine to do in all cases. */
|
||||||
|
|
||||||
if (settings.isExperimentalFeatureEnabled("ca-derivations")) {
|
if (settings.isExperimentalFeatureEnabled("ca-derivations")) {
|
||||||
for (auto& [outputName, newInfo] : infos)
|
for (auto& [outputName, newInfo] : infos) {
|
||||||
worker.store.registerDrvOutput(Realisation{
|
auto thisRealisation = Realisation{
|
||||||
.id = DrvOutput{initialOutputs.at(outputName).outputHash, outputName},
|
.id = DrvOutput{initialOutputs.at(outputName).outputHash,
|
||||||
.outPath = newInfo.path});
|
outputName},
|
||||||
|
.outPath = newInfo.path};
|
||||||
|
signRealisation(thisRealisation);
|
||||||
|
worker.store.registerDrvOutput(thisRealisation);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalDerivationGoal::signRealisation(Realisation & realisation)
|
||||||
|
{
|
||||||
|
getLocalStore().signRealisation(realisation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -161,6 +161,8 @@ struct LocalDerivationGoal : public DerivationGoal
|
||||||
as valid. */
|
as valid. */
|
||||||
void registerOutputs() override;
|
void registerOutputs() override;
|
||||||
|
|
||||||
|
void signRealisation(Realisation &) override;
|
||||||
|
|
||||||
/* Check that an output meets the requirements specified by the
|
/* Check that an output meets the requirements specified by the
|
||||||
'outputChecks' attribute (or the legacy
|
'outputChecks' attribute (or the legacy
|
||||||
'{allowed,disallowed}{References,Requisites}' attributes). */
|
'{allowed,disallowed}{References,Requisites}' attributes). */
|
||||||
|
|
|
@ -142,7 +142,7 @@ void PathSubstitutionGoal::tryNext()
|
||||||
/* Bail out early if this substituter lacks a valid
|
/* Bail out early if this substituter lacks a valid
|
||||||
signature. LocalStore::addToStore() also checks for this, but
|
signature. LocalStore::addToStore() also checks for this, but
|
||||||
only after we've downloaded the path. */
|
only after we've downloaded the path. */
|
||||||
if (!sub->isTrusted && worker.store.pathInfoIsTrusted(*info))
|
if (!sub->isTrusted && worker.store.pathInfoIsUntrusted(*info))
|
||||||
{
|
{
|
||||||
warn("substituter '%s' does not have a valid signature for path '%s'",
|
warn("substituter '%s' does not have a valid signature for path '%s'",
|
||||||
sub->getUri(), worker.store.printStorePath(storePath));
|
sub->getUri(), worker.store.printStorePath(storePath));
|
||||||
|
|
|
@ -6,6 +6,7 @@ create table if not exists Realisations (
|
||||||
drvPath text not null,
|
drvPath text not null,
|
||||||
outputName text not null, -- symbolic output id, usually "out"
|
outputName text not null, -- symbolic output id, usually "out"
|
||||||
outputPath integer not null,
|
outputPath integer not null,
|
||||||
|
signatures text, -- space-separated list
|
||||||
primary key (drvPath, outputName),
|
primary key (drvPath, outputName),
|
||||||
foreign key (outputPath) references ValidPaths(id) on delete cascade
|
foreign key (outputPath) references ValidPaths(id) on delete cascade
|
||||||
);
|
);
|
||||||
|
|
|
@ -310,13 +310,13 @@ LocalStore::LocalStore(const Params & params)
|
||||||
if (settings.isExperimentalFeatureEnabled("ca-derivations")) {
|
if (settings.isExperimentalFeatureEnabled("ca-derivations")) {
|
||||||
state->stmts->RegisterRealisedOutput.create(state->db,
|
state->stmts->RegisterRealisedOutput.create(state->db,
|
||||||
R"(
|
R"(
|
||||||
insert or replace into Realisations (drvPath, outputName, outputPath)
|
insert or replace into Realisations (drvPath, outputName, outputPath, signatures)
|
||||||
values (?, ?, (select id from ValidPaths where path = ?))
|
values (?, ?, (select id from ValidPaths where path = ?), ?)
|
||||||
;
|
;
|
||||||
)");
|
)");
|
||||||
state->stmts->QueryRealisedOutput.create(state->db,
|
state->stmts->QueryRealisedOutput.create(state->db,
|
||||||
R"(
|
R"(
|
||||||
select Output.path from Realisations
|
select Output.path, Realisations.signatures from Realisations
|
||||||
inner join ValidPaths as Output on Output.id = Realisations.outputPath
|
inner join ValidPaths as Output on Output.id = Realisations.outputPath
|
||||||
where drvPath = ? and outputName = ?
|
where drvPath = ? and outputName = ?
|
||||||
;
|
;
|
||||||
|
@ -652,6 +652,14 @@ void LocalStore::checkDerivationOutputs(const StorePath & drvPath, const Derivat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LocalStore::registerDrvOutput(const Realisation & info, CheckSigsFlag checkSigs)
|
||||||
|
{
|
||||||
|
settings.requireExperimentalFeature("ca-derivations");
|
||||||
|
if (checkSigs == NoCheckSigs || !realisationIsUntrusted(info))
|
||||||
|
registerDrvOutput(info);
|
||||||
|
else
|
||||||
|
throw Error("cannot register realisation '%s' because it lacks a valid signature", info.outPath.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
void LocalStore::registerDrvOutput(const Realisation & info)
|
void LocalStore::registerDrvOutput(const Realisation & info)
|
||||||
{
|
{
|
||||||
|
@ -662,6 +670,7 @@ void LocalStore::registerDrvOutput(const Realisation & info)
|
||||||
(info.id.strHash())
|
(info.id.strHash())
|
||||||
(info.id.outputName)
|
(info.id.outputName)
|
||||||
(printStorePath(info.outPath))
|
(printStorePath(info.outPath))
|
||||||
|
(concatStringsSep(" ", info.signatures))
|
||||||
.exec();
|
.exec();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1102,15 +1111,20 @@ const PublicKeys & LocalStore::getPublicKeys()
|
||||||
return *state->publicKeys;
|
return *state->publicKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LocalStore::pathInfoIsTrusted(const ValidPathInfo & info)
|
bool LocalStore::pathInfoIsUntrusted(const ValidPathInfo & info)
|
||||||
{
|
{
|
||||||
return requireSigs && !info.checkSignatures(*this, getPublicKeys());
|
return requireSigs && !info.checkSignatures(*this, getPublicKeys());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool LocalStore::realisationIsUntrusted(const Realisation & realisation)
|
||||||
|
{
|
||||||
|
return requireSigs && !realisation.checkSignatures(getPublicKeys());
|
||||||
|
}
|
||||||
|
|
||||||
void LocalStore::addToStore(const ValidPathInfo & info, Source & source,
|
void LocalStore::addToStore(const ValidPathInfo & info, Source & source,
|
||||||
RepairFlag repair, CheckSigsFlag checkSigs)
|
RepairFlag repair, CheckSigsFlag checkSigs)
|
||||||
{
|
{
|
||||||
if (checkSigs && pathInfoIsTrusted(info))
|
if (checkSigs && pathInfoIsUntrusted(info))
|
||||||
throw Error("cannot add path '%s' because it lacks a valid signature", printStorePath(info.path));
|
throw Error("cannot add path '%s' because it lacks a valid signature", printStorePath(info.path));
|
||||||
|
|
||||||
addTempRoot(info.path);
|
addTempRoot(info.path);
|
||||||
|
@ -1612,6 +1626,18 @@ void LocalStore::addSignatures(const StorePath & storePath, const StringSet & si
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void LocalStore::signRealisation(Realisation & realisation)
|
||||||
|
{
|
||||||
|
// FIXME: keep secret keys in memory.
|
||||||
|
|
||||||
|
auto secretKeyFiles = settings.secretKeyFiles;
|
||||||
|
|
||||||
|
for (auto & secretKeyFile : secretKeyFiles.get()) {
|
||||||
|
SecretKey secretKey(readFile(secretKeyFile));
|
||||||
|
realisation.sign(secretKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void LocalStore::signPathInfo(ValidPathInfo & info)
|
void LocalStore::signPathInfo(ValidPathInfo & info)
|
||||||
{
|
{
|
||||||
// FIXME: keep secret keys in memory.
|
// FIXME: keep secret keys in memory.
|
||||||
|
@ -1649,8 +1675,9 @@ std::optional<const Realisation> LocalStore::queryRealisation(
|
||||||
if (!use.next())
|
if (!use.next())
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
auto outputPath = parseStorePath(use.getStr(0));
|
auto outputPath = parseStorePath(use.getStr(0));
|
||||||
return Ret{
|
auto signatures = tokenizeString<StringSet>(use.getStr(1));
|
||||||
Realisation{.id = id, .outPath = outputPath}};
|
return Ret{Realisation{
|
||||||
|
.id = id, .outPath = outputPath, .signatures = signatures}};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} // namespace nix
|
} // namespace nix
|
||||||
|
|
|
@ -136,7 +136,8 @@ public:
|
||||||
void querySubstitutablePathInfos(const StorePathCAMap & paths,
|
void querySubstitutablePathInfos(const StorePathCAMap & paths,
|
||||||
SubstitutablePathInfos & infos) override;
|
SubstitutablePathInfos & infos) override;
|
||||||
|
|
||||||
bool pathInfoIsTrusted(const ValidPathInfo &) override;
|
bool pathInfoIsUntrusted(const ValidPathInfo &) override;
|
||||||
|
bool realisationIsUntrusted(const Realisation & ) override;
|
||||||
|
|
||||||
void addToStore(const ValidPathInfo & info, Source & source,
|
void addToStore(const ValidPathInfo & info, Source & source,
|
||||||
RepairFlag repair, CheckSigsFlag checkSigs) override;
|
RepairFlag repair, CheckSigsFlag checkSigs) override;
|
||||||
|
@ -202,6 +203,7 @@ public:
|
||||||
/* Register the store path 'output' as the output named 'outputName' of
|
/* Register the store path 'output' as the output named 'outputName' of
|
||||||
derivation 'deriver'. */
|
derivation 'deriver'. */
|
||||||
void registerDrvOutput(const Realisation & info) override;
|
void registerDrvOutput(const Realisation & info) override;
|
||||||
|
void registerDrvOutput(const Realisation & info, CheckSigsFlag checkSigs) override;
|
||||||
void cacheDrvOutputMapping(State & state, const uint64_t deriver, const string & outputName, const StorePath & output);
|
void cacheDrvOutputMapping(State & state, const uint64_t deriver, const string & outputName, const StorePath & output);
|
||||||
|
|
||||||
std::optional<const Realisation> queryRealisation(const DrvOutput&) override;
|
std::optional<const Realisation> queryRealisation(const DrvOutput&) override;
|
||||||
|
@ -272,9 +274,10 @@ private:
|
||||||
bool isValidPath_(State & state, const StorePath & path);
|
bool isValidPath_(State & state, const StorePath & path);
|
||||||
void queryReferrers(State & state, const StorePath & path, StorePathSet & referrers);
|
void queryReferrers(State & state, const StorePath & path, StorePathSet & referrers);
|
||||||
|
|
||||||
/* Add signatures to a ValidPathInfo using the secret keys
|
/* Add signatures to a ValidPathInfo or Realisation using the secret keys
|
||||||
specified by the ‘secret-key-files’ option. */
|
specified by the ‘secret-key-files’ option. */
|
||||||
void signPathInfo(ValidPathInfo & info);
|
void signPathInfo(ValidPathInfo & info);
|
||||||
|
void signRealisation(Realisation &);
|
||||||
|
|
||||||
Path getRealStoreDir() override { return realStoreDir; }
|
Path getRealStoreDir() override { return realStoreDir; }
|
||||||
|
|
||||||
|
|
|
@ -25,27 +25,69 @@ nlohmann::json Realisation::toJSON() const {
|
||||||
return nlohmann::json{
|
return nlohmann::json{
|
||||||
{"id", id.to_string()},
|
{"id", id.to_string()},
|
||||||
{"outPath", outPath.to_string()},
|
{"outPath", outPath.to_string()},
|
||||||
|
{"signatures", signatures},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Realisation Realisation::fromJSON(
|
Realisation Realisation::fromJSON(
|
||||||
const nlohmann::json& json,
|
const nlohmann::json& json,
|
||||||
const std::string& whence) {
|
const std::string& whence) {
|
||||||
auto getField = [&](std::string fieldName) -> std::string {
|
auto getOptionalField = [&](std::string fieldName) -> std::optional<std::string> {
|
||||||
auto fieldIterator = json.find(fieldName);
|
auto fieldIterator = json.find(fieldName);
|
||||||
if (fieldIterator == json.end())
|
if (fieldIterator == json.end())
|
||||||
|
return std::nullopt;
|
||||||
|
return *fieldIterator;
|
||||||
|
};
|
||||||
|
auto getField = [&](std::string fieldName) -> std::string {
|
||||||
|
if (auto field = getOptionalField(fieldName))
|
||||||
|
return *field;
|
||||||
|
else
|
||||||
throw Error(
|
throw Error(
|
||||||
"Drv output info file '%1%' is corrupt, missing field %2%",
|
"Drv output info file '%1%' is corrupt, missing field %2%",
|
||||||
whence, fieldName);
|
whence, fieldName);
|
||||||
return *fieldIterator;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StringSet signatures;
|
||||||
|
if (auto signaturesIterator = json.find("signatures"); signaturesIterator != json.end())
|
||||||
|
signatures.insert(signaturesIterator->begin(), signaturesIterator->end());
|
||||||
|
|
||||||
return Realisation{
|
return Realisation{
|
||||||
.id = DrvOutput::parse(getField("id")),
|
.id = DrvOutput::parse(getField("id")),
|
||||||
.outPath = StorePath(getField("outPath")),
|
.outPath = StorePath(getField("outPath")),
|
||||||
|
.signatures = signatures,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string Realisation::fingerprint() const
|
||||||
|
{
|
||||||
|
auto serialized = toJSON();
|
||||||
|
serialized.erase("signatures");
|
||||||
|
return serialized.dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Realisation::sign(const SecretKey & secretKey)
|
||||||
|
{
|
||||||
|
signatures.insert(secretKey.signDetached(fingerprint()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Realisation::checkSignature(const PublicKeys & publicKeys, const std::string & sig) const
|
||||||
|
{
|
||||||
|
return verifyDetached(fingerprint(), sig, publicKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Realisation::checkSignatures(const PublicKeys & publicKeys) const
|
||||||
|
{
|
||||||
|
// FIXME: Maybe we should return `maxSigs` if the realisation corresponds to
|
||||||
|
// an input-addressed one − because in that case the drv is enough to check
|
||||||
|
// it − but we can't know that here.
|
||||||
|
|
||||||
|
size_t good = 0;
|
||||||
|
for (auto & sig : signatures)
|
||||||
|
if (checkSignature(publicKeys, sig))
|
||||||
|
good++;
|
||||||
|
return good;
|
||||||
|
}
|
||||||
|
|
||||||
StorePath RealisedPath::path() const {
|
StorePath RealisedPath::path() const {
|
||||||
return std::visit([](auto && arg) { return arg.getPath(); }, raw);
|
return std::visit([](auto && arg) { return arg.getPath(); }, raw);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "path.hh"
|
#include "path.hh"
|
||||||
#include <nlohmann/json_fwd.hpp>
|
#include <nlohmann/json_fwd.hpp>
|
||||||
#include "comparator.hh"
|
#include "comparator.hh"
|
||||||
|
#include "crypto.hh"
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
|
||||||
|
@ -25,9 +26,16 @@ struct Realisation {
|
||||||
DrvOutput id;
|
DrvOutput id;
|
||||||
StorePath outPath;
|
StorePath outPath;
|
||||||
|
|
||||||
|
StringSet signatures;
|
||||||
|
|
||||||
nlohmann::json toJSON() const;
|
nlohmann::json toJSON() const;
|
||||||
static Realisation fromJSON(const nlohmann::json& json, const std::string& whence);
|
static Realisation fromJSON(const nlohmann::json& json, const std::string& whence);
|
||||||
|
|
||||||
|
std::string fingerprint() const;
|
||||||
|
void sign(const SecretKey &);
|
||||||
|
bool checkSignature(const PublicKeys & publicKeys, const std::string & sig) const;
|
||||||
|
size_t checkSignatures(const PublicKeys & publicKeys) const;
|
||||||
|
|
||||||
StorePath getPath() const { return outPath; }
|
StorePath getPath() const { return outPath; }
|
||||||
|
|
||||||
GENERATE_CMP(Realisation, me->id, me->outPath);
|
GENERATE_CMP(Realisation, me->id, me->outPath);
|
||||||
|
|
|
@ -798,7 +798,7 @@ std::map<StorePath, StorePath> copyPaths(ref<Store> srcStore, ref<Store> dstStor
|
||||||
auto pathsMap = copyPaths(srcStore, dstStore, storePaths, repair, checkSigs, substitute);
|
auto pathsMap = copyPaths(srcStore, dstStore, storePaths, repair, checkSigs, substitute);
|
||||||
try {
|
try {
|
||||||
for (auto & realisation : realisations) {
|
for (auto & realisation : realisations) {
|
||||||
dstStore->registerDrvOutput(realisation);
|
dstStore->registerDrvOutput(realisation, checkSigs);
|
||||||
}
|
}
|
||||||
} catch (MissingExperimentalFeature & e) {
|
} catch (MissingExperimentalFeature & e) {
|
||||||
// Don't fail if the remote doesn't support CA derivations is it might
|
// Don't fail if the remote doesn't support CA derivations is it might
|
||||||
|
|
|
@ -384,7 +384,12 @@ public:
|
||||||
we don't really want to add the dependencies listed in a nar info we
|
we don't really want to add the dependencies listed in a nar info we
|
||||||
don't trust anyyways.
|
don't trust anyyways.
|
||||||
*/
|
*/
|
||||||
virtual bool pathInfoIsTrusted(const ValidPathInfo &)
|
virtual bool pathInfoIsUntrusted(const ValidPathInfo &)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool realisationIsUntrusted(const Realisation & )
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -480,6 +485,8 @@ public:
|
||||||
*/
|
*/
|
||||||
virtual void registerDrvOutput(const Realisation & output)
|
virtual void registerDrvOutput(const Realisation & output)
|
||||||
{ unsupported("registerDrvOutput"); }
|
{ unsupported("registerDrvOutput"); }
|
||||||
|
virtual void registerDrvOutput(const Realisation & output, CheckSigsFlag checkSigs)
|
||||||
|
{ return registerDrvOutput(output); }
|
||||||
|
|
||||||
/* Write a NAR dump of a store path. */
|
/* Write a NAR dump of a store path. */
|
||||||
virtual void narFromPath(const StorePath & path, Sink & sink) = 0;
|
virtual void narFromPath(const StorePath & path, Sink & sink) = 0;
|
||||||
|
|
39
tests/ca/signatures.sh
Normal file
39
tests/ca/signatures.sh
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
source common.sh
|
||||||
|
|
||||||
|
# Globally enable the ca derivations experimental flag
|
||||||
|
sed -i 's/experimental-features = .*/& ca-derivations ca-references/' "$NIX_CONF_DIR/nix.conf"
|
||||||
|
|
||||||
|
clearStore
|
||||||
|
clearCache
|
||||||
|
|
||||||
|
nix-store --generate-binary-cache-key cache1.example.org $TEST_ROOT/sk1 $TEST_ROOT/pk1
|
||||||
|
pk1=$(cat $TEST_ROOT/pk1)
|
||||||
|
|
||||||
|
export REMOTE_STORE_DIR="$TEST_ROOT/remote_store"
|
||||||
|
export REMOTE_STORE="file://$REMOTE_STORE_DIR"
|
||||||
|
|
||||||
|
ensureCorrectlyCopied () {
|
||||||
|
attrPath="$1"
|
||||||
|
nix build --store "$REMOTE_STORE" --file ./content-addressed.nix "$attrPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
testOneCopy () {
|
||||||
|
clearStore
|
||||||
|
rm -rf "$REMOTE_STORE_DIR"
|
||||||
|
|
||||||
|
attrPath="$1"
|
||||||
|
nix copy --to $REMOTE_STORE "$attrPath" --file ./content-addressed.nix \
|
||||||
|
--secret-key-files "$TEST_ROOT/sk1"
|
||||||
|
|
||||||
|
ensureCorrectlyCopied "$attrPath"
|
||||||
|
|
||||||
|
# Ensure that we can copy back what we put in the store
|
||||||
|
clearStore
|
||||||
|
nix copy --from $REMOTE_STORE \
|
||||||
|
--file ./content-addressed.nix "$attrPath" \
|
||||||
|
--trusted-public-keys $pk1
|
||||||
|
}
|
||||||
|
|
||||||
|
for attrPath in rootCA dependentCA transitivelyDependentCA dependentNonCA dependentFixedOutput; do
|
||||||
|
testOneCopy "$attrPath"
|
||||||
|
done
|
|
@ -41,8 +41,9 @@ nix_tests = \
|
||||||
build.sh \
|
build.sh \
|
||||||
compute-levels.sh \
|
compute-levels.sh \
|
||||||
ca/build.sh \
|
ca/build.sh \
|
||||||
ca/nix-copy.sh \
|
|
||||||
ca/substitute.sh
|
ca/substitute.sh
|
||||||
|
ca/signatures.sh \
|
||||||
|
ca/nix-copy.sh
|
||||||
# parallel.sh
|
# parallel.sh
|
||||||
|
|
||||||
install-tests += $(foreach x, $(nix_tests), tests/$(x))
|
install-tests += $(foreach x, $(nix_tests), tests/$(x))
|
||||||
|
|
Loading…
Reference in a new issue