| /* |
| * Copyright 2024 Intel Corporation |
| * SPDX-License-Identifier: MIT |
| */ |
| |
| #include <dirent.h> |
| #include <errno.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/mman.h> |
| #include <sys/types.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| |
| #include "util/os_file.h" |
| #include "util/os_misc.h" |
| #include "util/ralloc.h" |
| #include "util/u_dynarray.h" |
| |
| #include "slice.h" |
| #include "tar.h" |
| |
| typedef struct content { |
| slice name; |
| slice fullname; |
| slice data; |
| } content; |
| |
| typedef struct object { |
| slice prefix; |
| slice name; |
| slice fullname; |
| |
| int versions_count; |
| content *versions; |
| |
| struct mesa_archive *ma; |
| } object; |
| |
| typedef struct mesa_archive { |
| slice filename; |
| slice contents; |
| int objects_count; |
| object *objects; |
| |
| const char *info; |
| slice detected_mda_prefix; |
| } mesa_archive; |
| |
| enum diff_mode { |
| DIFF_UNIFIED, |
| DIFF_SIDE_BY_SIDE, |
| }; |
| |
| typedef struct context { |
| const char *cmd_name; |
| |
| char **args; |
| int args_count; |
| |
| mesa_archive **archives; |
| int archives_count; |
| |
| struct { |
| enum diff_mode mode; |
| int param; |
| } diff; |
| } context; |
| |
| #define foreach_object(OBJ, MA) \ |
| for (object *OBJ = (MA)->objects; \ |
| OBJ < (MA)->objects + (MA)->objects_count; \ |
| OBJ++) |
| |
| #define foreach_version(CONTENT, OBJ) \ |
| for (content *CONTENT = (OBJ)->versions; \ |
| CONTENT < (OBJ)->versions + (OBJ)->versions_count; \ |
| CONTENT++) |
| |
| static void PRINTFLIKE(1, 2) |
| failf(const char *fmt, ...) |
| { |
| fflush(stdout); |
| va_list args; |
| va_start(args, fmt); |
| vfprintf(stderr, fmt, args); |
| va_end(args); |
| exit(1); |
| } |
| |
| typedef struct { |
| FILE *f; |
| const char *path; |
| } temp_file; |
| |
| static temp_file |
| make_temp_file(void *mem_ctx) |
| { |
| char path[] = "/tmp/fileXXXXXX"; |
| |
| int fd = mkstemp(path); |
| if (fd == -1) |
| failf("mda: failed creating temporary file: %s", strerror(errno)); |
| |
| FILE *f = fdopen(fd, "w"); |
| if (!f) |
| failf("mda: failed creating temporary file: %s", strerror(errno)); |
| |
| temp_file r = {0}; |
| r.f = f; |
| r.path = ralloc_strdup(mem_ctx, path); |
| return r; |
| } |
| |
| static void |
| diff(context *ctx, slice a, slice b) |
| { |
| void *mem_ctx = ralloc_context(NULL); |
| |
| temp_file file_a = make_temp_file(mem_ctx); |
| temp_file file_b = make_temp_file(mem_ctx); |
| |
| fwrite(a.data, a.len, 1, file_a.f); |
| fwrite(b.data, b.len, 1, file_b.f); |
| |
| fclose(file_a.f); |
| fclose(file_b.f); |
| |
| const char *diff_cmd = os_get_option("MDA_DIFF_COMMAND"); |
| if (!diff_cmd) { |
| if (ctx->diff.mode == DIFF_UNIFIED) { |
| diff_cmd = ralloc_asprintf(mem_ctx, "git diff --no-index --color-words -U%d -- %%s %%s | tail -n +5", ctx->diff.param); |
| } else { |
| diff_cmd = ralloc_asprintf(mem_ctx, "diff -y -W%d %%s %%s", ctx->diff.param); |
| } |
| } |
| |
| char *cmd = ralloc_asprintf(mem_ctx, diff_cmd, file_a.path, file_b.path); |
| |
| /* Make sure everything printed so far is flushed before the diff |
| * subprocess print things. |
| */ |
| fflush(stdout); |
| |
| system(cmd); |
| |
| unlink(file_a.path); |
| unlink(file_b.path); |
| ralloc_free(mem_ctx); |
| } |
| |
| static content * |
| first_version(object *obj) |
| { |
| assert(obj->versions_count > 0); |
| return &obj->versions[0]; |
| } |
| |
| static content * |
| last_version(object *obj) |
| { |
| assert(obj->versions_count > 0); |
| return &obj->versions[obj->versions_count - 1]; |
| } |
| |
| static void |
| print_repeated(char c, int count) |
| { |
| for (; count > 0; count--) |
| putchar(c); |
| } |
| |
| static mesa_archive * |
| parse_mesa_archive(void *mem_ctx, const char *filename) |
| { |
| size_t size = 0; |
| char *contents = os_read_file(filename, &size); |
| if (!contents) { |
| fprintf(stderr, "mda: error reading file %s: %s\n", filename, strerror(errno)); |
| return NULL; |
| } |
| |
| mesa_archive *ma = rzalloc(mem_ctx, mesa_archive); |
| ma->filename = slice_from_cstr(ralloc_strdup(ma, filename)); |
| ma->contents = (slice) { ralloc_memdup(ma, (const char *)contents, size), size }; |
| free(contents); |
| |
| tar_reader tr = {0}; |
| tar_reader_init_from_bytes(&tr, ma->contents.data, ma->contents.len); |
| |
| tar_reader_entry entry = {0}; |
| |
| bool found_mesa_txt = false; |
| ma->detected_mda_prefix = (slice){}; |
| while (tar_reader_next(&tr, &entry)) { |
| slice fullpath; |
| if (!slice_is_empty(entry.prefix)) { |
| char *fullpath_str = ralloc_asprintf(ma, "%.*s/%.*s", |
| SLICE_FMT(entry.prefix), |
| SLICE_FMT(entry.name)); |
| fullpath = slice_from_cstr(fullpath_str); |
| } else { |
| fullpath = entry.name; |
| } |
| |
| slice mda_mesa_txt = slice_from_cstr("mda/mesa.txt"); |
| |
| if (slice_ends_with(fullpath, mda_mesa_txt)) { |
| slice_cut_result cut = slice_cut(fullpath, '/'); |
| if (cut.found && slice_equal_cstr(cut.after, "mesa.txt")) { |
| /* Cut was succesful, so can extend to include the separator. */ |
| ma->detected_mda_prefix = (slice){ cut.before.data, cut.before.len+1 }; |
| ma->info = slice_to_cstr(ma, entry.contents); |
| found_mesa_txt = true; |
| break; |
| } |
| } |
| } |
| |
| if (!found_mesa_txt) { |
| fprintf(stderr, "mda: wrong archive, missing mesa.txt\n"); |
| return NULL; |
| } |
| |
| /* Now that we found mesa. Reset header. */ |
| tar_reader_init_from_bytes(&tr, ma->contents.data, ma->contents.len); |
| |
| struct hash_table *lookup = slice_hash_table_create(ma); |
| |
| while (tar_reader_next(&tr, &entry)) { |
| slice fullpath; |
| if (!slice_is_empty(entry.prefix)) { |
| char *fullpath_str = ralloc_asprintf(ma, "%.*s/%.*s", |
| SLICE_FMT(entry.prefix), |
| SLICE_FMT(entry.name)); |
| fullpath = slice_from_cstr(fullpath_str); |
| } else { |
| fullpath = entry.name; |
| } |
| |
| /* Ignore directory entries. */ |
| if (slice_is_empty(entry.contents)) |
| continue; |
| |
| if (!slice_starts_with(fullpath, ma->detected_mda_prefix)) { |
| fprintf(stderr, "mda: ignoring unexpected file with wrong prefix: %.*s\n", SLICE_FMT(fullpath)); |
| continue; |
| } |
| |
| /* Remove the detected prefix from paths. We'll use the filename later |
| * on since is more visible to the user. Most of the time is going to |
| * be the same. |
| */ |
| { |
| slice_cut_result cut = slice_cut(fullpath, '/'); |
| assert(cut.found); |
| fullpath = cut.after; |
| } |
| |
| /* Already processed this before. */ |
| if (slice_equal_cstr(fullpath, "mesa.txt")) |
| continue; |
| |
| slice_cut_result first_cut = slice_cut(fullpath, '/'); |
| if (!first_cut.found) |
| continue; |
| |
| slice prefix_normalized = first_cut.before; |
| slice_cut_result second_cut = slice_cut(first_cut.after, '/'); |
| |
| slice key_slice, object_name, version_name; |
| |
| if (second_cut.found) { |
| /* Normal format: "0/OBJECT-NAME/version-name". */ |
| object_name = second_cut.before; |
| version_name = second_cut.after; |
| key_slice = slice_substr_to(fullpath, |
| second_cut.before.data + second_cut.before.len - fullpath.data); |
| } else { |
| /* Single version format: "0/SPIRV". */ |
| object_name = first_cut.after; |
| version_name = slice_from_cstr("binary"); |
| key_slice = fullpath; |
| } |
| |
| struct hash_entry *hash_entry = slice_hash_table_search(lookup, key_slice); |
| int obj_index = hash_entry ? (intptr_t)hash_entry->data : -1; |
| object *obj; |
| |
| if (obj_index == -1) { |
| ma->objects = rerzalloc(ma, ma->objects, object, ma->objects_count, ma->objects_count + 1); |
| obj_index = ma->objects_count++; |
| obj = &ma->objects[obj_index]; |
| obj->prefix = prefix_normalized; |
| obj->name = object_name; |
| obj->ma = ma; |
| char *fullname_str = ralloc_asprintf(ma, "%.*s/%.*s/%.*s", |
| SLICE_FMT(ma->filename), |
| SLICE_FMT(prefix_normalized), |
| SLICE_FMT(object_name)); |
| obj->fullname = slice_from_cstr(fullname_str); |
| obj->versions = NULL; |
| obj->versions_count = 0; |
| |
| slice_hash_table_insert(lookup, key_slice, (void *)(intptr_t)obj_index); |
| } else { |
| obj = &ma->objects[obj_index]; |
| } |
| |
| obj->versions = rerzalloc(ma, obj->versions, content, |
| obj->versions_count, obj->versions_count + 1); |
| int s = obj->versions_count++; |
| |
| obj->versions[s].name = version_name; |
| obj->versions[s].data = entry.contents; |
| char *version_fullname_str = ralloc_asprintf(ma, "%.*s/%.*s", SLICE_FMT(obj->fullname), SLICE_FMT(version_name)); |
| obj->versions[s].fullname = slice_from_cstr(version_fullname_str); |
| } |
| |
| return ma; |
| } |
| |
| typedef struct { |
| slice fullname; |
| object *object; |
| content *content; |
| } match; |
| |
| typedef struct { |
| match *matches; |
| int matches_count; |
| } find_all_result; |
| |
| enum match_flags { |
| /* Up until first slash in the pattern, consider a prefix match, then |
| * fuzzy for the remaining of the pattern. |
| * |
| * This works better for the common case of mda.tar files with names |
| * containing hashes. Trying to disambiguate by a prefix might end up |
| * also fuzzy matching the middle of other hashes. |
| */ |
| MATCH_PREFIX_FIRST_SLASH = 1 << 0, |
| }; |
| |
| static bool |
| is_match(slice name_slice, const char *pattern, unsigned match_flags) |
| { |
| assert(!slice_is_empty(name_slice)); |
| |
| slice pattern_slice = slice_from_cstr(pattern); |
| |
| /* Non-fuzzy matching first. */ |
| if (slice_contains_str(name_slice, pattern_slice)) |
| return true; |
| |
| slice s = name_slice; |
| slice p = pattern_slice; |
| |
| if (match_flags & MATCH_PREFIX_FIRST_SLASH) { |
| slice_cut_result pattern_cut = slice_cut(pattern_slice, '/'); |
| if (pattern_cut.found) { |
| slice_cut_result name_cut = slice_cut(name_slice, '/'); |
| if (!name_cut.found || !slice_starts_with(name_cut.before, pattern_cut.before)) |
| return false; |
| |
| /* Update s and p to continue from after the slash. */ |
| s = name_cut.after; |
| p = pattern_cut.after; |
| } |
| } |
| |
| bool matched = false; |
| int s_idx = 0, p_idx = 0; |
| while (s_idx < s.len && p_idx < p.len) { |
| if (s.data[s_idx] == p.data[p_idx]) { |
| p_idx++; |
| if (p_idx == p.len) { |
| matched = true; |
| break; |
| } |
| } |
| s_idx++; |
| } |
| |
| return matched; |
| } |
| |
| static void |
| append_match(context *ctx, find_all_result *r, object *obj, content *c) |
| { |
| r->matches = rerzalloc(ctx, r->matches, match, r->matches_count, r->matches_count + 1); |
| |
| match *m = &r->matches[r->matches_count++]; |
| m->fullname = c ? c->fullname : obj->fullname; |
| m->object = obj; |
| m->content = c; |
| } |
| |
| static find_all_result |
| find_all(context *ctx, const char *pattern) |
| { |
| find_all_result r = {}; |
| |
| if (!pattern) |
| pattern = ""; |
| |
| unsigned round_flags[2] = {}; |
| unsigned rounds = 1; |
| if (strchr(pattern, '/')) { |
| /* See comment on the enum definition. */ |
| round_flags[0] = MATCH_PREFIX_FIRST_SLASH; |
| rounds++; |
| } |
| |
| for (int round = 0; round < rounds; round++) { |
| unsigned match_flags = round_flags[round]; |
| |
| for (int i = 0; i < ctx->archives_count; i++) { |
| mesa_archive *ma = ctx->archives[i]; |
| |
| foreach_object(obj, ma) { |
| if (is_match(obj->fullname, pattern, match_flags)) |
| append_match(ctx, &r, obj, NULL); |
| } |
| } |
| |
| if (r.matches_count > 0) |
| return r; |
| |
| for (int i = 0; i < ctx->archives_count; i++) { |
| mesa_archive *ma = ctx->archives[i]; |
| |
| foreach_object(obj, ma) { |
| foreach_version(c, obj) { |
| if (is_match(c->fullname, pattern, match_flags)) |
| append_match(ctx, &r, obj, c); |
| } |
| } |
| } |
| |
| if (r.matches_count > 0) |
| return r; |
| } |
| |
| return r; |
| } |
| |
| static match |
| find_one(context *ctx, const char *pattern) |
| { |
| find_all_result r = find_all(ctx, pattern); |
| |
| if (r.matches_count == 1) { |
| return r.matches[0]; |
| |
| } else if (r.matches_count == 0) { |
| fprintf(stderr, "mda: couldn't match pattern: %s\n", pattern); |
| return (match){}; |
| |
| } else { |
| assert(r.matches_count > 1); |
| fprintf(stderr, "error: multiple matches for pattern: %s\n", pattern); |
| |
| for (int i = 0; i < r.matches_count; i++) { |
| match *m = &r.matches[i]; |
| fprintf(stderr, " %.*s\n", SLICE_FMT(m->fullname)); |
| } |
| return (match){}; |
| } |
| } |
| |
| static int |
| cmd_info(context *ctx) |
| { |
| for (int i = 0; i < ctx->archives_count; i++) { |
| if (i > 0) { |
| printf("\n"); |
| } |
| |
| mesa_archive *ma = ctx->archives[i]; |
| printf("# From %.*s\n", SLICE_FMT(ma->filename)); |
| printf("%s\n", ma->info); |
| } |
| |
| return 0; |
| } |
| |
| static int |
| cmd_listraw(context *ctx) |
| { |
| for (int i = 0; i < ctx->archives_count; i++) { |
| mesa_archive *ma = ctx->archives[i]; |
| |
| foreach_object(obj, ma) { |
| foreach_version(c, obj) { |
| printf("%.*s\n", SLICE_FMT(c->fullname)); |
| } |
| } |
| } |
| return 0; |
| } |
| |
| static int |
| cmd_list(context *ctx) |
| { |
| bool all = !strcmp(ctx->cmd_name, "listall"); |
| |
| for (int i = 0; i < ctx->archives_count; i++) { |
| if (i > 0) { |
| printf("\n"); |
| } |
| |
| mesa_archive *ma = ctx->archives[i]; |
| printf("%.*s/\n", SLICE_FMT(ma->filename)); |
| |
| const char *cur_name = ""; |
| |
| foreach_object(obj, ma) { |
| if (!slice_equal_cstr(obj->prefix, cur_name)) { |
| printf(" %.*s/\n", SLICE_FMT(obj->prefix)); |
| cur_name = slice_to_cstr(ctx, obj->prefix); |
| } |
| printf(" %.*s/", SLICE_FMT(obj->name)); |
| if (obj->versions_count > 1) |
| printf(" (%d versions)", obj->versions_count); |
| printf("\n"); |
| if (all) { |
| foreach_version(c, obj) { |
| printf(" %.*s\n", SLICE_FMT(c->name)); |
| } |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int |
| cmd_logsum(context *ctx) |
| { |
| if (ctx->args_count == 0) { |
| fprintf(stderr, "mda: need to pass an object to log\n"); |
| return 1; |
| } |
| |
| const char *pattern = ctx->args[0]; |
| |
| match m = find_one(ctx, pattern); |
| if (!m.object) |
| return 1; |
| |
| |
| printf("%.*s/\n", SLICE_FMT(m.object->fullname)); |
| |
| foreach_version(c, m.object) { |
| printf(" %.*s\n", SLICE_FMT(c->name)); |
| } |
| |
| printf("\n"); |
| |
| return 0; |
| } |
| |
| static int |
| cmd_diff(context *ctx) |
| { |
| if (ctx->args_count != 2 && ctx->args_count != 3) { |
| fprintf(stderr, "mda: invalid arguments\n"); |
| return 1; |
| } |
| |
| match a = find_one(ctx, ctx->args[0]); |
| if (!a.object) |
| return 1; |
| |
| match b = find_one(ctx, ctx->args[1]); |
| if (!b.object) |
| return 1; |
| |
| if (!a.content) |
| a.content = last_version(a.object); |
| if (!b.content) |
| b.content = last_version(b.object); |
| |
| int x = printf("# A: %.*s\n", SLICE_FMT(a.content->fullname)); |
| int y = printf("# B: %.*s\n", SLICE_FMT(b.content->fullname)); |
| print_repeated('#', MAX2(x, y) - 1); |
| printf("\n\n"); |
| |
| diff(ctx, a.content->data, b.content->data); |
| printf("\n"); |
| |
| return 0; |
| } |
| |
| static int |
| cmd_log(context *ctx) |
| { |
| if (ctx->args_count != 1 && ctx->args_count != 2) { |
| fprintf(stderr, "mda: need to pass one or two patterns to log command\n"); |
| return 1; |
| } |
| |
| enum mode { |
| MODE_DIFF, |
| MODE_ONELINE, |
| MODE_FULL, |
| }; |
| enum mode mode = !strcmp(ctx->cmd_name, "logfull") ? MODE_FULL : |
| !strcmp(ctx->cmd_name, "log1") ? MODE_ONELINE : |
| MODE_DIFF; |
| |
| const char *start_pattern = ctx->args[0]; |
| const char *end_pattern = ctx->args_count > 1 ? ctx->args[1] |
| : NULL; |
| |
| match start = find_one(ctx, start_pattern); |
| if (!start.object) |
| return 1; |
| if (!start.content) |
| start.content = first_version(start.object); |
| |
| match end = {}; |
| if (end_pattern) { |
| end = find_one(ctx, end_pattern); |
| if (!end.object) |
| return 1; |
| if (!end.content) |
| end.content = last_version(end.object); |
| } else { |
| end = start; |
| end.content = last_version(end.object); |
| } |
| |
| if (start.object != end.object) |
| failf("can't log between two different objects"); |
| object *obj = start.object; |
| |
| if (mode == MODE_ONELINE) { |
| printf("%.*s/\n", SLICE_FMT(obj->fullname)); |
| for (const content *curr = start.content; curr <= end.content; curr++) { |
| printf(" %.*s\n", SLICE_FMT(curr->name)); |
| } |
| |
| } else if (mode == MODE_FULL) { |
| for (const content *c = start.content; c <= end.content; c++) { |
| int x = printf("# %.*s/\n", SLICE_FMT(obj->fullname)); |
| int y = printf("# %.*s\n", SLICE_FMT(c->name)); |
| print_repeated('#', MAX2(x, y) - 1); |
| printf("\n\n"); |
| |
| printf("%.*s\n", SLICE_FMT(c->data)); |
| } |
| |
| } else { |
| for (const content *c = start.content; c < end.content; c++) { |
| const content *next = c + 1; |
| |
| int x = printf("# %.*s/\n", SLICE_FMT(obj->fullname)); |
| int y = printf("# %.*s -> %.*s\n", SLICE_FMT(c->name), SLICE_FMT(next->name)); |
| print_repeated('#', MAX2(x, y) - 1); |
| printf("\n\n"); |
| |
| diff(ctx, c->data, next->data); |
| printf("\n"); |
| } |
| } |
| |
| printf("\n"); |
| return 0; |
| } |
| |
| static slice |
| get_spirv_disassembly(void *mem_ctx, object *obj) |
| { |
| assert(slice_equal_cstr(obj->name, "SPV")); |
| assert(obj->versions_count == 1); |
| |
| content *c = &obj->versions[0]; |
| |
| int stdin_pipe[2], stdout_pipe[2]; |
| if (pipe(stdin_pipe) < 0 || pipe(stdout_pipe) < 0) |
| return (slice){}; |
| |
| pid_t pid = fork(); |
| if (pid < 0) { |
| close(stdin_pipe[0]); |
| close(stdin_pipe[1]); |
| close(stdout_pipe[0]); |
| close(stdout_pipe[1]); |
| return (slice){}; |
| } |
| |
| /* Child process. */ |
| if (pid == 0) { |
| close(stdin_pipe[1]); |
| close(stdout_pipe[0]); |
| |
| dup2(stdin_pipe[0], STDIN_FILENO); |
| dup2(stdout_pipe[1], STDOUT_FILENO); |
| |
| close(stdin_pipe[0]); |
| close(stdout_pipe[1]); |
| |
| execvp("spirv-dis", (char *[]){"spirv-dis", "--color", "-", NULL}); |
| |
| /* If exec fails, exit with error. */ |
| exit(1); |
| } |
| |
| close(stdin_pipe[0]); |
| close(stdout_pipe[1]); |
| |
| ssize_t written = write(stdin_pipe[1], c->data.data, c->data.len); |
| close(stdin_pipe[1]); |
| |
| struct util_dynarray output; |
| util_dynarray_init(&output, mem_ctx); |
| |
| if (written != (ssize_t)c->data.len) |
| goto wait_and_fail; |
| |
| char read_buffer[1024]; |
| ssize_t bytes_read; |
| while ((bytes_read = read(stdout_pipe[0], read_buffer, sizeof(read_buffer))) > 0) { |
| if (!util_dynarray_grow_bytes(&output, bytes_read, 1)) |
| goto wait_and_fail; |
| |
| memcpy((char *)output.data + output.size - bytes_read, read_buffer, bytes_read); |
| } |
| |
| close(stdout_pipe[0]); |
| |
| int status; |
| waitpid(pid, &status, 0); |
| |
| if (WEXITSTATUS(status) != 0 || output.size == 0) |
| goto fail; |
| |
| util_dynarray_append(&output, char, '\0'); |
| |
| return slice_from_cstr(output.data); |
| |
| wait_and_fail: |
| close(stdout_pipe[0]); |
| waitpid(pid, NULL, 0); |
| |
| fail: |
| failf("mda: error when running spirv-dis"); |
| return (slice){}; |
| } |
| |
| static int |
| print_disassembled_spirv(void *mem_ctx, object *obj) |
| { |
| slice disassembly = get_spirv_disassembly(mem_ctx, obj); |
| if (slice_is_empty(disassembly)) { |
| fprintf(stderr, "mda: failed to disassemble SPIR-V\n"); |
| return 1; |
| } |
| |
| printf("%.*s\n", SLICE_FMT(disassembly)); |
| return 0; |
| } |
| |
| static int |
| cmd_print(context *ctx) |
| { |
| const bool raw = !strcmp(ctx->cmd_name, "printraw"); |
| |
| if (ctx->args_count == 0) { |
| fprintf(stderr, "mda: need to pass an object to print\n"); |
| return 1; |
| } |
| |
| const char *pattern = ctx->args[0]; |
| |
| match m = find_one(ctx, pattern); |
| if (!m.object) |
| return 1; |
| |
| if (!m.content) |
| m.content = last_version(m.object); |
| |
| if (!raw) { |
| if (slice_equal_cstr(m.object->name, "SPV")) |
| return print_disassembled_spirv(ctx, m.object); |
| |
| int x = printf("### %.*s\n", SLICE_FMT(m.content->fullname)); |
| print_repeated('#', x-1); |
| printf("\n\n"); |
| } |
| |
| printf("%.*s", SLICE_FMT(m.content->data)); |
| |
| if (!raw) |
| printf("\n"); |
| |
| return 0; |
| } |
| |
| static int |
| print_search_matches(slice content, slice search_string, slice fullname) |
| { |
| #define CONTEXT_SIZE 2 |
| int match_count = 0; |
| |
| /* Keep track of previous non-matching lines in case a matching line |
| * is found, so that context can be printed. |
| */ |
| slice prev_lines[CONTEXT_SIZE]; |
| int unprinted_prev_lines = 0; |
| |
| /* Allow to "merge" multiple matches that are near to each other |
| * in a single block of output. |
| */ |
| int lines_since_match = -1; |
| |
| slice remaining = content; |
| int line_num = 1; |
| |
| while (!slice_is_empty(remaining)) { |
| slice_cut_result cut = slice_cut(remaining, '\n'); |
| slice line = cut.found ? cut.before : remaining; |
| |
| if (slice_contains_str(line, search_string)) { |
| if (match_count == 0) |
| printf("=== %.*s ===\n", SLICE_FMT(fullname)); |
| |
| for (int i = 0; i < unprinted_prev_lines; i++) { |
| int prev_line_num = line_num - unprinted_prev_lines + i; |
| printf("%5d: %.*s\n", prev_line_num, SLICE_FMT(prev_lines[i])); |
| } |
| unprinted_prev_lines = 0; |
| |
| printf("%5d: %.*s\n", line_num, SLICE_FMT(line)); |
| |
| match_count++; |
| lines_since_match = 0; |
| |
| } else { |
| /* Print context after a match. */ |
| if (lines_since_match >= 0) { |
| if (lines_since_match < CONTEXT_SIZE) { |
| printf("%5d: %.*s\n", line_num, SLICE_FMT(line)); |
| lines_since_match++; |
| } else { |
| printf("\n"); |
| lines_since_match = -1; |
| } |
| } |
| |
| /* Maintain the sliding window of previous lines only |
| * if haven't printed them right above. |
| */ |
| if (lines_since_match < 0) { |
| if (unprinted_prev_lines < CONTEXT_SIZE) { |
| prev_lines[unprinted_prev_lines++] = line; |
| } else { |
| /* Shift. */ |
| for (int i = 0; i < CONTEXT_SIZE - 1; i++) |
| prev_lines[i] = prev_lines[i + 1]; |
| prev_lines[CONTEXT_SIZE - 1] = line; |
| } |
| } |
| } |
| |
| line_num++; |
| remaining = cut.after; |
| } |
| |
| if (match_count > 0) |
| printf("\n"); |
| |
| return match_count; |
| } |
| |
| static int |
| cmd_search(context *ctx) |
| { |
| bool search_all = !strcmp(ctx->cmd_name, "searchall"); |
| |
| if (ctx->args_count < 1 || ctx->args_count > 2) { |
| fprintf(stderr, "mda: %s requires 1-2 arguments\n", ctx->cmd_name); |
| return 1; |
| } |
| |
| slice search_string = slice_from_cstr(ctx->args[0]); |
| const char *pattern = ctx->args_count > 1 ? ctx->args[1] : ""; |
| |
| find_all_result matches = find_all(ctx, pattern); |
| int found_count = 0; |
| |
| for (int i = 0; i < matches.matches_count; i++) { |
| match *m = &matches.matches[i]; |
| |
| /* SPIR-V object has only one version. We probably could clean up |
| * handling of it here and elsewhere to something more general |
| * if we ever get another "special" object. |
| */ |
| const bool is_spirv = slice_equal_cstr(m->object->name, "SPV"); |
| |
| if (search_all && !is_spirv) { |
| foreach_version(c, m->object) |
| found_count += print_search_matches(c->data, search_string, c->fullname); |
| |
| } else { |
| content *latest = last_version(m->object); |
| |
| slice search_data; |
| if (is_spirv) |
| search_data = get_spirv_disassembly(m->object->ma, m->object); |
| else |
| search_data = latest->data; |
| |
| found_count += print_search_matches(search_data, search_string, latest->fullname); |
| } |
| } |
| |
| if (found_count == 0) |
| printf("No matches found\n"); |
| else |
| printf("Found %d match%s\n", found_count, found_count == 1 ? "" : "es"); |
| |
| return 0; |
| } |
| |
| static void |
| open_manual() |
| { |
| FILE *f = NULL; |
| |
| /* This fd will be set as stdin for executing man. */ |
| int fd = memfd_create("mda.1", 0); |
| if (fd != -1) |
| f = fdopen(fd, "w"); |
| |
| if (!f) { |
| /* Fallback to just printing the content out. */ |
| f = stderr; |
| } |
| |
| static const char *contents[] = { |
| ".TH mda 1 2025-03-29", |
| "", |
| ".SH NAME", |
| "", |
| "mda - reads mesa debugging archive files", |
| "", |
| ".SH SYNOPSIS", |
| "", |
| "mda [[-f FILE]... [-U[nnn]] [-Y[nnn]]] COMMAND [args]", |
| "", |
| ".SH DESCRIPTION", |
| "", |
| "Reads *.mda.tar files generated by Mesa drivers, these", |
| "files contain debugging information about a pipeline or", |
| "a single shader stage.", |
| "", |
| "Without command, all the objects are listed, an object can", |
| "be a particular internal shader form or other metadata.", |
| "Objects are identified by fuzzy matching a PATTERN with their", |
| "names. Names can be seen in 'list' commands.", |
| "", |
| "Objects may have multiple versions, e.g. multiple steps", |
| "of a shader generated during optimization. When not", |
| "specified in the PATTERN, commands pick a relevant version,", |
| "either first or last).", |
| "", |
| "By default all *.mda.tar files in the current directory are read.", |
| "To specify which files to read use one or more `-f FILENAME` flags", |
| "before the command.", |
| "", |
| ".SH COMMANDS", |
| "", |
| " list list objects", |
| "", |
| " listall list all versions of objects", |
| "", |
| " listraw list all versions of objects with full names", |
| "", |
| " print PATTERN formatted print an object", |
| "", |
| " printraw PATTERN unformatted print an object", |
| "", |
| " log PATTERN [PATTERN] print changes between versions of an object", |
| "", |
| " logfull PATTERN [PATTERN] print full contents of versions of an object", |
| "", |
| " log1 PATTERN [PATTERN] print names of the versions of an object", |
| "", |
| " diff PATTERN PATTERN compare two objects", |
| "", |
| " search STRING [PATTERN] search latest versions for string", |
| "", |
| " searchall STRING [PATTERN] search all versions for string", |
| "", |
| " info print metadata about the archive", |
| "", |
| ".SH OPTIONS", |
| "", |
| " -f FILENAME read from specific archive file", |
| "", |
| " -U[nnn] use unified diff (default: 5 context lines)", |
| "", |
| " -Y[nnn] use side-by-side diff (default: 240 width)", |
| "", |
| "The -U and -Y options are mutually exclusive. If neither is specified,", |
| "-U5 is used by default.", |
| "", |
| ".SH ENVIRONMENT VARIABLES", |
| "", |
| "The diff program used by mda can be configured by setting", |
| "the MDA_DIFF_COMMAND environment variable, which overrides", |
| "the -U and -Y options. Without MDA_DIFF_COMMAND:", |
| "", |
| " -U uses: git diff --no-index --color-words -Unnn -- %s %s | tail -n +5", |
| " -Y uses: diff -y -Wnnn %s %s", |
| "", |
| "When showing SPIR-V files, spirv-dis tool is used.", |
| "" |
| }; |
| |
| for (int i = 0; i < ARRAY_SIZE(contents); i++) { |
| fputs(contents[i], f); |
| putc('\n', f); |
| } |
| |
| fflush(f); |
| |
| if (f != stderr) { |
| /* Inject the temporary as stdin for man. */ |
| lseek(fd, 0, SEEK_SET); |
| dup2(fd, STDIN_FILENO); |
| fclose(f); |
| |
| execlp("man", "man", "-l", "-", (char *)NULL); |
| } else { |
| exit(0); |
| } |
| } |
| |
| static void |
| print_help() |
| { |
| printf("mda [[-f FILENAME]... [-U[nnn]] [-Y[nnn]]] CMD [ARGS...]\n" |
| "\n" |
| "OPTIONS\n" |
| "\n" |
| " -f FILENAME read from specific archive file\n" |
| " -U[nnn] use unified diff (default: 5 context lines)\n" |
| " -Y[nnn] use side-by-side diff (default: 240 width)\n" |
| "\n" |
| "COMMANDS\n" |
| "\n" |
| " list list objects\n" |
| " listall list all versions of objects\n" |
| " listraw list all versions of objects with full names\n" |
| " print PATTERN formatted print an object\n" |
| " printraw PATTERN unformatted print an object\n" |
| " log PATTERN [PATTERN] print changes between versions of an object\n" |
| " logfull PATTERN [PATTERN] print full contents of versions of an object\n" |
| " log1 PATTERN [PATTERN] print names of the versions of an object\n" |
| " diff PATTERN PATTERN compare two objects\n" |
| " search STRING [PATTERN] search latest versions for string\n" |
| " searchall STRING [PATTERN] search all versions for string\n" |
| " info print metadata about the archive\n" |
| "\n" |
| "ENVIRONMENT VARIABLES\n" |
| "\n" |
| " MDA_DIFF_COMMAND custom diff command (overrides -U/-Y)\n" |
| "\n" |
| "Default diff mode is -U5 (unified diff with 5 context lines).\n" |
| "For more details, use 'mda help' to open the manual.\n"); |
| } |
| |
| static bool |
| load_archive(context *ctx, const char *filename) |
| { |
| struct mesa_archive *ma = parse_mesa_archive(ctx, filename); |
| if (!ma) |
| return false; |
| |
| ctx->archives = rerzalloc(ctx, ctx->archives, mesa_archive *, ctx->archives_count, |
| ctx->archives_count + 1); |
| ctx->archives[ctx->archives_count] = ma; |
| ctx->archives_count++; |
| return true; |
| } |
| |
| static pid_t |
| setup_pager() |
| { |
| if (!isatty(STDOUT_FILENO) || |
| os_get_option("NO_PAGER")) |
| return 0; |
| |
| const char *term = os_get_option("TERM"); |
| if (!term || !strcmp(term, "dumb")) |
| return 0; |
| |
| int pipefd[2]; |
| if (pipe(pipefd) == -1) { |
| fprintf(stderr, "mda: couldn't create pipe for pager\n"); |
| return 0; |
| } |
| |
| pid_t pid = fork(); |
| if (pid == -1) { |
| close(pipefd[0]); |
| close(pipefd[1]); |
| fprintf(stderr, "mda: couldn't open pager\n"); |
| return 0; |
| } |
| |
| if (pid == 0) { |
| /* Child stdin will read from pipe. */ |
| close(pipefd[1]); |
| dup2(pipefd[0], STDIN_FILENO); |
| close(pipefd[0]); |
| |
| const char *pager = os_get_option("PAGER"); |
| if (pager && *pager) |
| execlp(pager, pager, NULL); |
| |
| execlp("less", "less", "-FSRi", NULL); |
| execlp("more", "more", NULL); |
| execlp("cat", "cat", NULL); |
| exit(1); |
| } |
| |
| /* Parent stdout will point to pipe. */ |
| close(pipefd[0]); |
| dup2(pipefd[1], STDOUT_FILENO); |
| close(pipefd[1]); |
| |
| return pid; |
| } |
| |
| int |
| main(int argc, char *argv[]) |
| { |
| if (argc >= 2) { |
| if (!strcmp(argv[1], "help") || |
| !strcmp(argv[1], "--help")) { |
| open_manual(); |
| return 0; |
| } else if (!strcmp(argv[1], "-h")) { |
| print_help(); |
| return 0; |
| } |
| } |
| |
| context *ctx = rzalloc(NULL, context); |
| ctx->diff.mode = DIFF_UNIFIED; |
| ctx->diff.param = 5; |
| |
| bool diff_set = false; |
| int cur_arg = 1; |
| |
| while (cur_arg < argc && argv[cur_arg][0] == '-') { |
| if (!strcmp(argv[cur_arg], "-f")) { |
| if (argc == cur_arg + 1) |
| failf("mda: missing filename after -f flag\n"); |
| |
| const char *filename = argv[cur_arg + 1]; |
| cur_arg += 2; |
| |
| for (int i = 0; i < ctx->archives_count; i++) { |
| mesa_archive *ma = ctx->archives[i]; |
| |
| /* Don't load duplicate files from command line. */ |
| if (slice_equal_cstr(ma->filename, filename)) { |
| filename = NULL; |
| break; |
| } |
| } |
| |
| if (filename && !load_archive(ctx, filename)) |
| failf("mda: failed to parse file: %s\n", filename); |
| } else if (argv[cur_arg][1] == 'U' || argv[cur_arg][1] == 'Y') { |
| if (diff_set) |
| failf("mda: -U and -Y options are mutually exclusive\n"); |
| |
| diff_set = true; |
| ctx->diff.mode = (argv[cur_arg][1] == 'U') ? DIFF_UNIFIED : DIFF_SIDE_BY_SIDE; |
| |
| /* Parse optional numeric parameter. */ |
| if (argv[cur_arg][2] != '\0') |
| ctx->diff.param = atoi(&argv[cur_arg][2]); |
| else |
| ctx->diff.param = ctx->diff.mode == DIFF_UNIFIED ? 5 : 240; |
| |
| cur_arg++; |
| } else { |
| /* Unknown flag, stop parsing flags */ |
| break; |
| } |
| } |
| |
| if (ctx->archives_count == 0) { |
| /* Load all mda files in the current directory. */ |
| DIR *d; |
| struct dirent *dir; |
| d = opendir("."); |
| if (!d) |
| failf("mda: couldn't find *.mda.tar files in current directory: %s\n", strerror(errno)); |
| |
| while ((dir = readdir(d)) != NULL) { |
| slice filename = slice_from_cstr(dir->d_name); |
| slice mda_ext = slice_from_cstr(".mda.tar"); |
| if (slice_ends_with(filename, mda_ext)) { |
| if (!load_archive(ctx, dir->d_name)) { |
| fprintf(stderr, "mda: ignoring file after parsing failure: %s\n", dir->d_name); |
| continue; |
| } |
| } |
| } |
| closedir(d); |
| |
| if (ctx->archives_count == 0) |
| failf("Couldn't load any *.mda.tar files in the current directory\n"); |
| } |
| |
| ctx->cmd_name = cur_arg < argc ? argv[cur_arg++] : "list"; |
| ctx->args_count = argc - cur_arg; |
| ctx->args = rzalloc_array(ctx, char *, argc - cur_arg + 1); |
| for (int i = 0; i < ctx->args_count; i++) |
| ctx->args[i] = ralloc_strdup(ctx, argv[cur_arg + i]); |
| |
| struct command { |
| const char *name; |
| int (*func)(context *ctx); |
| bool skip_pager; |
| }; |
| |
| static const struct command cmds[] = { |
| { "diff", cmd_diff }, |
| { "info", cmd_info, .skip_pager = true }, |
| { "list", cmd_list }, |
| { "listall", cmd_list }, |
| { "listraw", cmd_listraw }, |
| { "log", cmd_log }, |
| { "log1", cmd_log }, |
| { "logfull", cmd_log }, |
| { "print", cmd_print }, |
| { "printraw", cmd_print, .skip_pager = true }, |
| { "search", cmd_search }, |
| { "searchall", cmd_search }, |
| }; |
| |
| const struct command *cmd = NULL; |
| for (const struct command *c = cmds; c < cmds + ARRAY_SIZE(cmds); c++) { |
| if (!strcmp(c->name, ctx->cmd_name)) { |
| cmd = c; |
| break; |
| } |
| } |
| |
| if (!cmd) { |
| fprintf(stderr, "mda: unknown command '%s'\n", ctx->cmd_name); |
| print_help(); |
| return 1; |
| } |
| |
| pid_t pid = cmd->skip_pager ? -1 : setup_pager(); |
| |
| int r = cmd->func(ctx); |
| ralloc_free(ctx); |
| |
| if (pid > 0) { |
| fflush(stdout); |
| fclose(stdout); |
| waitpid(pid, NULL, 0); |
| } |
| |
| return r; |
| } |