Skip to content

Instantly share code, notes, and snippets.

@Cloudef
Created January 19, 2024 06:26
Show Gist options
  • Save Cloudef/b8c056ee6aff55aac5b5266e88876e76 to your computer and use it in GitHub Desktop.
Save Cloudef/b8c056ee6aff55aac5b5266e88876e76 to your computer and use it in GitHub Desktop.
const std = @import("std");
const builtin = @import("builtin");
const log = std.log.scoped(.runtime);
fn setenv(key: []const u8, value: []const u8) !void {
// Potential footgun here?
// It is possible to do a version that does not rely on std.os.environ at all using prctl and reading /proc/self/environ.
// https://github.com/ziglang/zig/issues/4524
for (std.os.environ) |*kv| {
var token = std.mem.splitScalar(u8, std.mem.span(kv.*), '=');
const env_key = token.first();
if (std.mem.eql(u8, env_key, key)) {
const allocator = if (builtin.link_libc) std.heap.c_allocator else std.heap.page_allocator;
// XXX: non c_allocator is leaky
if (builtin.link_libc) try allocator.free(kv.*);
var buf = try allocator.allocSentinel(u8, key.len + value.len + 1, 0);
@memcpy(buf[0..key.len], key[0..]);
buf[key.len] = '=';
@memcpy(buf[key.len + 1..], value[0..]);
kv.* = buf;
return;
}
}
}
const LinuxOptions = struct {
sonames: []const []const u8 = &.{},
};
pub const OsRelease = struct {
// Only because BoundedArray has no comptime len field or const
// and @sizeOf(bounded.buffer) apparently isn't comptime O_o
const bufsz = 4096;
bounded: std.BoundedArray(u8, bufsz) = .{},
bug_report_url: ?[]const u8 = null,
build_id: ?[]const u8 = null,
documentation_url: ?[]const u8 = null,
home_url: ?[]const u8 = null,
id: ?[]const u8 = null,
logo: ?[]const u8 = null,
name: ?[]const u8 = null,
pretty_name: ?[]const u8 = null,
support_url: ?[]const u8 = null,
version: ?[]const u8 = null,
version_codename: ?[]const u8 = null,
version_id: ?[]const u8 = null,
pub fn init() ?@This() {
var this: @This() = .{};
{
const f = std.fs.openFileAbsolute("/etc/os-release", .{.mode = .read_only}) catch return null;
defer f.close();
f.reader().readIntoBoundedBytes(bufsz, &this.bounded) catch return null;
}
var tokens = std.mem.splitAny(u8, this.bounded.constSlice(), "=\n");
while (tokens.next()) |key| if (tokens.next()) |value| {
inline for (std.meta.fields(@This())) |f| {
if (comptime !std.mem.eql(u8, f.name, "bounded")) {
comptime var upper: [f.name.len]u8 = undefined;
_ = comptime std.ascii.upperString(&upper, f.name);
if (std.mem.eql(u8, upper[0..], key)) {
const stripped = if (value.len > 0 and value[0] == '"') value[1..value.len - 1] else value;
@field(this, f.name) = stripped;
std.log.debug("{s} = {s}", .{key, stripped});
}
}
}
};
return this;
}
};
const Distro = enum {
nixos,
guix,
gobolinux,
other,
};
// https://old.reddit.com/r/linuxquestions/comments/62g28n/deleted_by_user/dfmjht6/
fn detectDistro() Distro {
if (std.os.getenv("DISTRO_OVERRIDE")) |env| {
return std.meta.stringToEnum(Distro, env) orelse .other;
}
if (OsRelease.init()) |distro| {
if (distro.id) |id| {
std.log.info("Detected Linux distribution: {s}", .{
distro.pretty_name orelse distro.name orelse id
});
if (std.meta.stringToEnum(Distro, id)) |d| return d;
}
}
if (std.fs.accessAbsolute("/run/current-system/nixos-version", .{.mode = .read_only})) {
std.log.info("Detected Linux distribution: NixOS", .{});
return .nixos;
} else |_| {}
if (std.fs.accessAbsolute("/etc/GoboLinuxVersion", .{.mode = .read_only})) {
std.log.info("Detected Linux distribution: GoboLinux", .{});
return .gobolinux;
} else |_| {}
std.log.info("Unknown Linux distribution", .{});
return .other;
}
fn setupLinux(allocator: std.mem.Allocator, comptime opts: LinuxOptions) !void {
switch (detectDistro()) {
.nixos => {
// packages that match soname, don't have to be included
const map = std.comptime_string_map.ComptimeStringMap([]const u8, .{
.{ "libvulkan", "vulkan-loader" },
.{ "libGL", "libglvnd" },
.{ "libEGL", "libglvnd" },
.{ "libGLdispatch", "libglvnd" },
.{ "libGLESv1_CM", "libglvnd" },
.{ "libGLESv2", "libglvnd" },
.{ "libGLX", "libglvnd" },
.{ "libOSMesa", "mesa" },
.{ "libOpenGL", "libglvnd" },
.{ "libX11-xcb", "libX11" },
.{ "libwayland-client", "wayland" },
.{ "libwayland-cursor", "wayland" },
.{ "libwayland-server", "wayland" },
.{ "libwayland-egl", "wayland" },
.{ "libdecor-0", "libdecor" },
.{ "libgamemode", "gamemode" },
});
var env = std.ArrayListUnmanaged(u8){};
defer env.deinit(allocator);
var env_writer = env.writer(allocator);
// get relevant nix store paths
const store = blk: {
const cmd = "nix-store -q --tree /run/current-system | grep -o '/nix/store/[^ ]*' | sort -u";
const rr = try std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "sh", "-c", cmd },
.max_output_bytes = std.math.maxInt(usize) / 2,
});
defer allocator.free(rr.stderr);
break :blk rr.stdout;
};
defer allocator.free(store);
// this part most likely can be reused for guix and gobolinux
var tmp = std.BoundedArray(u8, 1024){};
inline for (opts.sonames) |soname| {
comptime var split = std.mem.splitScalar(u8, soname, '.');
const base = comptime split.first();
const pkg = comptime map.get(base) orelse base;
const needle = std.fmt.comptimePrint("-{s}-", .{pkg});
var found_any = false;
var paths = std.mem.tokenizeScalar(u8, store, '\n');
while (paths.next()) |path| {
if (std.mem.count(u8, path, needle) == 0) continue;
try tmp.resize(0);
try tmp.writer().print("{s}/lib", .{path});
if (std.mem.count(u8, env.items, tmp.constSlice()) > 0) {
// duplicate
found_any = true;
continue;
}
if (std.fs.accessAbsolute(tmp.constSlice(), .{.mode = .read_only})) {
if (env.items.len == 0) {
// append to the LD_LIBRARY_PATH rather than prepend, so we don't mess with user's own preferences
if (std.os.getenv("LD_LIBRARY_PATH")) |path0| {
try env_writer.writeAll(path0);
}
}
try env_writer.print("{s}{s}/lib", .{ if (env.items.len > 0) ":" else "", path });
found_any = true;
} else |_| {}
}
if (!found_any) {
log.warn("missing library: {s}", .{soname});
}
}
if (env.items.len > 0) {
try setenv("LD_LIBRARY_PATH", env.items);
}
},
.guix => std.log.warn("guix is untested: things may not work", .{}),
.gobolinux => std.log.warn("gobolinux is untested: things may not work", .{}),
.other => {},
}
}
const Options = struct {
linux: LinuxOptions = .{},
};
pub fn setup(allocator: std.mem.Allocator, comptime opts: Options) void {
switch (builtin.os.tag) {
.linux => setupLinux(allocator, opts.linux) catch |err| {
log.warn("{}: runtime is incomplete and the program may not function properly", .{err});
},
else => {},
}
}
pub fn main() !void {
setup(std.heap.page_allocator, .{
.linux = .{
.sonames = &.{
"libGL.so",
"libEGL.so",
"libOSMesa.so.8",
"libXext.so.6",
"libXi.so.6",
"libOSMesa.so.6",
"libX11.so.6",
"libXrandr.so.2",
"libGLESv2.so.2",
"libXcursor.so.1",
"libXrender.so.1",
"libvulkan.so.1",
"libXxf86vm.so.1",
"libwayland-egl.so.1",
"libX11-xcb.so.1",
"libXinerama.so.1",
"libGLES_CM.so.1",
"libGLESv1_CM.so.1",
"libGL.so.1",
"libEGL.so.1",
"libwayland-client.so.0",
"libwayland-cursor.so.0",
"libxkbcommon.so.0",
"libGLX.so.0",
"libOpenGL.so.0",
"libdecor-0.so.0",
"libgamemode.so.0",
},
}
});
if (std.os.getenv("LD_LIBRARY_PATH")) |path0| log.warn("{s}", .{path0});
var args = try std.process.argsAlloc(std.heap.page_allocator);
defer std.process.argsFree(std.heap.page_allocator, args);
_ = try std.process.Child.run(.{ .allocator = std.heap.page_allocator, .argv = args[1..] });
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment