| git.druid.rocks | index | druid520 | dtc | dtc.sh |
dtc.sh
#!/usr/bin/env sh
# dtc -- devel toolchain. a handful of small, focused commands for
# working with an sc/btf-based project, from scratch through ongoing
# growth: scaffold a new one, check the toolchain it needs is actually
# present, add a binary or a docs page to one that already exists, or
# scaffold a new mp port for something ready to be packaged. dtc never
# bundles copies of the tools it scaffolds FOR (sc, btf2html, btf2man)
# -- every command below relies on them already being on $PATH, real
# dependencies of this tool's own port, same as any other command
# reaching for a compiler or the like.
#
# usage: dtc <command> [args].
# commands: new doctor addbin newdoc port.
set -e
_dtc_die() {
echo "err: $*" >&2
exit 1
}
# reads "key=value" (a plain double-quoted assignment, sc.conf's own
# format) out of sc.conf -- first match, so a later, commented-out, or
# per-target override line never wins over the real one. empty if the
# key isn't set at all, same as sc.sh's own "keeps its default" case --
# every caller applies ITS OWN default the identical way sc.sh does
# (": ${x:=...}"), rather than this function guessing one on its
# behalf.
_dtc_conf_get() {
sed -n "s/^$1=\"\([^\"]*\)\"\$/\1/p" sc.conf | head -1
}
# writes one starter source file for $1=path $2=kind $3=display-name,
# matching exactly the kinds sc.sh's own per-$bin-name resolution
# understands (see sc.conf.example's own bin= documentation for the
# full list) -- shared between "new" (every name defaults to "c", its
# long-standing behavior, unchanged) and "addbin" (which asks).
_dtc_write_stub() {
path="$1"; kind="$2"; label="$3"
case "$kind" in
c)
# a prototype above main(), the function itself defined below
# it -- mp's own C (src/*.c) always declares a function before
# using it, never relies on an implicit/later declaration;
# return type on its own line, same as every real mp function
# definition (see e.g. src/mpx.c's own main()).
cat > "$path" <<EOF
#include <stdio.h>
int main(void);
int
main(void)
{
printf("hello from $label\n");
return 0;
}
EOF
;;
s|S)
# a minimal, dependency-free x86-64 Linux syscall exit -- unlike
# the C stub above, assembly has no portable "print a greeting"
# convention to lean on (it would need a different syscall
# number/calling convention per target OS), so this just proves
# the binary links and exits cleanly rather than pretending to
# be portable.
cat > "$path" <<EOF
.global main
.text
main:
mov \$60, %rax
xor %rdi, %rdi
syscall
EOF
;;
pl)
cat > "$path" <<EOF
#!/usr/bin/env perl
use strict;
use warnings;
print "hello from $label\n";
EOF
chmod +x "$path"
;;
sh|bin)
cat > "$path" <<EOF
#!/bin/sh
echo "hello from $label"
EOF
chmod +x "$path"
;;
esac
}
# writes the shared base style.btfs -- factored out since both "new"
# and "newdoc" need to be able to lay one down (a docs/ folder started
# by "new" already has one; "newdoc" run against a project that opted
# out of docs at "new" time, or had its docs/ folder created some other
# way, still needs to create one the first time it adds a page).
_dtc_write_style() {
cat > "$1" <<'BTFS'
[rule sel="*"]
[prop n="box-sizing"]border-box[e]
[e]
[rule sel="body"]
[prop n="background"]#0a0a0a[e]
[prop n="color"]#b8b8b8[e]
[prop n="font"]14px/1.6 "Courier New", Courier, monospace[e]
[prop n="max-width"]72ch[e]
[prop n="margin"]0 auto[e]
[prop n="padding"]2ch 2ch 8ch 2ch[e]
[e]
[rule sel="h1, h2, h3, h4"]
[prop n="color"]#d0d0d0[e]
[prop n="font"]bold 16px/1.4 "Courier New", Courier, monospace[e]
[prop n="border-bottom"]1px solid #222[e]
[prop n="margin"]1.5em 0 0.5em 0[e]
[e]
[rule sel="a"]
[prop n="color"]#88ccbb[e]
[prop n="text-decoration"]none[e]
[e]
[rule sel="a:hover"]
[prop n="background"]#222[e]
[prop n="color"]#cceecc[e]
[e]
[rule sel="pre, code, tt"]
[prop n="font"]14px/1.4 "Courier New", Courier, monospace[e]
[prop n="background"]#0f0f0f[e]
[prop n="padding"]0.5em[e]
[prop n="overflow-x"]auto[e]
[e]
[rule sel="hr"]
[prop n="border"]0[e]
[prop n="border-top"]1px solid #222[e]
[prop n="margin"]1.5em 0[e]
[e]
[rule sel="ul, ol"]
[prop n="padding-left"]2ch[e]
[e]
[rule sel="table"]
[prop n="border-collapse"]collapse[e]
[e]
[rule sel="th, td"]
[prop n="border"]1px solid #222[e]
[prop n="padding"]0.3em 0.6em[e]
[e]
[rule sel="th"]
[prop n="background"]#111[e]
[prop n="color"]#d0d0d0[e]
[e]
[rule sel="#btf-hdr"]
[prop n="text-align"]right[e]
[prop n="color"]#555[e]
[prop n="font"]11px/1 "Courier New", Courier, monospace[e]
[prop n="border-bottom"]1px solid #1a1a1a[e]
[prop n="padding-bottom"]0.4em[e]
[prop n="margin-bottom"]1.5em[e]
[e]
[rule sel="#btf-hdr a"]
[prop n="color"]#666[e]
[e]
[rule sel="#btf-hdr a:hover"]
[prop n="background"]transparent[e]
[prop n="color"]#999[e]
[e]
[rule sel="#btf-ftr"]
[prop n="text-align"]right[e]
[prop n="color"]#444[e]
[prop n="font"]11px/1 "Courier New", Courier, monospace[e]
[prop n="border-top"]1px solid #1a1a1a[e]
[prop n="padding-top"]0.5em[e]
[prop n="margin-top"]2em[e]
[e]
[rule sel="#btf-ftr a"]
[prop n="color"]#555[e]
[e]
[rule sel="#btf-ftr a:hover"]
[prop n="background"]transparent[e]
[prop n="color"]#888[e]
[e]
[rule sel="#tabs"]
[prop n="display"]flex[e]
[prop n="flex-wrap"]wrap[e]
[prop n="gap"]0.3em 1.2em[e]
[prop n="border-bottom"]1px solid #222[e]
[prop n="padding-bottom"]0.5em[e]
[prop n="margin-bottom"]1.5em[e]
[prop n="font"]13px/1 "Courier New", Courier, monospace[e]
[e]
[rule sel="#tabs a"]
[prop n="color"]#88ccbb[e]
[e]
[rule sel="#tabs a:hover"]
[prop n="background"]#222[e]
[prop n="color"]#cceecc[e]
[e]
BTFS
}
_dtc_new() {
dir="$1"
if [ -z "$dir" ]; then
printf 'project directory [.]: '
read -r dir
[ -z "$dir" ] && dir='.'
fi
mkdir -p "$dir"
if [ -f "$dir/sc.conf" ]; then
_dtc_die "$dir/sc.conf already exists -- dtc only scaffolds a NEW project (see 'dtc addbin'/'dtc newdoc' to grow an existing one)."
fi
printf 'binary name (or several, space-separated) [myprogram]: '
read -r bin
[ -z "$bin" ] && bin='myprogram'
printf 'supported OSes [linux freebsd netbsd openbsd]: '
read -r oss
[ -z "$oss" ] && oss='linux freebsd netbsd openbsd'
printf 'add a docs/ folder (style.btfs + a starter index.btft)? [Y/n] '
read -r want_docs
case "$want_docs" in
[Nn]*) want_docs='no' ;;
*) want_docs='yes' ;;
esac
mkdir -p "$dir/src"
# "set -- $bin" overwrites the script's own $1/$#/$@ (its CLI args,
# already-saved into $dir above by this point) -- firstbin is
# captured explicitly, here, before that happens, since the docs/
# section further down wants "the project's name" for its starter
# page and has no other reasonable source for one.
set -- $bin
firstbin="$1"
# always src/<name>.c, one name at a time (never src/main.c, even
# for a single name): a file named after its own $bin entry is that
# binary's own compiled entry point, never sc's "shared" bucket (see
# sc.sh's own _is_bin_name) -- a later "dtc addbin" adding a second
# compiled binary can then never collide with this one's "main",
# which a shared src/main.c genuinely would (sc.sh links every
# shared source into every compiled-kind binary, "main" symbol
# included). "dtc addbin" is how to add one of the other kinds
# afterward.
for name in "$@"; do
[ -e "$dir/src/$name.c" ] && continue
_dtc_write_stub "$dir/src/$name.c" c "$name"
done
cat > "$dir/sc.conf" <<EOF
# sc.sh config -- see sc.conf.example (in the sc repo) for every key
# this file could set, with full documentation of each. dtc only ever
# fills in the handful a new project actually needs to get building;
# everything else keeps sc.sh's own defaults until you ask for it.
bin="$bin"
oss="$oss"
EOF
# sc.sh's own build output (out/, logs/) and rendered docs
# (docs/html, docs/man) -- every one of them regenerated from source
# on the next "sc && make"/"mk . d", never something worth
# committing. never clobbers an existing .gitignore -- appends
# instead, same "grow what's already there" reasoning "dtc addbin"
# already applies to sc.conf's own bin=.
if [ -e "$dir/.gitignore" ]; then
{
echo "/out"
echo "/docs/html"
echo "/docs/man"
echo "/logs"
} >> "$dir/.gitignore"
else
cat > "$dir/.gitignore" <<'EOF'
/out
/docs/html
/docs/man
/logs
EOF
fi
if [ "$want_docs" = 'yes' ]; then
mkdir -p "$dir/docs"
if [ ! -e "$dir/docs/index.btft" ]; then
cat > "$dir/docs/index.btft" <<EOF
[h1]$firstbin[e]
welcome to $firstbin.
EOF
fi
[ -e "$dir/docs/style.btfs" ] || _dtc_write_style "$dir/docs/style.btfs"
fi
echo "dtc: scaffolded $dir. run 'sc' there (or 'cd $dir && sc') to configure it."
}
_dtc_doctor() {
# $CC, if set, is what a real "sc" run in this same environment
# would actually invoke in place of a bare "cc" -- checking that
# one specifically (when set) matches sc's own "${CC:-cc}" exactly,
# rather than reporting a plain "cc" that's present while the one
# sc would really use is not.
ccbin="${CC:-cc}"
echo "dtc doctor:"
for tool in sc btf2html btf2man mk "$ccbin" perl pkg-config; do
label="$tool"
[ "$tool" = "$ccbin" ] && label="cc ($ccbin)"
if command -v "$tool" >/dev/null 2>&1; then
echo "ok: $label found."
else
echo "warn: $label not found on \$PATH."
fi
done
}
_dtc_addbin() {
name="$1"; kind="${2:-c}"
[ -n "$name" ] || _dtc_die "usage: dtc addbin <name> [c|s|S|pl|sh|bin]."
[ -f sc.conf ] || _dtc_die "no sc.conf here -- run this from an existing sc project's own directory (see 'dtc new')."
case "$kind" in
c|s|S|pl|sh|bin) ;;
*) _dtc_die "unknown kind '$kind' -- one of c, s, S, pl, sh, bin (bin: a bare, extension-less file -- see sc.conf.example's own bin= documentation)." ;;
esac
src="$(_dtc_conf_get src)"; : "${src:=src}"
mkdir -p "$src"
ext=""; [ "$kind" != "bin" ] && ext=".$kind"
target="$src/$name$ext"
[ -e "$target" ] && _dtc_die "$target already exists."
_dtc_write_stub "$target" "$kind" "$name"
grep -q '^bin="' sc.conf \
|| _dtc_die "sc.conf has no plain bin=\"...\" line to append to (unexpected format) -- add '$name' to it by hand instead."
awk -v name="$name" '/^bin="/ { sub(/"$/, " " name "\"") } { print }' sc.conf > sc.conf.new
mv sc.conf.new sc.conf
echo "dtc: added '$name' ($kind) at $target, and appended it to sc.conf's bin=. re-run sc to reconfigure."
}
_dtc_newdoc() {
name="$1"
[ -n "$name" ] || _dtc_die "usage: dtc newdoc <name>."
[ -f sc.conf ] || _dtc_die "no sc.conf here -- run this from an existing sc project's own directory (see 'dtc new')."
docsdir="$(_dtc_conf_get docsdir)"; : "${docsdir:=docs}"
mkdir -p "$docsdir"
target="$docsdir/$name.btft"
[ -e "$target" ] && _dtc_die "$target already exists."
cat > "$target" <<EOF
[h1]$name[e]
EOF
[ -e "$docsdir/style.btfs" ] || _dtc_write_style "$docsdir/style.btfs"
echo "dtc: added $target."
}
# unlike "new"/"addbin"/"newdoc" (which grow a project), this wraps one
# that already exists -- run FROM an sc project's own directory, it
# scaffolds the real, working port for exactly that project's own sc.conf
# (every bin= name, a real "sc; mk . b" build step), not mp new's generic,
# not-yet-written-project template (a plain "cp <name> ...", nothing to
# actually build) -- the two are complementary, not overlapping: mp new
# for a from-scratch or non-sc port, this for "I already have a working
# sc project, now make it installable". category defaults to the literal
# "port" and name to the project's own first bin= entry (dtc port, no
# args, from a project called myprogram, scaffolds port/myprogram) --
# pass <category>/<name> explicitly to override either half.
_dtc_port() {
catname="$1"
[ -f sc.conf ] || _dtc_die "no sc.conf here -- run this from an existing sc project's own directory (see 'dtc new') -- for a from-scratch, non-sc port instead, see mp new."
bin="$(_dtc_conf_get bin)"
[ -n "$bin" ] || _dtc_die "sc.conf has no bin=\"...\" to build a port from."
out="$(_dtc_conf_get out)"; : "${out:=out}"
set -- $bin
firstbin="$1"
: "${catname:=port/$firstbin}"
case "$catname" in
*/*) ;;
*) _dtc_die "expected <category>/<name>, e.g. port/$firstbin." ;;
esac
name="${catname##*/}"
[ -e "$catname" ] && _dtc_die "$catname already exists."
mkdir -p "$catname"
# the project's own git remote, if this is a git checkout with one --
# a real pkg_fetch instead of a TODO placeholder whenever it's
# actually knowable, since the whole point of this command is
# "already have a real project", not "starting from nothing".
remote=""
if git rev-parse --git-dir >/dev/null 2>&1; then
remote="$(git remote get-url origin 2>/dev/null)"
fi
{
echo "pkg_name=\"$name\""
echo "pkg_license=\"fmc\""
echo "pkg_ver=\"git\""
echo "pkg_tags=\"$name\""
echo "pkg_pref=\"50\""
echo "pkg_deps=\"sc mk %shell %coreutils\""
if [ -n "$remote" ]; then
echo "pkg_fetch=\"git:$remote\""
else
echo "# TODO: point this at the real repo before pushing."
echo "pkg_fetch=\"git:git://REPLACE_ME/$name.git\""
fi
echo
echo "install:"
printf '\tsc\n'
printf '\tmk . b\n'
printf '\tmkdir -p $MP_PREFIX/bin\n'
for b in $bin; do
printf '\tcp %s/%s $MP_PREFIX/bin/%s\n' "$out" "$b" "$b"
done
printf '\tchmod +x'
for b in $bin; do printf ' $MP_PREFIX/bin/%s' "$b"; done
printf '\n\n'
echo "remove:"
printf '\trm -f'
for b in $bin; do printf ' $MP_PREFIX/bin/%s' "$b"; done
printf '\n'
} > "$catname/pkg.conf"
echo "dtc: scaffolded $catname/pkg.conf from this project's own sc.conf (bin=\"$bin\")."
if [ -n "$remote" ]; then
echo "dtc: fetches from $remote -- push there, then 'mp install $name' (or 'mp devtest --dir=. $name' to try the whole install/remove cycle straight from here first)."
else
echo "dtc: no git remote found here -- fill in pkg_fetch by hand before pushing."
fi
}
cmd="$1"
[ -n "$cmd" ] && shift
case "$cmd" in
new) _dtc_new "$@" ;;
doctor) _dtc_doctor "$@" ;;
addbin) _dtc_addbin "$@" ;;
newdoc) _dtc_newdoc "$@" ;;
port) _dtc_port "$@" ;;
''|help|-h|--help)
echo "usage: dtc <command> [args]." >&2
echo "commands: new doctor addbin newdoc port." >&2
[ -z "$cmd" ] && exit 2
exit 0
;;
*)
echo "err: unknown command '$cmd'." >&2
echo "usage: dtc <command> [args]." >&2
echo "commands: new doctor addbin newdoc port." >&2
exit 2
;;
esac