push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

11
pkgs/pkgs-lib/default.nix Normal file
View File

@@ -0,0 +1,11 @@
# pkgs-lib is for functions and values that can't be in lib because
# they depend on some packages. This notably is *not* for supporting package
# building, instead pkgs/build-support is the place for that.
{ lib, pkgs }:
{
# setting format types and generators. These do not fit in lib/types.nix,
# because they depend on pkgs for rendering some formats
formats = import ./formats.nix {
inherit lib pkgs;
};
}

1128
pkgs/pkgs-lib/formats.nix Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,182 @@
{
lib,
pkgs,
}:
let
inherit (pkgs) buildPackages callPackage;
hocon-generator = buildPackages.rustPlatform.buildRustPackage {
name = "hocon-generator";
version = "0.1.0";
src = ./src;
passthru.updateScript = ./update.sh;
cargoLock.lockFile = ./src/Cargo.lock;
};
hocon-validator =
pkgs.writers.writePython3Bin "hocon-validator"
{
libraries = [ pkgs.python3Packages.pyhocon ];
}
''
from sys import argv
from pyhocon import ConfigFactory
if not len(argv) == 2:
print("USAGE: hocon-validator <file>")
ConfigFactory.parse_file(argv[1])
'';
in
{
format =
{
generator ? hocon-generator,
validator ? hocon-validator,
doCheck ? true,
}:
let
hoconLib = {
mkInclude =
value:
let
includeStatement =
if lib.isAttrs value && !(lib.isDerivation value) then
{
required = false;
type = null;
_type = "include";
}
// value
else
{
value = toString value;
required = false;
type = null;
_type = "include";
};
in
assert lib.assertMsg
(lib.elem includeStatement.type [
"file"
"url"
"classpath"
null
])
''
Type of HOCON mkInclude is not of type 'file', 'url' or 'classpath':
${(lib.generators.toPretty { }) includeStatement}
'';
includeStatement;
mkAppend = value: {
inherit value;
_type = "append";
};
mkSubstitution =
value:
if lib.isString value then
{
inherit value;
optional = false;
_type = "substitution";
}
else
assert lib.assertMsg (lib.isAttrs value) ''
Value of invalid type provided to `hocon.lib.mkSubstitution`: ${lib.typeOf value}
'';
assert lib.assertMsg (value ? "value") ''
Argument to `hocon.lib.mkSubstitution` is missing a `value`:
${builtins.toJSON value}
'';
{
value = value.value;
optional = value.optional or false;
_type = "substitution";
};
};
in
{
type =
let
type' =
with lib.types;
let
atomType = nullOr (oneOf [
bool
float
int
path
str
]);
includeType = addCheck attrs (x: (x._type or null) == "include");
in
(oneOf [
atomType
(addCheck (listOf atomType) (lib.all atomType.check))
(addCheck (listOf includeType) (lib.all includeType.check))
(attrsOf type')
])
// {
description = "HOCON value";
};
in
type';
lib = hoconLib;
generate =
name: value:
callPackage
(
{
stdenvNoCC,
hocon-generator,
hocon-validator,
writeText,
}:
stdenvNoCC.mkDerivation rec {
inherit name;
dontUnpack = true;
preferLocalBuild = true;
json = builtins.toJSON value;
passAsFile = [ "json" ];
strictDeps = true;
nativeBuildInputs = [ hocon-generator ];
buildPhase = ''
runHook preBuild
hocon-generator < $jsonPath > output.conf
runHook postBuild
'';
inherit doCheck;
nativeCheckInputs = [ hocon-validator ];
checkPhase = ''
runHook preCheck
hocon-validator output.conf
runHook postCheck
'';
installPhase = ''
runHook preInstall
mv output.conf $out
runHook postInstall
'';
passthru.json = writeText "${name}.json" json;
}
)
{
hocon-generator = generator;
hocon-validator = validator;
};
};
}

View File

@@ -0,0 +1 @@
target

View File

@@ -0,0 +1,89 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "hocon-generator"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "proc-macro2"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "serde"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

View File

@@ -0,0 +1,10 @@
[package]
name = "hocon-generator"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1.0.178"
serde_json = "1.0.104"

View File

@@ -0,0 +1,243 @@
use serde_json::{value, Map, Value};
#[derive(Debug)]
enum HOCONValue {
Null,
Append(Box<HOCONValue>),
Bool(bool),
Number(value::Number),
String(String),
List(Vec<HOCONValue>),
Substitution(String, bool),
Object(Vec<HOCONInclude>, Vec<(String, HOCONValue)>),
Literal(String),
}
#[derive(Debug)]
enum HOCONInclude {
Heuristic(String, bool),
Url(String, bool),
File(String, bool),
ClassPath(String, bool),
}
impl HOCONInclude {
fn map_fst(&self, f: &dyn Fn(&String) -> String) -> HOCONInclude {
match self {
HOCONInclude::Heuristic(s, r) => HOCONInclude::Heuristic(f(s), *r),
HOCONInclude::Url(s, r) => HOCONInclude::Url(f(s), *r),
HOCONInclude::File(s, r) => HOCONInclude::File(f(s), *r),
HOCONInclude::ClassPath(s, r) => HOCONInclude::ClassPath(f(s), *r),
}
}
}
fn parse_include(o: &Map<String, Value>) -> HOCONInclude {
let value = o
.get("value")
.expect("Missing field 'value' for include statement")
.as_str()
.expect("Field 'value' is not a string in include statement")
.to_string();
let required = o
.get("required")
.expect("Missing field 'required' for include statement")
.as_bool()
.expect("Field 'required'is not a bool in include statement");
let include_type = match o
.get("type")
.expect("Missing field 'type' for include statement")
{
Value::Null => None,
Value::String(s) => Some(s.as_str()),
t => panic!("Field 'type' is not a string in include statement: {:?}", t),
};
// Assert that this was an intentional include
debug_assert!(o.get("_type").and_then(|t| t.as_str()) == Some("include"));
match include_type {
None => HOCONInclude::Heuristic(value, required),
Some("url") => HOCONInclude::Url(value, required),
Some("file") => HOCONInclude::File(value, required),
Some("classpath") => HOCONInclude::ClassPath(value, required),
_ => panic!(
"Could not recognize type for include statement: {}",
include_type.unwrap()
),
}
}
fn parse_special_types(o: &Map<String, Value>) -> Option<HOCONValue> {
o.get("_type")
.and_then(|r#type| r#type.as_str())
.map(|r#type| match r#type {
"substitution" => {
let value = o
.get("value")
.expect("Missing value for substitution")
.as_str()
.unwrap_or_else(|| panic!("Substitution value is not a string: {:?}", o));
let required = o
.get("required")
.unwrap_or(&Value::Bool(false))
.as_bool()
.unwrap_or_else(|| panic!("Substitution value is not a string: {:?}", o));
debug_assert!(!value.contains('}'));
HOCONValue::Substitution(value.to_string(), required)
}
"append" => {
let value = o.get("value").expect("Missing value for append");
HOCONValue::Append(Box::new(json_to_hocon(value)))
}
"unquoted_string" => {
let value = o
.get("value")
.expect("Missing value for unquoted_string")
.as_str()
.unwrap_or_else(|| panic!("Unquoted string value is not a string: {:?}", o));
HOCONValue::Literal(value.to_string())
}
_ => panic!(
"\
Attribute set contained special element '_type',\
but its value is not recognized:\n{}",
r#type
),
})
}
fn json_to_hocon(v: &Value) -> HOCONValue {
match v {
Value::Null => HOCONValue::Null,
Value::Bool(b) => HOCONValue::Bool(*b),
Value::Number(n) => HOCONValue::Number(n.clone()),
Value::String(s) => HOCONValue::String(s.clone()),
Value::Array(a) => {
let items = a.iter().map(json_to_hocon).collect::<Vec<HOCONValue>>();
HOCONValue::List(items)
}
Value::Object(o) => {
if let Some(result) = parse_special_types(o) {
return result;
}
let mut items = o
.iter()
.filter(|(key, _)| key.as_str() != "_includes")
.map(|(key, value)| (key.clone(), json_to_hocon(value)))
.collect::<Vec<(String, HOCONValue)>>();
items.sort_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap());
let includes = o
.get("_includes")
.map(|x| {
x.as_array()
.expect("_includes is not an array")
.iter()
.map(|x| {
x.as_object()
.unwrap_or_else(|| panic!("Include is not an object: {}", x))
})
.map(parse_include)
.collect::<Vec<HOCONInclude>>()
})
.unwrap_or(vec![]);
HOCONValue::Object(includes, items)
}
}
}
impl ToString for HOCONValue {
fn to_string(&self) -> String {
match self {
HOCONValue::Null => "null".to_string(),
HOCONValue::Bool(b) => b.to_string(),
HOCONValue::Number(n) => n.to_string(),
HOCONValue::String(s) => serde_json::to_string(&Value::String(s.clone())).unwrap(),
HOCONValue::Substitution(v, required) => {
format!("${{{}{}}}", if *required { "" } else { "?" }, v)
}
HOCONValue::List(l) => {
let items = l
.iter()
.map(|item| item.to_string())
.collect::<Vec<String>>()
.join(",\n")
.split('\n')
.map(|s| " ".to_owned() + s)
.collect::<Vec<String>>()
.join("\n");
format!("[\n{}\n]", items)
}
HOCONValue::Object(i, o) => {
let includes = i
.iter()
.map(|x| {
x.map_fst(&|s| serde_json::to_string(&Value::String(s.clone())).unwrap())
})
.map(|x| match x {
HOCONInclude::Heuristic(s, r) => (s.to_string(), r),
HOCONInclude::Url(s, r) => (format!("url({})", s), r),
HOCONInclude::File(s, r) => (format!("file({})", s), r),
HOCONInclude::ClassPath(s, r) => (format!("classpath({})", s), r),
})
.map(|(i, r)| if r { format!("required({})", i) } else { i })
.map(|s| format!("include {}", s))
.collect::<Vec<String>>()
.join("\n");
let items = o
.iter()
.map(|(key, value)| {
(
serde_json::to_string(&Value::String(key.clone())).unwrap(),
value,
)
})
.map(|(key, value)| match value {
HOCONValue::Append(v) => format!("{} += {}", key, v.to_string()),
v => format!("{} = {}", key, v.to_string()),
})
.collect::<Vec<String>>()
.join("\n");
let content = (if includes.is_empty() {
items
} else {
format!("{}\n{}", includes, items)
})
.split('\n')
.map(|s| {
if s.is_empty() {
"".to_string()
} else {
format!(" {}", s)
}
})
.collect::<Vec<String>>()
.join("\n");
format!("{{\n{}\n}}", content)
}
HOCONValue::Append(_) => panic!("Append should not be present at this point"),
Self::Literal(s) => s.to_string(),
}
}
}
fn main() {
let stdin = std::io::stdin().lock();
let json = serde_json::Deserializer::from_reader(stdin)
.into_iter::<Value>()
.next()
.expect("Could not read content from stdin")
.expect("Could not parse JSON from stdin");
print!("{}\n\n", json_to_hocon(&json).to_string());
}

View File

@@ -0,0 +1,101 @@
{
lib,
formats,
stdenvNoCC,
writeText,
...
}:
let
hocon = formats.hocon { };
include_file =
(writeText "hocon-test-include.conf" ''
"val" = 1
'').overrideAttrs
(
_: _: {
outputHashAlgo = "sha256";
outputHashMode = "flat";
outputHash = "sha256-UhkJLhT3bD6znq+IdDjs/ahP19mLzrLCy/R14pVrfew=";
}
);
expression = {
simple_top_level_attr = "1.0";
nested.attrset.has.a.integer.value = 100;
some_floaty = 29.95;
array2d = [
[
1
2
"a"
]
[
2
1
"b"
]
];
nasty_string = "\"@\n\\\t^*bf\n0\";'''$";
"misc attrs" = {
x = 1;
y = hocon.lib.mkAppend { a = 1; };
};
"cursed \" .attrs \" " = {
"a" = 1;
"a b" = hocon.lib.mkSubstitution "a";
"a b c" = hocon.lib.mkSubstitution {
value = "a b";
required = false;
};
};
to_include = {
_includes = [
(hocon.lib.mkInclude include_file)
(hocon.lib.mkInclude "https://example.com")
(hocon.lib.mkInclude {
required = true;
type = "file";
value = include_file;
})
(hocon.lib.mkInclude { value = include_file; })
(hocon.lib.mkInclude {
value = "https://example.com";
type = "url";
})
];
};
};
hocon-test-conf = hocon.generate "hocon-test.conf" expression;
in
stdenvNoCC.mkDerivation {
name = "pkgs.formats.hocon-test-comprehensive";
dontUnpack = true;
dontBuild = true;
doCheck = true;
checkPhase = ''
runHook preCheck
diff -U3 ${./expected.txt} ${hocon-test-conf}
runHook postCheck
'';
installPhase = ''
runHook preInstall
mkdir $out
cp ${./expected.txt} $out/expected.txt
cp ${hocon-test-conf} $out/hocon-test.conf
cp ${hocon-test-conf.passthru.json} $out/hocon-test.json
runHook postInstall
'';
}

View File

@@ -0,0 +1,48 @@
{
"array2d" = [
[
1,
2,
"a"
],
[
2,
1,
"b"
]
]
"cursed \" .attrs \" " = {
"a" = 1
"a b" = ${?a}
"a b c" = ${?a b}
}
"misc attrs" = {
"x" = 1
"y" += {
"a" = 1
}
}
"nasty_string" = "\"@\n\\\t^*bf\n0\";'''$"
"nested" = {
"attrset" = {
"has" = {
"a" = {
"integer" = {
"value" = 100
}
}
}
}
}
"simple_top_level_attr" = "1.0"
"some_floaty" = 29.95
"to_include" = {
include "/nix/store/ccnzr53dpipdacxgci3ii3bqacvb5hxm-hocon-test-include.conf"
include "https://example.com"
include required(file("/nix/store/ccnzr53dpipdacxgci3ii3bqacvb5hxm-hocon-test-include.conf"))
include "/nix/store/ccnzr53dpipdacxgci3ii3bqacvb5hxm-hocon-test-include.conf"
include url("https://example.com")
}
}

View File

@@ -0,0 +1,4 @@
{ pkgs, ... }:
{
comprehensive = pkgs.callPackage ./comprehensive { };
}

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env nix-shell
#!nix-shell -p cargo -i bash
cd "$(dirname "$0")"
cargo update

View File

@@ -0,0 +1,153 @@
{ lib, pkgs }:
let
inherit (lib) types;
inherit (types)
attrsOf
oneOf
coercedTo
str
bool
int
float
package
;
in
{
javaProperties =
{
comment ? "Generated with Nix",
boolToString ? lib.boolToString,
}:
{
# Design note:
# A nested representation of inevitably leads to bad UX:
# 1. keys like "a.b" must be disallowed, or
# the addition of options in a freeformType module
# become breaking changes
# 2. adding a value for "a" after "a"."b" was already
# defined leads to a somewhat hard to understand
# Nix error, because that's not something you can
# do with attrset syntax. Workaround: "a"."", but
# that's too little too late. Another workaround:
# mkMerge [ { a = ...; } { a.b = ...; } ].
#
# Choosing a non-nested representation does mean that
# we sacrifice the ability to override at the (conceptual)
# hierarchical levels, _if_ an application exhibits those.
#
# Some apps just use periods instead of spaces in an odd
# mix of attempted categorization and natural language,
# with no meaningful hierarchy.
#
# We _can_ choose to support hierarchical config files
# via nested attrsets, but the module author should
# make sure that problem (2) does not occur.
type =
let
elemType =
oneOf [
# `package` isn't generalized to `path` because path values
# are ambiguous. Are they host path strings (toString /foo/bar)
# or should they be added to the store? ("${/foo/bar}")
# The user must decide.
(coercedTo package toString str)
(coercedTo bool boolToString str)
(coercedTo int toString str)
(coercedTo float toString str)
]
// {
description = "string, package, bool, int or float";
};
in
attrsOf elemType;
generate =
name: value:
pkgs.runCommand name
{
# Requirements
# ============
#
# 1. Strings in Nix carry over to the same
# strings in Java => need proper escapes
# 2. Generate files quickly
# - A JVM would have to match the app's
# JVM to avoid build closure bloat
# - Even then, JVM startup would slow
# down config generation.
#
#
# Implementation
# ==============
#
# Escaping has two steps
#
# 1. jq
# Escape known separators, in order not
# to break up the keys and values.
# This handles typical whitespace correctly,
# but may produce garbage for other control
# characters.
#
# 2. iconv
# Escape >ascii code points to java escapes,
# as .properties files are supposed to be
# encoded in ISO 8859-1. It's an old format.
# UTF-8 behavior may exist in some apps and
# libraries, but we can't rely on this in
# general.
preferLocalBuild = true;
passAsFile = [ "value" ];
value = builtins.toJSON value;
nativeBuildInputs = [
pkgs.jq
pkgs.libiconvReal
];
jqCode =
let
main = ''
to_entries
| .[]
| "\(
.key
| ${commonEscapes}
| gsub(" "; "\\ ")
| gsub("="; "\\=")
) = \(
.value
| ${commonEscapes}
| gsub("^ "; "\\ ")
| gsub("\\n "; "\n\\ ")
)"
'';
# Most escapes are equal for both keys and values.
commonEscapes = ''
gsub("\\\\"; "\\\\")
| gsub("\\n"; "\\n\\\n")
| gsub("#"; "\\#")
| gsub("!"; "\\!")
| gsub("\\t"; "\\t")
| gsub("\r"; "\\r")
'';
in
main;
inputEncoding = "UTF-8";
inherit comment;
}
''
(
echo "$comment" | while read -r ln; do echo "# $ln"; done
echo
jq -r --arg hash '#' "$jqCode" "$valuePath" \
| iconv --from-code "$inputEncoding" --to-code JAVA \
) > "$out"
'';
};
}

View File

@@ -0,0 +1,27 @@
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
import java.util.SortedSet;
import java.util.TreeSet;
class Main {
public static void main (String args[]) {
try {
InputStream input = new FileInputStream(args[0]);
Properties prop = new Properties();
prop.load(input);
SortedSet<String> keySet = new TreeSet(prop.keySet());
for (String key : keySet) {
System.out.println("KEY");
System.out.println(key);
System.out.println("VALUE");
System.out.println(prop.get(key));
System.out.println("");
}
} catch (Exception e) {
e.printStackTrace();
System.err.println(e.toString());
System.exit(1);
}
}
}

View File

@@ -0,0 +1,90 @@
{
formats,
glibcLocales,
jdk,
lib,
stdenv,
}:
# This test primarily tests correct escaping.
# See also testJavaProperties in
# pkgs/pkgs-lib/tests/formats.nix, which tests
# type coercions and is a bit easier to read.
let
inherit (lib) concatStrings attrValues mapAttrs;
javaProperties = formats.javaProperties { };
input = {
foo = "bar";
"empty value" = "";
"typical.dot.syntax" = "com.sun.awt";
"" = "empty key's value";
"1" = "2 3";
"#" = "not a comment # still not";
"!" = "not a comment!";
"!a" = "still not! a comment";
"!b" = "still not ! a comment";
"dos paths" = "C:\\Program Files\\Nix For Windows\\nix.exe";
"a \t\nb" = " c";
"angry \t\nkey" = ''
multi
${"\tline\r"}
space-
indented
trailing-space${" "}
trailing-space${" "}
value
'';
"this=not" = "bad";
"nor = this" = "bad";
"all stuff" = "foo = bar";
"unicode big brain" = "e = mc";
"ütf-8" = "dûh";
# NB: Some editors (vscode) show this _whole_ line in right-to-left order
"الجبر" = "أكثر من مجرد أرقام";
};
in
stdenv.mkDerivation {
name = "pkgs.formats.javaProperties-test-${jdk.name}";
nativeBuildInputs = [
jdk
glibcLocales
];
# technically should go through the type.merge first, but that's tested
# in tests/formats.nix.
properties = javaProperties.generate "example.properties" input;
# Expected output as printed by Main.java
passAsFile = [ "expected" ];
expected = concatStrings (
attrValues (
mapAttrs (key: value: ''
KEY
${key}
VALUE
${value}
'') input
)
);
src = lib.sourceByRegex ./. [
".*\\.java"
];
# On Linux, this can be C.UTF-8, but darwin + zulu requires en_US.UTF-8
LANG = "en_US.UTF-8";
buildPhase = ''
javac Main.java
'';
doCheck = true;
checkPhase = ''
cat -v $properties
java Main $properties >actual
diff -U3 $expectedPath actual
'';
installPhase = "touch $out";
}

View File

@@ -0,0 +1,128 @@
{
lib,
pkgs,
}:
let
inherit (pkgs) buildPackages callPackage;
libconfig-generator = buildPackages.rustPlatform.buildRustPackage {
name = "libconfig-generator";
version = "0.1.0";
src = ./src;
passthru.updateScript = ./update.sh;
cargoLock.lockFile = ./src/Cargo.lock;
};
libconfig-validator =
buildPackages.runCommandCC "libconfig-validator"
{
buildInputs = with buildPackages; [ libconfig ];
}
''
mkdir -p "$out/bin"
$CC -lconfig -x c - -o "$out/bin/libconfig-validator" ${./validator.c}
'';
in
{
format =
{
generator ? libconfig-generator,
validator ? libconfig-validator,
}:
{
inherit generator;
type =
with lib.types;
let
valueType =
(oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
])
// {
description = "libconfig value";
};
in
attrsOf valueType;
lib = {
mkHex = value: {
_type = "hex";
inherit value;
};
mkOctal = value: {
_type = "octal";
inherit value;
};
mkFloat = value: {
_type = "float";
inherit value;
};
mkArray = value: {
_type = "array";
inherit value;
};
mkList = value: {
_type = "list";
inherit value;
};
};
generate =
name: value:
callPackage
(
{
stdenvNoCC,
libconfig-generator,
libconfig-validator,
writeText,
}:
stdenvNoCC.mkDerivation rec {
inherit name;
dontUnpack = true;
preferLocalBuild = true;
json = builtins.toJSON value;
passAsFile = [ "json" ];
strictDeps = true;
nativeBuildInputs = [ libconfig-generator ];
buildPhase = ''
runHook preBuild
libconfig-generator < $jsonPath > output.cfg
runHook postBuild
'';
doCheck = true;
nativeCheckInputs = [ libconfig-validator ];
checkPhase = ''
runHook preCheck
libconfig-validator output.cfg
runHook postCheck
'';
installPhase = ''
runHook preInstall
mv output.cfg $out
runHook postInstall
'';
passthru.json = writeText "${name}.json" json;
}
)
{
libconfig-generator = generator;
libconfig-validator = validator;
};
};
}

View File

@@ -0,0 +1,40 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "libconfig-generator"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "serde"
version = "1.0.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c"
[[package]]
name = "serde_json"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
dependencies = [
"itoa",
"ryu",
"serde",
]

View File

@@ -0,0 +1,10 @@
[package]
name = "libconfig-generator"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1.0.178"
serde_json = "1.0.104"

View File

@@ -0,0 +1,271 @@
use serde_json::Value;
use std::mem::discriminant;
#[derive(Debug)]
enum LibConfigIntNumber {
Oct(i64),
Hex(i64),
Int(i64),
}
#[derive(Debug)]
enum LibConfigValue {
Bool(bool),
Int(LibConfigIntNumber),
Float(f64),
String(String),
Array(Vec<LibConfigValue>),
List(Vec<LibConfigValue>),
Group(Vec<String>, Vec<(String, LibConfigValue)>),
}
fn validate_setting_name(key: &str) -> bool {
let first_char = key.chars().next().expect("Empty setting name");
(first_char.is_alphabetic() || first_char == '*')
&& key[1..]
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '*' || c == '-')
}
const SPECIAL_TYPES: [&str; 5] = ["octal", "hex", "float", "list", "array"];
fn object_is_special_type(o: &serde_json::Map<String, Value>) -> Option<&str> {
o.get("_type").and_then(|x| x.as_str()).and_then(|x| {
if SPECIAL_TYPES.contains(&x) {
Some(x)
} else {
None
}
})
}
fn vec_is_array(v: &Vec<LibConfigValue>) -> bool {
if v.is_empty() {
return true;
}
let first_item = v.first().unwrap();
if match first_item {
LibConfigValue::Array(_) => true,
LibConfigValue::List(_) => true,
LibConfigValue::Group(_, _) => true,
_ => false,
} {
return false;
};
v[1..]
.iter()
.all(|item| discriminant(first_item) == discriminant(item))
}
fn json_to_libconfig(v: &Value) -> LibConfigValue {
match v {
Value::Null => panic!("Null value not allowed in libconfig"),
Value::Bool(b) => LibConfigValue::Bool(b.clone()),
Value::Number(n) => {
if n.is_i64() {
LibConfigValue::Int(LibConfigIntNumber::Int(n.as_i64().unwrap()))
} else if n.is_f64() {
LibConfigValue::Float(n.as_f64().unwrap())
} else {
panic!("{} is not i64 or f64, cannot be represented as number in libconfig", n);
}
}
Value::String(s) => LibConfigValue::String(s.to_string()),
Value::Array(a) => {
let items = a
.iter()
.map(|item| json_to_libconfig(item))
.collect::<Vec<LibConfigValue>>();
LibConfigValue::List(items)
}
Value::Object(o) => {
if let Some(_type) = object_is_special_type(o) {
let value = o
.get("value")
.expect(format!("Missing value for special type: {}", &_type).as_str());
return match _type {
"octal" => {
let str_value = value
.as_str()
.expect(
format!("Value is not a string for special type: {}", &_type)
.as_str(),
)
.to_owned();
LibConfigValue::Int(LibConfigIntNumber::Oct(
i64::from_str_radix(&str_value, 8)
.expect(format!("Invalid octal value: {}", value).as_str()),
))
}
"hex" => {
let str_value = value
.as_str()
.expect(
format!("Value is not a string for special type: {}", &_type)
.as_str(),
)
.to_owned();
LibConfigValue::Int(LibConfigIntNumber::Hex(
i64::from_str_radix(&str_value[2..], 16)
.expect(format!("Invalid hex value: {}", value).as_str()),
))
}
"float" => {
let str_value = value
.as_str()
.expect(
format!("Value is not a string for special type: {}", &_type)
.as_str(),
)
.to_owned();
LibConfigValue::Float(
str_value
.parse::<f64>()
.expect(format!("Invalid float value: {}", value).as_str()),
)
}
"list" => {
let items = value
.as_array()
.expect(
format!("Value is not an array for special type: {}", &_type)
.as_str(),
)
.to_owned()
.iter()
.map(|item| json_to_libconfig(item))
.collect::<Vec<LibConfigValue>>();
LibConfigValue::List(items)
}
"array" => {
let items = value
.as_array()
.expect(
format!("Value is not an array for special type: {}", &_type)
.as_str(),
)
.to_owned()
.iter()
.map(|item| json_to_libconfig(item))
.collect::<Vec<LibConfigValue>>();
if !vec_is_array(&items) {
panic!(
"This can not be an array because of its contents: {:#?}",
items
);
}
LibConfigValue::Array(items)
}
_ => panic!("Invalid type: {}", _type),
};
}
let mut items = o
.iter()
.filter(|(key, _)| key.as_str() != "_includes")
.map(|(key, value)| (key.clone(), json_to_libconfig(value)))
.collect::<Vec<(String, LibConfigValue)>>();
items.sort_by(|(a,_),(b,_)| a.partial_cmp(b).unwrap());
let includes = o
.get("_includes")
.map(|x| {
x.as_array()
.expect("_includes is not an array")
.iter()
.map(|x| {
x.as_str()
.expect("_includes item is not a string")
.to_owned()
})
.collect::<Vec<String>>()
})
.unwrap_or(vec![]);
for (key,_) in items.iter() {
if !validate_setting_name(key) {
panic!("Invalid setting name: {}", key);
}
}
LibConfigValue::Group(includes, items)
}
}
}
impl ToString for LibConfigValue {
fn to_string(&self) -> String {
match self {
LibConfigValue::Bool(b) => b.to_string(),
LibConfigValue::Int(i) => match i {
LibConfigIntNumber::Oct(n) => format!("0{:o}", n),
LibConfigIntNumber::Hex(n) => format!("0x{:x}", n),
LibConfigIntNumber::Int(n) => n.to_string(),
},
LibConfigValue::Float(n) => format!("{:?}", n),
LibConfigValue::String(s) => {
format!("\"{}\"", s.replace("\\", "\\\\").replace("\"", "\\\""))
}
LibConfigValue::Array(a) => {
let items = a
.iter()
.map(|item| item.to_string())
.collect::<Vec<String>>()
.join(", ");
format!("[{}]", items)
}
LibConfigValue::List(a) => {
let items = a
.iter()
.map(|item| item.to_string())
.collect::<Vec<String>>()
.join(", ");
format!("({})", items)
}
LibConfigValue::Group(i, o) => {
let includes = i
.iter()
.map(|x| x.replace("\\", "\\\\").replace("\"", "\\\""))
.map(|x| format!("@include \"{}\"", x))
.collect::<Vec<String>>()
.join("\n");
let items = o
.iter()
.map(|(key, value)| format!("{}={};", key, value.to_string()))
.collect::<Vec<String>>()
.join("");
if includes.is_empty() {
format!("{{{}}}", items)
} else {
format!("{{\n{}\n{}}}", includes, items)
}
}
}
}
}
fn main() {
let stdin = std::io::stdin().lock();
let json = serde_json::Deserializer::from_reader(stdin)
.into_iter::<Value>()
.next()
.expect("Could not read content from stdin")
.expect("Could not parse JSON from stdin");
for (key, value) in json
.as_object()
.expect("Top level of JSON file is not an object")
{
print!("{}={};", key, json_to_libconfig(value).to_string());
}
print!("\n\n");
}

View File

@@ -0,0 +1,111 @@
{
lib,
formats,
stdenvNoCC,
writeText,
...
}:
let
libconfig = formats.libconfig { };
include_expr = {
val = 1;
};
include_file = writeText "libconfig-test-include" ''
val=1;
'';
expression = {
simple_top_level_attr = "1.0";
nested.attrset.has.a.integer.value = 100;
some_floaty = 29.95;
## dashes in key names
top-level-dash = "pass";
nested.level-dash = "pass";
## Same syntax here on these two, but they should get serialized differently:
# > A list may have zero or more elements, each of which can be a scalar value, an array, a group, or another list.
list1d = libconfig.lib.mkList [
1
"mixed!"
5
2
];
# You might also omit the mkList, as a list will be a list (in contrast to an array) by default.
list2d = [
1
[
1
1.2
"foo"
]
[
"bar"
1.2
1
]
];
# > An array may have zero or more elements, but the elements must all be scalar values of the same type.
array1d = libconfig.lib.mkArray [
1
5
2
];
array2d = [
(libconfig.lib.mkArray [
1
2
])
(libconfig.lib.mkArray [
2
1
])
];
nasty_string = "\"@\n\\\t^*bf\n0\";'''$";
weirderTypes = {
_includes = [ include_file ];
pi = 3.141592654;
bigint = 9223372036854775807;
hex = libconfig.lib.mkHex "0x1FC3";
octal = libconfig.lib.mkOctal "0027";
float = libconfig.lib.mkFloat "1.2E-3";
array_of_ints = libconfig.lib.mkArray [
(libconfig.lib.mkOctal "0732")
(libconfig.lib.mkHex "0xA3")
1234
];
list_of_weird_types = [
3.141592654
9223372036854775807
(libconfig.lib.mkHex "0x1FC3")
(libconfig.lib.mkOctal "0027")
(libconfig.lib.mkFloat "1.2E-32")
(libconfig.lib.mkFloat "1")
];
};
};
libconfig-test-cfg = libconfig.generate "libconfig-test.cfg" expression;
in
stdenvNoCC.mkDerivation {
name = "pkgs.formats.libconfig-test-comprehensive";
dontUnpack = true;
dontBuild = true;
doCheck = true;
checkPhase = ''
cp ${./expected.txt} expected.txt
substituteInPlace expected.txt \
--subst-var-by include_file "${include_file}"
diff -U3 ./expected.txt ${libconfig-test-cfg}
'';
installPhase = ''
mkdir $out
cp expected.txt $out
cp ${libconfig-test-cfg} $out/libconfig-test.cfg
cp ${libconfig-test-cfg.passthru.json} $out/libconfig-test.json
'';
}

View File

@@ -0,0 +1,6 @@
array1d=[1, 5, 2];array2d=([1, 2], [2, 1]);list1d=(1, "mixed!", 5, 2);list2d=(1, (1, 1.2, "foo"), ("bar", 1.2, 1));nasty_string="\"@
\\ ^*bf
0\";'''$";nested={attrset={has={a={integer={value=100;};};};};level-dash="pass";};simple_top_level_attr="1.0";some_floaty=29.95;top-level-dash="pass";weirderTypes={
@include "@include_file@"
array_of_ints=[0732, 0xa3, 1234];bigint=9223372036854775807;float=0.0012;hex=0x1fc3;list_of_weird_types=(3.141592654, 9223372036854775807, 0x1fc3, 027, 1.2e-32, 1.0);octal=027;pi=3.141592654;};

View File

@@ -0,0 +1,4 @@
{ pkgs, ... }:
{
comprehensive = pkgs.callPackage ./comprehensive { };
}

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env nix-shell
#!nix-shell -p cargo -i bash
cd "$(dirname "$0")"
cargo update

View File

@@ -0,0 +1,21 @@
// Copyright (C) 2005-2023 Mark A Lindner, ckie
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <stdio.h>
#include <libconfig.h>
int main(int argc, char **argv)
{
config_t cfg;
config_init(&cfg);
if (argc != 2)
{
fprintf(stderr, "USAGE: validator <path-to-validate>");
}
if(! config_read_file(&cfg, argv[1]))
{
fprintf(stderr, "[libconfig] %s:%d - %s\n", config_error_file(&cfg),
config_error_line(&cfg), config_error_text(&cfg));
config_destroy(&cfg);
return 1;
}
printf("[libconfig] validation ok\n");
}

View File

@@ -0,0 +1,97 @@
{ pkgs, lib }:
{
# Format for defining configuration of some PHP services, that use "include 'config.php';" approach.
format =
{
finalVariable ? null,
}:
let
toPHP =
value:
{
"null" = "null";
"bool" = if value then "true" else "false";
"int" = toString value;
"float" = toString value;
"string" = string value;
"set" = attrs value;
"list" = list value;
}
.${builtins.typeOf value}
or (abort "should never happen: unknown value type ${builtins.typeOf value}");
# https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.single
escapeSingleQuotedString = lib.escape [
"'"
"\\"
];
string = value: "'${escapeSingleQuotedString value}'";
listContent = values: lib.concatStringsSep ", " (map toPHP values);
list = values: "[" + (listContent values) + "]";
attrsContent =
values:
lib.pipe values [
(lib.mapAttrsToList (k: v: "${toPHP k} => ${toPHP v}"))
(lib.concatStringsSep ", ")
];
attrs = set: if set ? _phpType then specialType set else "[" + attrsContent set + "]";
mixedArray =
{ list, set }: if list == [ ] then attrs set else "[${listContent list}, ${attrsContent set}]";
specialType =
{ value, _phpType }:
{
"mixed_array" = mixedArray value;
"raw" = value;
}
.${_phpType};
type =
with lib.types;
nullOr (oneOf [
bool
int
float
str
(attrsOf type)
(listOf type)
])
// {
description = "PHP value";
};
in
{
inherit type;
lib = {
mkMixedArray = list: set: {
_phpType = "mixed_array";
value = { inherit list set; };
};
mkRaw = raw: {
_phpType = "raw";
value = raw;
};
};
generate =
name: value:
pkgs.writeTextFile {
inherit name;
text =
let
# strict_types enabled here to easily debug problems when calling functions of incorrect type using `mkRaw`.
phpHeader = "<?php\ndeclare(strict_types=1);\n";
in
if finalVariable == null then
phpHeader + "return ${toPHP value};\n"
else
phpHeader + "\$${finalVariable} = ${toPHP value};\n";
};
};
}

View File

@@ -0,0 +1,60 @@
# Call nix-build on this file to run all tests in this directory
# This produces a link farm derivation with the original attrs
# merged on top of it.
# You can run parts of the "hierarchy" with for example:
# nix-build -A java-properties
# See `structured` below.
{
pkgs ? import ../../.. { },
}:
let
inherit (pkgs.lib)
mapAttrs
mapAttrsToList
isDerivation
mergeAttrs
foldl'
attrValues
recurseIntoAttrs
;
structured = {
formats = import ./formats.nix { inherit pkgs; };
java-properties = recurseIntoAttrs {
jdk11 = pkgs.callPackage ../formats/java-properties/test { jdk = pkgs.jdk11_headless; };
jdk17 = pkgs.callPackage ../formats/java-properties/test { jdk = pkgs.jdk17_headless; };
jdk = pkgs.callPackage ../formats/java-properties/test { jdk = pkgs.jdk_headless; };
};
libconfig = recurseIntoAttrs (import ../formats/libconfig/test { inherit pkgs; });
hocon = recurseIntoAttrs (import ../formats/hocon/test { inherit pkgs; });
};
flatten =
prefix: as:
foldl' mergeAttrs { } (
attrValues (
mapAttrs (
k: v:
if isDerivation v then
{ "${prefix}${k}" = v; }
else if v ? recurseForDerivations then
flatten "${prefix}${k}-" (removeAttrs v [ "recurseForDerivations" ])
else
builtins.trace v throw "expected derivation or recurseIntoAttrs"
) as
)
);
in
# It has to be a link farm for inclusion in the hydra unstable jobset.
pkgs.linkFarm "pkgs-lib-formats-tests" (
mapAttrsToList (k: v: {
name = k;
path = v;
}) (flatten "" structured)
)
// structured

File diff suppressed because it is too large Load Diff

View File