[build.zig]: a concrete approach to build for web

This commit is contained in:
haxsam 2025-08-31 16:16:49 +02:00 committed by Nikolas
parent df70c5c952
commit b44a355a81
7 changed files with 211 additions and 238 deletions

View File

@ -55,9 +55,9 @@ want to run an example, say `basic_window` run `zig build basic_window`
### Using raylib-zig's template ### Using raylib-zig's template
* Execute `project_setup.sh project_name`, this will create a folder with the name specified - Execute `project_setup.sh project_name`, this will create a folder with the name specified
* You can copy that folder anywhere you want and edit the source - You can copy that folder anywhere you want and edit the source
* Run `zig build run` at any time to test your project - Run `zig build run` at any time to test your project
### In an existing project (e.g. created with `zig init`) ### In an existing project (e.g. created with `zig init`)
@ -88,8 +88,8 @@ exe.root_module.addImport("raylib", raylib);
exe.root_module.addImport("raygui", raygui); exe.root_module.addImport("raygui", raygui);
``` ```
If you additionally want to support Web as a platform with emscripten, you will need to use `emcc.zig` by importing If you additionally want to support Web as a platform with emscripten, you will need to use `emsdk` by importing
raylib-zig's build script with `const rlz = @import("raylib_zig");` and then accessing its functions with `rlz.emcc`. raylib-zig's build script with `const rlz = @import("raylib_zig");` and then accessing like described here [Exporting for web](https://github.com/raylib-zig/raylib-zig?tab=readme-ov-file#exporting-for-web).
Refer to raylib-zig's project template on how to use them. Refer to raylib-zig's project template on how to use them.
### Passing build options ### Passing build options
@ -118,16 +118,50 @@ raylib_artifact.root_module.addCMacro("SUPPORT_FILEFORMAT_JPG", "");
## Exporting for web ## Exporting for web
To export your project for the web, first install emsdk. To export your project for the web, first add emsdk to your dependencies.
Once emsdk is installed, set it up by running Its also possible to use a local emsdk folder.
`emsdk install latest` `zig fetch --save git+https://github.com/emscripten-core/emsdk#4.0.9`
Find the folder where it's installed and run Add this to your build method to build for the web
`zig build -Dtarget=wasm32-emscripten --sysroot [path to emsdk]/upstream/emscripten` ```zig
if (target.query.os_tag == .emscripten) {
const emsdk = rlz.emsdk;
const wasm = b.addLibrary(.{
.name = <your_project_name>,
.root_module = exe_mod,
});
once that is finished, the exported project should be located at `zig-out/htmlout` const install_dir: std.Build.InstallDir = .{ .custom = "web" };
const emcc_flags = emsdk.emccDefaultFlags(b.allocator, .{ .optimize = optimize });
const emcc_settings = emsdk.emccDefaultSettings(b.allocator, .{ .optimize = optimize });
const emcc_step = emsdk.emccStep(b, raylib_artifact, wasm, .{
.optimize = optimize,
.flags = emcc_flags,
.settings = emcc_settings,
.install_dir = install_dir,
});
b.getInstallStep().dependOn(emcc_step);
const html_filename = try std.fmt.allocPrint(b.allocator, "{s}.html", .{wasm.name});
const emrun_step = emsdk.emrunStep(
b,
b.getInstallPath(install_dir, html_filename),
&.{},
);
emrun_step.dependOn(emcc_step);
run_step.dependOn(emrun_step);
}
```
then you can run
`zig build -Dtarget=wasm32-emscripten`
once that is finished, the exported project should be located at `zig-out/web`
### When is the binding updated? ### When is the binding updated?
@ -136,6 +170,6 @@ implementation stuff should be updatable with some hacks on your side.
### What needs to be done? ### What needs to be done?
+ _(Done)_ Set up a proper package build and a build script for the examples - _(Done)_ Set up a proper package build and a build script for the examples
+ Port all the examples - Port all the examples
+ Member functions/initialisers - Member functions/initialisers

View File

@ -3,8 +3,7 @@
const std = @import("std"); const std = @import("std");
const this = @This(); const this = @This();
const rl = @import("raylib"); const rl = @import("raylib");
pub const emsdk = rl.emsdk;
pub const emcc = @import("emcc.zig");
pub const Options = rl.Options; pub const Options = rl.Options;
pub const OpenglVersion = rl.OpenglVersion; pub const OpenglVersion = rl.OpenglVersion;
@ -27,7 +26,7 @@ fn getRaylib(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
.rtext = options.rtext, .rtext = options.rtext,
.rtextures = options.rtextures, .rtextures = options.rtextures,
.platform = options.platform, .platform = options.platform,
.shared = options.shared, .linkage = options.linkage,
.linux_display_backend = options.linux_display_backend, .linux_display_backend = options.linux_display_backend,
.opengl_version = options.opengl_version, .opengl_version = options.opengl_version,
.android_api_version = options.android_api_version, .android_api_version = options.android_api_version,
@ -390,33 +389,54 @@ pub fn build(b: *std.Build) !void {
const examples_step = b.step("examples", "Builds all the examples"); const examples_step = b.step("examples", "Builds all the examples");
for (examples) |ex| { for (examples) |ex| {
const mod = b.createModule(.{
.root_source_file = b.path(ex.path),
.target = target,
.optimize = optimize,
});
if (target.query.os_tag == .emscripten) { if (target.query.os_tag == .emscripten) {
const exe_lib = try emcc.compileForEmscripten(b, ex.name, ex.path, target, optimize); const wasm = b.addLibrary(.{
exe_lib.root_module.addImport("raylib", raylib); .name = ex.name,
exe_lib.root_module.addImport("raygui", raygui); .root_module = mod,
});
wasm.root_module.addImport("raylib", raylib);
wasm.root_module.addImport("raygui", raygui);
wasm.linkLibrary(raylib_artifact);
// Note that raylib itself isn't actually added to the exe_lib const install_dir: std.Build.InstallDir = .{ .custom = "web" };
// output file, so it also needs to be linked with emscripten. const emcc_flags = emsdk.emccDefaultFlags(b.allocator, .{
exe_lib.linkLibrary(raylib_artifact); .optimize = optimize,
const link_step = try emcc.linkWithEmscripten(b, &[_]*std.Build.Step.Compile{ exe_lib, raylib_artifact }); .asyncify = !std.mem.endsWith(u8, ex.name, "web"),
link_step.addArg("--emrun"); });
link_step.addArg("--embed-file"); const emcc_settings = emsdk.emccDefaultSettings(b.allocator, .{
link_step.addArg("resources/"); .optimize = optimize,
});
const emcc_step = emsdk.emccStep(b, raylib_artifact, wasm, .{
.optimize = optimize,
.flags = emcc_flags,
.settings = emcc_settings,
.shell_file_path = emsdk.shell(b),
.install_dir = install_dir,
.embed_paths = &.{.{ .src_path = "resources/" }},
});
const html_filename = try std.fmt.allocPrint(b.allocator, "{s}.html", .{wasm.name});
const emrun_step = emsdk.emrunStep(
b,
b.getInstallPath(install_dir, html_filename),
&.{},
);
emrun_step.dependOn(emcc_step);
const run_step = try emcc.emscriptenRunStep(b);
run_step.step.dependOn(&link_step.step);
const run_option = b.step(ex.name, ex.desc); const run_option = b.step(ex.name, ex.desc);
run_option.dependOn(emrun_step);
run_option.dependOn(&run_step.step); examples_step.dependOn(emcc_step);
examples_step.dependOn(&exe_lib.step);
} else { } else {
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = ex.name, .name = ex.name,
.root_module = b.createModule(.{ .root_module = mod,
.root_source_file = b.path(ex.path),
.target = target,
.optimize = optimize,
}),
}); });
exe.linkLibrary(raylib_artifact); exe.linkLibrary(raylib_artifact);
exe.root_module.addImport("raylib", raylib); exe.root_module.addImport("raylib", raylib);

View File

@ -4,13 +4,17 @@
.fingerprint = 0xc4cfa8c610114f28, .fingerprint = 0xc4cfa8c610114f28,
.dependencies = .{ .dependencies = .{
.raylib = .{ .raylib = .{
.url = "git+https://github.com/raysan5/raylib?ref=master#f83c5cb6e1b3462c9b723c0ab3b479951aa48458", .url = "git+https://github.com/raysan5/raylib#82d65e110a2caba862b31a3f1c11ca7a8ba60e3b",
.hash = "raylib-5.5.0-whq8uBlJxwRAvOHHNQIi8WS0QTbQJcdx7FbjSSOnPn6n", .hash = "raylib-5.6.0-dev-whq8uHRjyARJNFHTJ8fBtYU71IrnB-rTHJwuasjUnRWt",
}, },
.raygui = .{ .raygui = .{
.url = "git+https://github.com/raysan5/raygui#6530ee136b3c5af86c5640151f07837a604308ec", .url = "git+https://github.com/raysan5/raygui#6530ee136b3c5af86c5640151f07837a604308ec",
.hash = "N-V-__8AAOQabwCjOjMI2uUTw4Njc0tAUOO6Lw2kCydLbvVG", .hash = "N-V-__8AAOQabwCjOjMI2uUTw4Njc0tAUOO6Lw2kCydLbvVG",
}, },
.emsdk = .{
.url = "git+https://github.com/emscripten-core/emsdk#4.0.9",
.hash = "N-V-__8AAJl1DwBezhYo_VE6f53mPVm00R-Fk28NPW7P14EQ",
},
}, },
.minimum_zig_version = "0.15.1", .minimum_zig_version = "0.15.1",
.paths = .{ .paths = .{

126
emcc.zig
View File

@ -1,126 +0,0 @@
// raylib-zig (c) Nikolas Wipper 2020-2024
const std = @import("std");
const builtin = @import("builtin");
const emccOutputDir = "zig-out" ++ std.fs.path.sep_str ++ "htmlout" ++ std.fs.path.sep_str;
const emccOutputFile = "index.html";
pub fn emscriptenRunStep(b: *std.Build) !*std.Build.Step.Run {
// If compiling on windows , use emrun.bat.
const emrunExe = switch (builtin.os.tag) {
.windows => "emrun.bat",
else => "emrun",
};
var emrun_run_arg = try b.allocator.alloc(u8, b.sysroot.?.len + emrunExe.len + 1);
defer b.allocator.free(emrun_run_arg);
if (b.sysroot == null) {
emrun_run_arg = try std.fmt.bufPrint(emrun_run_arg, "{s}", .{emrunExe});
} else {
emrun_run_arg = try std.fmt.bufPrint(emrun_run_arg, "{s}" ++ std.fs.path.sep_str ++ "{s}", .{ b.sysroot.?, emrunExe });
}
const run_cmd = b.addSystemCommand(&[_][]const u8{ emrun_run_arg, emccOutputDir ++ emccOutputFile });
return run_cmd;
}
// Creates the static library to build a project for Emscripten.
pub fn compileForEmscripten(
b: *std.Build,
name: []const u8,
root_source_file: []const u8,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
) !*std.Build.Step.Compile {
// TODO: It might be a good idea to create a custom compile step, that does
// both the compile to static library and the link with emcc by overidding
// the make function of the step. However it might also be a bad idea since
// it messes with the build system itself.
// The project is built as a library and linked later.
const lib = b.addLibrary(.{
.name = name,
.linkage = .static,
.root_module = b.createModule(.{
.root_source_file = b.path(root_source_file),
.target = target,
.optimize = optimize,
}),
});
const emscripten_headers = try std.fs.path.join(b.allocator, &.{ b.sysroot.?, "cache", "sysroot", "include" });
defer b.allocator.free(emscripten_headers);
lib.addIncludePath(.{ .cwd_relative = emscripten_headers });
return lib;
}
// Links a set of items together using emscripten.
//
// Will accept objects and static libraries as items to link. As for files to
// include, it is recomended to have a single resources directory and just pass
// the entire directory instead of passing every file individually. The entire
// path given will be the path to read the file within the program. So, if
// "resources/image.png" is passed, your program will use "resources/image.png"
// as the path to load the file.
//
// TODO: Test if shared libraries are accepted, I don't remember if emcc can
// link a shared library with a project or not.
// TODO: Add a parameter that allows a custom output directory.
pub fn linkWithEmscripten(
b: *std.Build,
itemsToLink: []const *std.Build.Step.Compile,
) !*std.Build.Step.Run {
const emccExe = switch (builtin.os.tag) {
.windows => "emcc.bat",
else => "emcc",
};
var emcc_run_arg = try b.allocator.alloc(u8, b.sysroot.?.len + emccExe.len + 1);
defer b.allocator.free(emcc_run_arg);
if (b.sysroot == null) {
emcc_run_arg = try std.fmt.bufPrint(emcc_run_arg, "{s}", .{emccExe});
} else {
emcc_run_arg = try std.fmt.bufPrint(
emcc_run_arg,
"{s}" ++ std.fs.path.sep_str ++ "{s}",
.{ b.sysroot.?, emccExe },
);
}
// Create the output directory because emcc can't do it.
const mkdir_command = switch (builtin.os.tag) {
.windows => b.addSystemCommand(&.{ "cmd.exe", "/c", "if", "not", "exist", emccOutputDir, "mkdir", emccOutputDir }),
else => b.addSystemCommand(&.{ "mkdir", "-p", emccOutputDir }),
};
// Actually link everything together.
const emcc_command = b.addSystemCommand(&[_][]const u8{emcc_run_arg});
for (itemsToLink) |item| {
emcc_command.addFileArg(item.getEmittedBin());
emcc_command.step.dependOn(&item.step);
}
// This puts the file in zig-out/htmlout/index.html.
emcc_command.step.dependOn(&mkdir_command.step);
emcc_command.addArgs(&[_][]const u8{
"-o",
emccOutputDir ++ emccOutputFile,
"-sUSE_OFFSET_CONVERTER",
"-sFULL-ES3=1",
"-sUSE_GLFW=3",
"-sASYNCIFY",
"-O3",
"-fsanitize=undefined",
});
return emcc_command;
}
// TODO: See if zig's standard library already has somehing like this.
fn lastIndexOf(string: []const u8, character: u8) usize {
// Interestingly, Zig has no nice way of iterating a slice backwards.
for (0..string.len) |i| {
const index = string.len - i - 1;
if (string[index] == character) return index;
}
return string.len - 1;
}

View File

@ -1,11 +1,10 @@
// A raylib-zig port of https://github.com/raysan5/raylib/blob/master/examples/core/core_basic_window_web.c // A raylib-zig port of https://github.com/raysan5/raylib/blob/master/examples/core/core_basic_window_web.c
const std = @import("std");
const rl = @import("raylib"); const rl = @import("raylib");
const builtin = @import("builtin"); const builtin = @import("builtin");
const c = if (builtin.os.tag == .emscripten) @cImport({ const emscripten = std.os.emscripten;
@cInclude("emscripten/emscripten.h");
});
//---------------------------------------------------------------------------------- //----------------------------------------------------------------------------------
// Global Variables Definition // Global Variables Definition
@ -24,7 +23,7 @@ pub fn main() anyerror!void {
defer rl.closeWindow(); // Close window and OpenGL context defer rl.closeWindow(); // Close window and OpenGL context
if (builtin.os.tag == .emscripten) { if (builtin.os.tag == .emscripten) {
c.emscripten_set_main_loop(@ptrCast(&updateDrawFrame), 0, 1); emscripten.emscripten_set_main_loop(@ptrCast(&updateDrawFrame), 0, 1);
} else { } else {
rl.setTargetFPS(60); // Set our game to run at 60 frames-per-second rl.setTargetFPS(60); // Set our game to run at 60 frames-per-second

View File

@ -19,60 +19,83 @@ const rlz = @import("raylib_zig");
pub fn build(b: *std.Build) !void { pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const raylib_dep = b.dependency("raylib_zig", .{ const raylib_dep = b.dependency("raylib_zig", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
const raylib = raylib_dep.module("raylib"); const raylib = raylib_dep.module("raylib");
const raylib_artifact = raylib_dep.artifact("raylib"); const raylib_artifact = raylib_dep.artifact("raylib");
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe_mod.addImport("raylib", raylib);
exe_mod.linkLibrary(raylib_artifact);
const run_step = b.step("run", "Run the app");
//web exports are completely separate //web exports are completely separate
if (target.query.os_tag == .emscripten) { if (target.query.os_tag == .emscripten) {
const exe_lib = try rlz.emcc.compileForEmscripten(b, "$PROJECT_NAME", "src/main.zig", target, optimize); const emsdk = rlz.emsdk;
const wasm = b.addLibrary(.{
.name = "$PROJECT_NAME",
.root_module = exe_mod,
});
exe_lib.linkLibrary(raylib_artifact); const install_dir: std.Build.InstallDir = .{ .custom = "web" };
exe_lib.root_module.addImport("raylib", raylib); const emcc_flags = emsdk.emccDefaultFlags(b.allocator, .{
// Note that raylib itself is not actually added to the exe_lib output file, so it also needs to be linked with emscripten.
const link_step = try rlz.emcc.linkWithEmscripten(b, &[_]*std.Build.Step.Compile{ exe_lib, raylib_artifact });
//this lets your program access files like "resources/my-image.png":
link_step.addArg("--emrun");
link_step.addArg("--embed-file");
link_step.addArg("resources/");
b.getInstallStep().dependOn(&link_step.step);
const run_step = try rlz.emcc.emscriptenRunStep(b);
run_step.step.dependOn(&link_step.step);
const run_option = b.step("run", "Run $PROJECT_NAME");
run_option.dependOn(&run_step.step);
return;
}
const exe = b.addExecutable(.{
.name = "'$PROJECT_NAME'",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.optimize = optimize, .optimize = optimize,
.target = target });
}), const emcc_settings = emsdk.emccDefaultSettings(b.allocator, .{
}); .optimize = optimize,
});
exe.linkLibrary(raylib_artifact); const emcc_step = emsdk.emccStep(b, raylib_artifact, wasm, .{
exe.root_module.addImport("raylib", raylib); .optimize = optimize,
.flags = emcc_flags,
.settings = emcc_settings,
.shell_file_path = emsdk.shell(raylib_dep.builder),
.embed_paths = &.{
.{
.src_path = b.pathJoin(&.{ module_subpath, "resources" }),
.virtual_path = "resources",
},
},
.install_dir = install_dir,
});
b.getInstallStep().dependOn(emcc_step);
const run_cmd = b.addRunArtifact(exe); const html_filename = try std.fmt.allocPrint(b.allocator, "{s}.html", .{wasm.name});
const run_step = b.step("run", "Run $PROJECT_NAME"); const emrun_step = emsdk.emrunStep(
run_step.dependOn(&run_cmd.step); b,
b.getInstallPath(install_dir, html_filename),
&.{},
);
b.installArtifact(exe); emrun_step.dependOn(emcc_step);
run_step.dependOn(emrun_step);
} else {
const exe = b.addExecutable(.{
.name = "$PROJECT_NAME",
.root_module = exe_mod,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
run_step.dependOn(&run_cmd.step);
}
} }
"@ "@
New-Item -Name "build.zig" -ItemType "file" -Value $BUILD_DOT_ZIG -Force New-Item -Name "build.zig" -ItemType "file" -Value $BUILD_DOT_ZIG -Force
zig fetch --save git+https://github.com/raylib-zig/raylib-zig#devel zig fetch --save git+https://github.com/Not-Nik/raylib-zig#devel
zig fetch --save git+https://github.com/emscripten-core/emsdk#4.0.9
New-Item -Name "resources" -ItemType "directory" New-Item -Name "resources" -ItemType "directory"
New-Item -Name "resources/placeholder.txt" -ItemType "file" -Value "" -Force New-Item -Name "resources/placeholder.txt" -ItemType "file" -Value "" -Force

View File

@ -20,57 +20,76 @@ const rlz = @import("raylib_zig");
pub fn build(b: *std.Build) !void { pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const raylib_dep = b.dependency("raylib_zig", .{ const raylib_dep = b.dependency("raylib_zig", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
const raylib = raylib_dep.module("raylib"); const raylib = raylib_dep.module("raylib");
const raylib_artifact = raylib_dep.artifact("raylib"); const raylib_artifact = raylib_dep.artifact("raylib");
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe_mod.addImport("raylib", raylib);
exe_mod.linkLibrary(raylib_artifact);
const run_step = b.step("run", "Run the app");
//web exports are completely separate //web exports are completely separate
if (target.query.os_tag == .emscripten) { if (target.query.os_tag == .emscripten) {
const exe_lib = try rlz.emcc.compileForEmscripten(b, "'$PROJECT_NAME'", "src/main.zig", target, optimize); const emsdk = rlz.emsdk;
const wasm = b.addLibrary(.{
.name = "'$PROJECT_NAME'",
.root_module = exe_mod,
});
exe_lib.linkLibrary(raylib_artifact); const install_dir: std.Build.InstallDir = .{ .custom = "web" };
exe_lib.root_module.addImport("raylib", raylib); const emcc_flags = emsdk.emccDefaultFlags(b.allocator, .{ .optimize = optimize });
const emcc_settings = emsdk.emccDefaultSettings(b.allocator, .{ .optimize = optimize });
// Note that raylib itself is not actually added to the exe_lib output file, so it also needs to be linked with emscripten. const emcc_step = emsdk.emccStep(b, raylib_artifact, wasm, .{
const link_step = try rlz.emcc.linkWithEmscripten(b, &[_]*std.Build.Step.Compile{ exe_lib, raylib_artifact });
//this lets your program access files like "resources/my-image.png":
link_step.addArg("--emrun");
link_step.addArg("--embed-file");
link_step.addArg("resources/");
b.getInstallStep().dependOn(&link_step.step);
const run_step = try rlz.emcc.emscriptenRunStep(b);
run_step.step.dependOn(&link_step.step);
const run_option = b.step("run", "Run '$PROJECT_NAME'");
run_option.dependOn(&run_step.step);
return;
}
const exe = b.addExecutable(.{
.name = "'$PROJECT_NAME'",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.optimize = optimize, .optimize = optimize,
.target = target .flags = emcc_flags,
}), .settings = emcc_settings,
}); .shell_file_path = emsdk.shell(raylib_dep.builder),
.embed_paths = &.{
.{
.src_path = b.pathJoin(&.{ module_subpath, "resources" }),
.virtual_path = "resources",
},
},
.install_dir = install_dir,
});
b.getInstallStep().dependOn(emcc_step);
exe.linkLibrary(raylib_artifact); const html_filename = try std.fmt.allocPrint(b.allocator, "{s}.html", .{wasm.name});
exe.root_module.addImport("raylib", raylib); const emrun_step = emsdk.emrunStep(
b,
b.getInstallPath(install_dir, html_filename),
&.{},
);
const run_cmd = b.addRunArtifact(exe); emrun_step.dependOn(emcc_step);
const run_step = b.step("run", "Run '$PROJECT_NAME'"); run_step.dependOn(emrun_step);
run_step.dependOn(&run_cmd.step); } else {
const exe = b.addExecutable(.{
.name = "'$PROJECT_NAME'",
.root_module = exe_mod,
});
b.installArtifact(exe);
b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
run_step.dependOn(&run_cmd.step);
}
}' >> build.zig }' >> build.zig
zig fetch --save git+https://github.com/raylib-zig/raylib-zig#devel zig fetch --save git+https://github.com/Not-Nik/raylib-zig#devel
zig fetch --save git+https://github.com/emscripten-core/emsdk#4.0.9
mkdir resources mkdir resources
touch resources/placeholder.txt touch resources/placeholder.txt