Files
RetroArch/tools/sample_objdir_check.c
T
LibretroAdmin b366054913 tools: C ports of the remaining Python CI checkers
Every Python checker still run by a workflow, rewritten in C89 on
tools/check_common.h and run through tools/check.sh, which builds each
one once and reuses the binary:

  command_table_check        overlay_setter_bounds
  companion_parity           overlay_texture_ownership
  ctx_presentable_check      removed_member_check
  d3d12_idle_wait_check      rgui_pixfmt_check
  gl_present_vsynced_check   sample_objdir_check
  gl_ring_fence_check        settings_capacity_check
  input_driver_table_check   surface_requirements_check
  input_video_separation_check  texture_image_init_check
  msg_hash_format_check      vfs_backend_parity
  msg_hash_packed_check      video_thread_state_check
  vulkan_idle_wait_check     vulkan_teardown_order_check

Each gives the same output and exit status as the script it replaces,
on the tree and on its self-test; each was also run against the
script on a few hundred generated inputs with no difference. Where the
script iterated a Python set, the C reports in sorted order.

msg_hash_format_check gains --rows FILE, the row filter intl/json2h.py
needs. rgui_pixfmt_check's harness moves out of a string into
tools/rgui_pixfmt_harness.inc, which includes the lifted block.

check_common.h gains ck_capture() and ck_sb_shq() to run a command and
read its output, ck_set_free(), ck_repr(), ck_blank_code() and
ck_def_name().
2026-10-13 10:23:47 +02:00

482 lines
12 KiB
C

/* Every sample keeps its objects in its own directory.
*
* Dry-runs each sample Makefile (make -n -B) and fails on any object
* written outside the sample, where the next sample to link that source
* would take it whatever flags it was built with.
*
* The dry runs are started several at a time, one a processor, and read
* in order.
*
* tools/check.sh sample_objdir_check [DIR...]
* tools/check.sh sample_objdir_check --selftest */
#include "check_common.h"
#ifndef _WIN32
#include <sys/wait.h>
#endif
#define COUNT(a) (sizeof(a) / sizeof((a)[0]))
#define MAX_JOBS 16
static const char *const roots[] = { "samples", "libretro-common/samples" };
/* the default goal, all and check */
static const char *const targets[] = { "", " all", " check" };
struct state
{
struct ck_strv dirs;
const char *base;
};
static int is_space(int c)
{
return CK_IS_SPACE(c) || (c >= 0x1c && c <= 0x1f);
}
/* A command line split as a POSIX shell would split it; where a quote
* or an escape is left open, at white space. */
static void split_line(const char *b, const char *e, struct ck_strv *argv)
{
struct ck_sb tok = {0};
const char *p;
int in_tok = 0;
int open = 0;
char quote = 0;
for (p = b; p < e; p++)
{
char c = *p;
if (quote)
{
if (c == quote)
quote = 0;
else if (c == '\\' && quote == '"')
{
if (++p == e)
{
open = 1;
break;
}
if (*p != '\\' && *p != '"')
ck_sb_putc(&tok, '\\');
ck_sb_putc(&tok, *p);
}
else
ck_sb_putc(&tok, c);
}
else if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
{
if (in_tok)
{
ck_sb_puts(&tok, "");
ck_strv_push(argv, ck_strdup(tok.p));
tok.len = 0;
in_tok = 0;
}
}
else
{
in_tok = 1;
if (c == '\\')
{
if (++p == e)
{
open = 1;
break;
}
ck_sb_putc(&tok, *p);
}
else if (c == '\'' || c == '"')
quote = c;
else
ck_sb_putc(&tok, c);
}
}
if (open || quote)
{
/* left open: every run of white space separates */
ck_strv_free(argv);
for (p = b; p < e; )
{
const char *w;
while (p < e && is_space((unsigned char)*p))
p++;
for (w = p; p < e && !is_space((unsigned char)*p); p++);
if (p > w)
ck_strv_push(argv, ck_strndup(w, (size_t)(p - w)));
}
}
else if (in_tok)
{
ck_sb_puts(&tok, "");
ck_strv_push(argv, ck_strdup(tok.p));
}
ck_sb_free(&tok);
}
/* A path with its symbolic links followed as far as it exists, and
* the rest of it normalised. */
static char *real_path(const char *path, int depth)
{
struct ck_sb res = {0};
struct ck_sb abs = {0};
const char *p;
if (path[0] != '/'
#ifdef _WIN32
&& !(path[0] && path[1] == ':')
#endif
)
{
char cwd[4096];
#ifdef _WIN32
if (!_getcwd(cwd, sizeof(cwd)))
#else
if (!getcwd(cwd, sizeof(cwd)))
#endif
ck_die("cannot read the current directory");
ck_sb_puts(&abs, cwd);
ck_sb_putc(&abs, '/');
}
ck_sb_puts(&abs, path);
ck_sb_puts(&res, "");
for (p = abs.p; *p; )
{
const char *c = p;
size_t n, keep;
while (*p && *p != '/')
p++;
n = (size_t)(p - c);
while (*p == '/')
p++;
if (!n || (n == 1 && c[0] == '.'))
continue;
if (n == 2 && c[0] == '.' && c[1] == '.')
{
while (res.len && res.p[res.len - 1] != '/')
res.len--;
if (res.len)
res.len--;
res.p[res.len] = '\0';
continue;
}
keep = res.len;
ck_sb_putc(&res, '/');
ck_sb_add(&res, c, n);
#ifndef _WIN32
if (depth < 40 && ck_is_link(res.p))
{
char link[4096];
long got = (long)readlink(res.p, link, sizeof(link) - 1);
if (got > 0)
{
struct ck_sb to = {0};
char *target;
link[got] = '\0';
if (link[0] != '/')
{
ck_sb_add(&to, res.p, keep);
ck_sb_putc(&to, '/');
}
ck_sb_puts(&to, link);
target = real_path(to.p, depth + 1);
res.len = 0;
ck_sb_puts(&res, target);
free(target);
ck_sb_free(&to);
}
}
#else
(void)depth;
(void)keep;
#endif
}
if (!res.len)
ck_sb_puts(&res, "/");
ck_sb_free(&abs);
return res.p;
}
static int outside(const char *sample_dir, const char *path)
{
struct ck_sb joined = {0};
char *base, *full;
size_t n;
int out;
if (path[0] != '/')
{
ck_sb_puts(&joined, sample_dir);
ck_sb_putc(&joined, '/');
}
ck_sb_puts(&joined, path);
base = real_path(sample_dir, 0);
full = real_path(joined.p, 0);
n = strlen(base);
out = strncmp(base, full, n) || (full[n] && full[n] != '/' && n > 1);
free(base);
free(full);
ck_sb_free(&joined);
return out;
}
/* Objects in a dry run's output that land outside sample_dir: what a
* compile line writes with -o, where it ends in .o or .obj. */
static void stray(const char *sample_dir, const char *text,
struct ck_strv *bad)
{
const char *line = text;
while (*line)
{
struct ck_strv argv = {0};
const char *e = line;
size_t i, k;
while (*e && *e != '\n' && *e != '\r' && *e != '\v' && *e != '\f'
&& !(*e >= 0x1c && *e <= 0x1e))
e++;
split_line(line, e, &argv);
for (i = 0; i < argv.n; i++)
{
const char *a = argv.v[i];
const char *o = NULL;
if (!strcmp(a, "-o") && i + 1 < argv.n)
o = argv.v[++i];
else if (!strncmp(a, "-o", 2) && a[2] && a[2] != 'n')
o = a + 2;
if (!o || !(ck_ends_with(o, ".o") || ck_ends_with(o, ".obj"))
|| !outside(sample_dir, o))
continue;
for (k = 0; k < bad->n && strcmp(bad->v[k], o); k++);
if (k == bad->n)
ck_strv_push(bad, ck_strdup(o));
}
ck_strv_free(&argv);
line = *e ? e + 1 : e;
}
}
static void quote_arg(struct ck_sb *sb, const char *s)
{
#ifdef _WIN32
ck_sb_putc(sb, '"');
ck_sb_puts(sb, s);
ck_sb_putc(sb, '"');
#else
ck_sb_putc(sb, '\'');
for (; *s; s++)
{
if (*s == '\'')
ck_sb_puts(sb, "'\\''");
else
ck_sb_putc(sb, *s);
}
ck_sb_putc(sb, '\'');
#endif
}
static int processors(void)
{
long n = 2;
#ifdef _WIN32
SYSTEM_INFO si;
GetSystemInfo(&si);
n = (long)si.dwNumberOfProcessors;
#elif defined(_SC_NPROCESSORS_ONLN)
n = sysconf(_SC_NPROCESSORS_ONLN);
#endif
return n < 2 ? 2 : n > MAX_JOBS ? MAX_JOBS : (int)n;
}
/* What the dry runs of every directory print, in ok[] whether any of
* a directory's three could be run. Each writes to a file of its own,
* so none waits on a reader. */
static void dry_runs(const struct ck_strv *dirs, char **text, int *ok)
{
char *tmp = ck_tmpdir();
size_t total = dirs->n * COUNT(targets);
int jobs = processors();
struct ck_sb cmd = {0};
FILE *running[MAX_JOBS];
size_t at, i;
#ifdef _WIN32
const char *limit = "";
const char *sink = "2>NUL";
#else
const char *sink = "2>/dev/null";
const char *limit = system("command -v timeout >/dev/null 2>&1") == 0
? "timeout 120 " : "";
#endif
for (i = 0; i < dirs->n; i++)
{
text[i] = NULL;
ok[i] = 0;
}
for (at = 0; at < total; at += (size_t)jobs)
{
size_t n = total - at < (size_t)jobs ? total - at : (size_t)jobs;
for (i = 0; i < n; i++)
{
char num[32];
sprintf(num, "/%lu.out", (unsigned long)(at + i));
cmd.len = 0;
ck_sb_puts(&cmd, limit);
ck_sb_puts(&cmd, "make -n -B -C ");
quote_arg(&cmd, dirs->v[(at + i) / COUNT(targets)]);
ck_sb_puts(&cmd, targets[(at + i) % COUNT(targets)]);
ck_sb_puts(&cmd, " > ");
ck_sb_puts(&cmd, tmp);
ck_sb_puts(&cmd, num);
ck_sb_putc(&cmd, ' ');
ck_sb_puts(&cmd, sink);
#ifdef _WIN32
running[i] = _popen(cmd.p, "r");
#else
running[i] = popen(cmd.p, "r");
#endif
}
for (i = 0; i < n; i++)
{
size_t d = (at + i) / COUNT(targets);
int status;
char num[32];
if (!running[i])
continue;
#ifdef _WIN32
status = _pclose(running[i]);
#else
status = pclose(running[i]);
status = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
#endif
if (status == 0)
{
struct ck_sb all = {0};
char *out;
sprintf(num, "/%lu.out", (unsigned long)(at + i));
cmd.len = 0;
ck_sb_puts(&cmd, tmp);
ck_sb_puts(&cmd, num);
out = ck_read_file(cmd.p, NULL);
ck_sb_puts(&all, text[d] ? text[d] : "");
ck_sb_puts(&all, out ? out : "");
free(out);
free(text[d]);
text[d] = all.p;
ok[d] = 1;
}
}
}
ck_rmtree(tmp);
free(tmp);
ck_sb_free(&cmd);
}
/* Sample directories; the Makefile at a root runs the samples through
* $(MAKE), which make -n would run for real, and builds nothing of its
* own. */
static void add_makefile(const char *path, const char *rel, void *ud)
{
struct state *st = (struct state*)ud;
const char *slash = strrchr(path, '/');
(void)rel;
if (!slash || strcmp(slash + 1, "Makefile")
|| ((size_t)(slash - path) == strlen(st->base)
&& !strncmp(path, st->base, strlen(st->base))))
return;
ck_strv_push(&st->dirs, ck_strndup(path, (size_t)(slash - path)));
}
static int selftest(void)
{
static const char d[] = "samples/x/y";
static const char *const cases[][2] = {
{ "cc -c -o a.o a.c", NULL },
{ "cc -c -oobj/a.o a.c", NULL },
{ "cc -c -o obj/__/__/__/f/b.o ../../../f/b.c", NULL },
{ "cc -c -o ../../../f/b.o ../../../f/b.c", "../../../f/b.o" },
{ "cc -c -o../../../f/b.o ../../../f/b.c", "../../../f/b.o" },
{ "cc -o test a.o ../../../f/b.o", NULL },
{ "x86_64-w64-mingw32-gcc -c -o ../z.obj z.c", "../z.obj" }
};
size_t i;
int fails = 0;
for (i = 0; i < COUNT(cases); i++)
{
struct ck_strv got = {0};
const char *want = cases[i][1];
stray(d, cases[i][0], &got);
if (want ? got.n != 1 || strcmp(got.v[0], want) : got.n != 0)
{
printf("selftest: '%s' gave %s%s%s, want %s%s%s\n", cases[i][0],
got.n ? "['" : "[", got.n ? got.v[0] : "", got.n ? "']" : "]",
want ? "['" : "[", want ? want : "", want ? "']" : "]");
fails++;
}
ck_strv_free(&got);
}
printf("selftest: %lu case(s), %d failure(s)\n",
(unsigned long)COUNT(cases), fails);
return fails ? 1 : 0;
}
int main(int argc, char **argv)
{
struct state st;
char **text;
int *ok;
size_t i;
int bad = 0;
if (argc == 2 && !strcmp(argv[1], "--selftest"))
return selftest();
memset(&st, 0, sizeof(st));
if (argc > 1)
{
int a;
for (a = 1; a < argc; a++)
ck_strv_push(&st.dirs, ck_strdup(argv[a]));
}
else
{
for (i = 0; i < COUNT(roots); i++)
{
struct ck_sb base = {0};
ck_sb_puts(&base, "./");
ck_sb_puts(&base, roots[i]);
st.base = base.p;
ck_walk(base.p, NULL, add_makefile, &st);
ck_sb_free(&base);
}
ck_strv_sort(&st.dirs);
}
text = (char**)ck_alloc((st.dirs.n + 1) * sizeof(char*));
ok = (int*)ck_alloc((st.dirs.n + 1) * sizeof(int));
dry_runs(&st.dirs, text, ok);
for (i = 0; i < st.dirs.n; i++)
{
struct ck_strv objs = {0};
if (!ok[i])
{
bad++;
printf("%s: make cannot dry-run it, so it cannot be checked\n",
st.dirs.v[i]);
continue;
}
stray(st.dirs.v[i], text[i], &objs);
if (objs.n)
{
bad++;
printf("%s: %lu object(s) outside the sample, e.g. %s\n",
st.dirs.v[i], (unsigned long)objs.n, objs.v[0]);
}
ck_strv_free(&objs);
free(text[i]);
}
printf("%d of %lu sample(s) build objects outside their directory\n",
bad, (unsigned long)st.dirs.n);
return bad ? 1 : 0;
}