diff --git a/cmd/export/export.v b/cmd/export/export.v index 81c4102..87e59e1 100644 --- a/cmd/export/export.v +++ b/cmd/export/export.v @@ -5,9 +5,8 @@ module main import os import flag -// import shy.vxt -import shy.cli import shy.export +import shy.utils pub const exe_version = '0.0.2' // version() @@ -17,196 +16,70 @@ pub const exe_dir = os.dir(os.real_path(os.executable())) pub const exe_args_description = 'input or: [options] input' -pub const exe_description = 'export exports both plain V applications and shy-based applications. +pub const exe_description = 'shy ${exe_short_name} [options] +${exe_short_name} exports both plain V applications and shy-based applications. The exporter is based on the `shy.export` module. export can compile, package and deploy V apps for production use on a wide range of platforms like: Linux, macOS, Windows, Android and HTML5 (WASM). -The following does the same as if they were passed to the v compiler: - -Flags: - -autofree, -gc , -g, -cg, -showcc - Sub-commands: - run Run the output package after successful export' + run Run the output package after successful export -pub const rip_vflags = ['-autofree', '-gc', '-g', '-cg', 'run', '-showcc'] -pub const accepted_input_files = ['.v'] +Exporters: + ${export.supported_exporters}' -pub const export_env_vars = [ - 'SHY_FLAGS', - 'SHY_EXPORT_FLAGS', - 'VEXE', - 'VMODULES', -] - -/* -pub fn run(args []string) os.Result { - return os.execute(args.join(' ')) -}*/ pub struct Options { pub: // These fields would make little sense to change during a run - verbosity int @[only: v; repeats; xdoc: 'Verbosity level 1-3'] - work_dir string = export.work_dir() @[xdoc: 'Directory to use for temporary work files'] + verbosity int @[repeats; short: v; xdoc: 'Verbosity level 1-3'] // - run bool @[ignore] - no_parallel bool @[xdoc: 'Do not run tasks in parallel'] // Run, what can be run, in parallel - nocache bool @[xdoc: 'Do not use caching'] // defaults to false in os.args/flag parsing phase - gl_version string = '3' @[only: gl; xdoc: 'GL(ES) version to use from any of 3,es3'] - format string = 'zip' @[xdoc: 'Format of output (default is a .zip)'] + run bool @[ignore] + nocache bool @[xdoc: 'Do not use caching'] // defaults to false in os.args/flag parsing phase // echo and exit dump_usage bool @[long: help; short: h; xdoc: 'Show this help message and exit'] show_version bool @[long: version; xdoc: 'Output version information and exit'] -mut: - unmatched_args []string @[ignore] // args that could not be matched -pub mut: - // I/O - input string @[tail] - output string @[short: o; xdoc: 'Path to output (dir/file)'] - is_prod bool = true @[ignore] - c_flags []string @[long: cflag; short: c; xdoc: 'Additional flags for the C compiler'] // flags passed to the C compiler(s) - v_flags []string @[long: flag; short: f; xdoc: 'Additional flags for the V compiler'] // flags passed to the V compiler - assets_extra []string @[long: asset; short: a; xdoc: 'Asset dir(s) to include in build'] // list of (extra) paths to assets dirs to include - libs_extra []string @[long: libs; short: l; xdoc: 'Lib dir(s) to include in build'] + // + exporter []string @[ignore] } -// options_from_env returns an `Option` struct filled with flags set via -// the `SHY_EXPORT_FLAGS` env variable otherwise it returns the `defaults` `Option` struct. -pub fn options_from_env(defaults Options) !Options { - env_flags := os.getenv('SHY_EXPORT_FLAGS') - if env_flags != '' { - mut flags := [os.args[0]] - flags << cli.string_to_args(env_flags)! - opts := args_to_options(flags, defaults)! - return opts +pub fn args_to_options(arguments []string) !Options { + if arguments.len <= 1 { + return error('no arguments given') } - return defaults -} - -// extend_from_dot_shy will merge the `Options` with any content -// found in any `.shy` config files. -pub fn (mut opt Options) extend_from_dot_shy() ! { - // Look up values in input .shy file next to input if no flags or defaults was set - // TODO use TOML format here - // dot_shy_file := dot_shy_path(opt.input) - // dot_shy := os.read_file(dot_shy_file) or { '' } -} -// validate_env ensures that `Options` meet all runtime requirements. -pub fn (opt &Options) validate_env() ! {} - -// args_to_options returns an `Option` merged from (CLI/Shell) `arguments` using `defaults` as -// values where no value can be obtained from `arguments`. -pub fn args_to_options(arguments []string, defaults Options) !Options { mut args := arguments.clone() - - mut v_flags := []string{} - mut cmd_flags := []string{} - // Indentify special flags in args before FlagParser ruin them. - // E.g. the -autofree flag will result in dump_usage being called for some weird reason??? - for special_flag in rip_vflags { - if special_flag in args { - if special_flag == '-gc' { - gc_type := args[(args.index(special_flag)) + 1] - v_flags << special_flag + ' ${gc_type}' - args.delete(args.index(special_flag) + 1) - } else if special_flag.starts_with('-') { - v_flags << special_flag - } else { - cmd_flags << special_flag - } - args.delete(args.index(special_flag)) - } - } - - mut opt, unmatched_args := flag.using(defaults, args, skip: 1)! - - // Validate format - if opt.format != '' { - export.string_to_export_format(opt.format)! - } - - mut unmatched := unmatched_args.clone() - for unmatched_arg in defaults.unmatched_args { - if unmatched_arg !in unmatched { - unmatched << unmatched_arg + mut run := false + for i, arg in args { + if arg == 'run' { + run = true + args.delete(i) + break } } - opt.unmatched_args = unmatched - - mut c_flags := []string{} - c_flags << opt.c_flags - for c_flag in defaults.c_flags { - if c_flag !in c_flags { - c_flags << c_flag + mut exporter := []string{cap: 0} + for i, arg in args { + if arg in export.supported_exporters { + exporter = args[i..].clone() + args = args[..i].clone() + break } } - opt.c_flags = c_flags + opt, _ := flag.to_struct[Options](args, skip: 1)! - v_flags << opt.v_flags - for v_flag in defaults.v_flags { - if v_flag !in v_flags { - v_flags << v_flag - } + return Options{ + ...opt + run: run + exporter: exporter } - opt.v_flags = v_flags - - return opt -} - -pub fn (opt &Options) to_export_options() export.Options { - format := export.string_to_export_format(opt.format) or { export.Format.zip } - mut gl_version := opt.gl_version - - opts := export.Options{ - verbosity: opt.verbosity - work_dir: opt.work_dir - parallel: !opt.no_parallel - cache: !opt.nocache - gl_version: gl_version - format: format - input: opt.input - output: opt.output - is_prod: opt.is_prod - c_flags: opt.c_flags - v_flags: opt.v_flags - assets: opt.assets_extra - } - return opts } fn main() { - args := os.args[1..] - - // Collect user flags in an extended manner. - mut opt := Options{} - - /* TODO: (lmp) fix this .shy should be used first, then from env then flags... right? - opt = extend_from_dot_shy() or { - eprintln('Error while parsing `.shy`: ${err}') - eprintln('Use `${cli.exe_short_name} -h` to see all flags') - exit(1) - } - */ - - opt = options_from_env(opt) or { - eprintln('Error while parsing `SHY_EXPORT_FLAGS`: ${err}') - eprintln('Use `${exe_short_name} -h` to see all flags') - exit(1) - } - - opt = args_to_options(args, opt) or { - eprintln('Error while parsing `os.args`: ${err}') - eprintln('Use `${exe_short_name} -h` to see all flags') - exit(1) - } - - if args.len == 1 { - eprintln('No arguments given') - eprintln('Use `shy export -h` to see all flags') + opt := args_to_options(os.args) or { + utils.shy_error('Error while parsing arguments: ${err}', + details: 'Use `${exe_short_name} -h` to see all flags' + ) exit(1) } @@ -227,43 +100,42 @@ fn main() { exit(0) } - // All flags after this requires an input argument - if opt.input == '' { - dump(opt) - eprintln('No input given') - eprintln('See `shy export -h` for help') - exit(1) - } - - // Validate environment after options and input has been resolved - opt.validate_env() or { panic(err) } - - // input_ext := os.file_ext(opt.input) - if opt.verbosity > 2 { - dump(opt) - } - - // TODO - if opt.unmatched_args.len > 1 { - eprintln('Unknown args: ${opt.unmatched_args}') - exit(1) - } - - // Validate environment after options and input has been resolved - opt.validate_env() or { - eprintln('${err}') + if opt.exporter.len == 0 { + utils.shy_error('No exporter defined', details: 'See `shy export -h` for help') exit(1) } - // input_ext := os.file_ext(opt.input) - if opt.verbosity > 3 { - eprintln('--- ${exe_short_name} ---') - eprintln(opt) + mut export_result := export.Result{} + match opt.exporter[0] { + 'appimage' { + appimage_export_options := export.args_to_appimage_options(opt.exporter) or { + utils.shy_error('Error getting AppImage options', details: '${err}') + exit(1) + } + export_result = export.appimage(appimage_export_options) or { + utils.shy_error('Error while exporting to AppImage', details: '${err}') + exit(1) + } + } + 'wasm' { + wasm_export_options := export.args_to_wasm_options(opt.exporter) or { + utils.shy_error('Error getting wasm options', details: '${err}') + exit(1) + } + export_result = export.wasm(wasm_export_options) or { + utils.shy_error('Error while exporting to wasm', details: '${err}') + exit(1) + } + } + else { + utils.shy_error('Unknown exporter "${opt.exporter[0]}"', + details: 'Valid exporters: ${export.supported_exporters}' + ) + exit(1) + } } - - export_opts := opt.to_export_options() - export.export(export_opts) or { - eprintln('Error while exporting `${export_opts.input}`: ${err}') - exit(1) + if export_result.output != '' { + println(export_result.output) } + exit(0) } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 58ee743..7873c13 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,9 @@ #### Notable changes * Add support for `wasm32_emscripten` build target making it possible to target the Web via `emscripten`/`emcc` +* **WIP** Rewrite `shy export` internals + - Exporting to an `AppImage` on Linux now works via `shy export appimage ...` + - Exporting to the Web (via `emcc`) now works via `shy export wasm ...` #### Breaking changes diff --git a/docs/dev/emscripten_howto.txt b/docs/dev/emscripten_howto.txt index a452207..a35afd2 100644 --- a/docs/dev/emscripten_howto.txt +++ b/docs/dev/emscripten_howto.txt @@ -43,8 +43,7 @@ static char __CLOSURE_GET_DATA_BYTES[] = { -gsource-map -sSTACK_SIZE=8mb # -sTOTAL_MEMORY=300mb -sINITIAL_MEMORY=200mb - +-sEMSCRIPTEN_KEEPALIVE?? test this instead of -sEXPORTED_FUNCTIONS... # Puzzle Vibes shy_root="$(pwd)"; pro="$HOME/Projects/puzzle_vibes"; v -skip-unused -gc none -d wasm32_emscripten -os wasm32_emscripten -o /tmp/shyem/vc_src.c $pro && emcc -flto -fPIC -fvisibility=hidden --preload-file $shy_root/assets@/ --preload-file $pro/assets@/ -sEXPORTED_FUNCTIONS="['_malloc', '_main']" -sSTACK_SIZE=1mb -sERROR_ON_UNDEFINED_SYMBOLS=0 -sASSERTIONS=1 -sUSE_WEBGL2=1 -sUSE_SDL=2 -sNO_EXIT_RUNTIME=1 -sALLOW_MEMORY_GROWTH=1 -O0 -g -D_DEBUG_ -D_DEBUG -D SOKOL_GLES3 -D SOKOL_NO_ENTRY -D MINIAUDIO_IMPLEMENTATION -D _REENTRANT -I "$shy_root/thirdparty/stb" -I "$shy_root/thirdparty/fontstash" -I "$shy_root/thirdparty/sokol" -I "$shy_root/thirdparty/sokol/util" -I "$shy_root/wraps/miniaudio/c/miniaudio" -I "$shy_root/shy" -Wno-enum-conversion -Wno-unused-value $shy_root/thirdparty/stb/stbi.c /tmp/shyem/vc_src.c -lm -lpthread -ldl -o /tmp/shyem/vc_src.html - diff --git a/export/android.v b/export/android.v new file mode 100644 index 0000000..3f4cf48 --- /dev/null +++ b/export/android.v @@ -0,0 +1,63 @@ +// Copyright(C) 2022 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module export + +import flag + +pub struct AndroidOptions { +pub: + // These fields would make little sense to change during a run + verbosity int @[only: v; repeats; xdoc: 'Verbosity level 1-3'] + work_dir string // TODO: + // + run bool @[ignore] + nocache bool @[xdoc: 'Do not use caching'] // defaults to false in os.args/flag parsing phase + parallel bool = true @[long: 'no-parallel'] // Run, what can be run, in parallel + // echo and exit + dump_usage bool @[long: help; short: h; xdoc: 'Show this help message and exit'] + show_version bool @[long: version; xdoc: 'Output version information and exit'] +pub mut: + // I/O + input string + output string + is_prod bool + c_flags []string // flags passed to the C compiler(s) + v_flags []string // flags passed to the V compiler + assets []string // list of (extra) paths to asset (roots) dirs to include +} + +pub fn args_to_android_options(args []string) !AndroidOptions { + opt, _ := flag.to_struct[AndroidOptions](args, skip: 1)! + return opt +} + +pub fn android(opt AndroidOptions) !Result { + // mut gl_version := opt.gl_version + // match opt.format { + // .android_apk, .android_aab { + // if gl_version in ['3', '2'] { + // mut auto_gl_version := 'es2' + // if gl_version == '3' { + // auto_gl_version = 'es3' + // } + // if opt.verbosity > 0 { + // eprintln('Auto adjusting OpenGL version for Android from ${gl_version} to ${auto_gl_version}') + // } + // gl_version = auto_gl_version + // } + // } + // else {} + // } + // adjusted_options := Options{ + // ...opt + // gl_version: gl_version + // } + // if opt.verbosity > 3 { + eprintln('--- ${@MOD}.${@FN} ---') + // eprintln(adjusted_options) + //} + return Result{ + output: '' + } +} diff --git a/export/export_appimage.v b/export/appimage.v similarity index 55% rename from export/export_appimage.v rename to export/appimage.v index 505b431..7cb99ef 100644 --- a/export/export_appimage.v +++ b/export/appimage.v @@ -4,10 +4,130 @@ module export import os +import flag import shy.vxt +import shy.paths +import shy.utils import net.http -fn (opt Options) ensure_appimagetool() !string { +pub const appimage_exporter_version = '0.0.1' +pub const appimage_fn_description = 'shy export appimage +exports both plain V applications and shy-based applications to Linux AppImages. +' + +pub enum AppImageFormat { + app_image // .AppImage + app_dir // .AppDir +} + +pub fn (aif AppImageFormat) ext() string { + return match aif { + .app_image { + 'AppImage' + } + .app_dir { + 'AppDir' + } + } +} + +pub struct AppImageOptions { + ExportOptions +pub: + work_dir string = os.join_path(paths.tmp_work(), 'export', 'appimage') @[ignore] + compress bool @[xdoc: 'Compress executable with `upx` if available'] + strip bool @[xdoc: 'Strip executable symbols with `strip` if available'] + format AppImageFormat = .app_image +} + +fn (aio AppImageOptions) help_or_docs() ?Result { + if aio.show_version { + return Result{ + output: 'shy export appimage ${appimage_exporter_version}' + } + } + + if aio.dump_usage { + export_doc := flag.to_doc[ExportOptions]( + name: 'shy export appimage' + version: '${appimage_exporter_version}' + description: appimage_fn_description + ) or { return none } + + appimage_doc := flag.to_doc[AppImageOptions]( + options: flag.DocOptions{ + show: .flags | .flag_type | .flag_hint | .footer + } + ) or { return none } + + return Result{ + output: '${export_doc}\n${appimage_doc}' + } + } + return none +} + +pub fn args_to_appimage_options(args []string) !AppImageOptions { + export_options, unmatched := flag.to_struct[ExportOptions](args, skip: 1)! + options, no_match := flag.to_struct[AppImageOptions](unmatched)! + if no_match.len > 0 { + return error('Unrecognized argument(s): ${no_match}') + } + return AppImageOptions{ + ...options + ExportOptions: export_options + } +} + +// resolve_input returns the resolved path/file of the input. +fn (opt AppImageOptions) resolve_input() !string { + mut input := opt.input.trim_right(os.path_separator) + // If no specific output file is given, we use the input file as a base + if input == '' { + return error('${@MOD}.${@FN}: no input given') + } + if input in ['.', '..'] || os.is_dir(input) { + input = os.real_path(input) + } + return input +} + +// resolve_output returns output according to what `input` contains. +fn (opt AppImageOptions) resolve_output(input string) !string { + // Resolve output + mut output_file := '' + // input_file_ext := os.file_ext(opt.input).trim_left('.').to_lower() + output_file_ext := os.file_ext(opt.output).trim_left('.').to_lower() + // Infer from output + if output_file_ext in ['appimage', 'appdir'] { + output_file = opt.output + } else { // Generate from defaults: [-o ] + default_file_name := input.all_after_last(os.path_separator).replace(' ', '_').to_lower() + if opt.output != '' { + ext := os.file_ext(opt.output) + if ext != '' { + output_file = opt.output.all_before(ext) + } else { + output_file = os.join_path(opt.output.trim_right(os.path_separator), default_file_name) + } + } else { + output_file = default_file_name + } + if opt.format == .app_image { + output_file += '.AppImage' + } else { + output_file += '.AppDir' + } + } + return output_file +} + +pub fn (opt AppImageOptions) resolve_io() !(string, string, AppImageFormat) { + input := opt.resolve_input()! + return input, opt.resolve_output(input)!, opt.format +} + +fn (opt AppImageOptions) ensure_appimagetool() !string { appimagetool_url := 'https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage' mut appimagetool_exe := os.join_path(ensure_cache_dir()!, 'squashfs-root', 'AppRun') if !os.exists(appimagetool_exe) { @@ -50,25 +170,33 @@ fn (opt Options) ensure_appimagetool() !string { return appimagetool_exe } -fn export_appimage(opt Options) ! { - if opt.verbosity > 3 { - eprintln('--- ${@MOD}.${@FN} ---') - eprintln(opt) +pub fn appimage(opt AppImageOptions) !Result { + if result := opt.help_or_docs() { + return result + } + + if opt.input == '' { + return error('${@MOD}.${@FN}: no input') + } + + if os.user_os() != 'linux' { + return error('${@MOD}.${@FN}: AppImage generation is only supported on Linux') } appimagetool_exe := opt.ensure_appimagetool()! - // Resolve and sanitize input path - input := os.real_path(opt.input).trim_string_right(os.path_separator) + // Resolve and sanitize input and output + input, output, format := opt.resolve_io()! + + paths.ensure(opt.work_dir)! // Build V input app for host platform v_app := os.join_path(opt.work_dir, 'v_app') - if opt.verbosity > 0 { - eprintln('Building V source as "${v_app}"...') - } + opt.verbose(1, 'Building V source(s) as "${v_app}"...') + mut v_cmd := [ vxt.vexe(), ] - if opt.is_prod { + if opt.supported_v_flags.prod { v_cmd << '-prod' } v_cmd << opt.v_flags @@ -91,9 +219,8 @@ fn export_appimage(opt Options) ! { if app_name == '' { return error('${@MOD}.${@FN}: failed resolving app name from ${input}') } - if opt.verbosity > 1 { - eprintln('Resolved app name to "${app_name}"') - } + opt.verbose(2, 'Resolved app name to "${app_name}"') + // Prepare AppDir directory. We do it manually because the "format", // or rather, conventions - are fairly straight forward and appimage-builder is a mess. // https://docs.appimage.org/packaging-guide/overview.html#manually-creating-an-appdir @@ -101,9 +228,9 @@ fn export_appimage(opt Options) ! { // app_dir_path := os.join_path(opt.work_dir, '${app_name}.AppDir') if os.exists(app_dir_path) { - os.rmdir_all(app_dir_path)! + os.rmdir_all(app_dir_path) or {} } - os.mkdir_all(app_dir_path)! + paths.ensure(app_dir_path)! // Create an AppDir structure, that the appimagetool can work on. // @@ -203,7 +330,6 @@ exec "${EXEC}" "$@"' rd_config := ResolveDependenciesConfig{ verbosity: opt.verbosity - format: opt.format exe: v_app excludes: so_excludes skip_resolve: skip_resolve @@ -218,9 +344,7 @@ exec "${EXEC}" "$@"' if os.is_link(lib_real_path) { lib_real_path = os.real_path(lib_real_path) } - if opt.verbosity > 1 { - eprintln('Copying "${lib_real_path}" to "${app_lib}"') - } + opt.verbose(2, 'Copying "${lib_real_path}" to "${app_lib}"') os.cp(lib_real_path, app_lib) or { return error('${@MOD}.${@FN}: failed to copy "${lib_real_path}" to "${app_lib}": ${err}') } @@ -256,9 +380,7 @@ exec "${EXEC}" "$@"' // Look for "assets" dir in same location as input assets_by_side := os.join_path(assets_by_side_path, 'assets') if os.is_dir(assets_by_side) { - if opt.verbosity > 0 { - eprintln('Including assets from "${assets_by_side}"') - } + opt.verbose(1, 'Including assets from "${assets_by_side}"') os.cp_all(assets_by_side, assets_path, false) or { return error('${@MOD}.${@FN}: failed to copy "${assets_by_side}" to "${assets_path}": ${err}') } @@ -271,13 +393,9 @@ exec "${EXEC}" "$@"' assets_above := os.real_path(os.join_path(assets_by_side_path, '..', 'assets')) if os.is_dir(assets_above) { if os.real_path(assets_above) in included_asset_paths { - if opt.verbosity > 1 { - eprintln('Skipping "${assets_above}" since it\'s already included') - } + opt.verbose(2, 'Skipping "${assets_above}" since it\'s already included') } else { - if opt.verbosity > 0 { - eprintln('Including assets from "${assets_above}"') - } + opt.verbose(1, 'Including assets from "${assets_above}"') os.cp_all(assets_above, assets_path, false) or { return error('${@MOD}.${@FN}: failed to copy "${assets_above}" to "${assets_path}": ${err}') } @@ -291,13 +409,9 @@ exec "${EXEC}" "$@"' os.join_path('shy', 'assets') if os.is_dir(shy_example_assets) { if os.real_path(shy_example_assets) in included_asset_paths { - if opt.verbosity > 1 { - eprintln('Skipping "${shy_example_assets}" since it\'s already included') - } + opt.verbose(2, 'Skipping "${shy_example_assets}" since it\'s already included') } else { - if opt.verbosity > 0 { - eprintln('Including assets from "${shy_example_assets}"') - } + opt.verbose(1, 'Including assets from "${shy_example_assets}"') os.cp_all(shy_example_assets, assets_path, false) or { return error('${@MOD}.${@FN}: failed to copy (assets in dir) "${shy_example_assets}" to "${assets_path}": ${err}') } @@ -310,13 +424,9 @@ exec "${EXEC}" "$@"' if os.is_dir(assets_in_dir) { assets_in_dir_resolved := os.real_path(os.join_path(os.getwd(), assets_in_dir)) if assets_in_dir_resolved in included_asset_paths { - if opt.verbosity > 1 { - eprintln('Skipping "${assets_in_dir}" since it\'s already included') - } + opt.verbose(2, 'Skipping "${assets_in_dir}" since it\'s already included') } else { - if opt.verbosity > 0 { - eprintln('Including assets from "${assets_in_dir}"') - } + opt.verbose(1, 'Including assets from "${assets_in_dir}"') os.cp_all(assets_in_dir, assets_path, false) or { return error('${@MOD}.${@FN}: failed to copy (assets in dir) "${assets_in_dir}" to "${assets_path}": ${err}') } @@ -328,13 +438,9 @@ exec "${EXEC}" "$@"' if os.is_dir(user_asset) { user_asset_resolved := os.real_path(user_asset) if user_asset_resolved in included_asset_paths { - if opt.verbosity > 1 { - eprintln('Skipping "${user_asset}" since it\'s already included') - } + opt.verbose(2, 'Skipping "${user_asset}" since it\'s already included') } else { - if opt.verbosity > 0 { - eprintln('Including assets from "${user_asset}"') - } + opt.verbose(1, 'Including assets from "${user_asset}"') os.cp_all(user_asset, assets_path, false) or { return error('${@MOD}.${@FN}: failed to copy "${user_asset}" to "${assets_path}": ${err}') } @@ -342,25 +448,25 @@ exec "${EXEC}" "$@"' } } else { os.cp(user_asset, assets_path) or { - eprintln('Skipping invalid or non-existent asset file "${user_asset}"') + utils.shy_notice('Skipping invalid or non-existent asset file "${user_asset}"') } } } // strip exe - strip_exe := os.find_abs_path_of_executable('strip') or { '' } - if os.is_executable(strip_exe) { - if opt.verbosity > 0 { - eprintln('Running ${strip_exe} "${app_exe}"...') - } - strip_cmd := [ - '${strip_exe}', - '"${app_exe}"', - ] - strip_res := os.execute(strip_cmd.join(' ')) - if strip_res.exit_code != 0 { - stripcmd := strip_cmd.join(' ') - return error('${@MOD}.${@FN}: "${stripcmd}" failed: ${strip_res.output}') + if opt.strip { + strip_exe := os.find_abs_path_of_executable('strip') or { '' } + if os.is_executable(strip_exe) { + opt.verbose(1, 'Running ${strip_exe} "${app_exe}"...') + strip_cmd := [ + '${strip_exe}', + '"${app_exe}"', + ] + strip_res := os.execute(strip_cmd.join(' ')) + if strip_res.exit_code != 0 { + stripcmd := strip_cmd.join(' ') + return error('${@MOD}.${@FN}: "${stripcmd}" failed: ${strip_res.output}') + } } } @@ -368,9 +474,7 @@ exec "${EXEC}" "$@"' if opt.compress { upx_exe := os.find_abs_path_of_executable('upx') or { '' } if os.is_executable(upx_exe) { - if opt.verbosity > 0 { - eprintln('Compressing "${app_exe}"...') - } + opt.verbose(1, 'Compressing "${app_exe}"...') upx_cmd := [ '${upx_exe}', '-9', @@ -402,29 +506,29 @@ exec "${EXEC}" "$@"' }) } - if opt.format == .appimage_dir { - return + if format == .app_dir { + return Result{ + output: 'Successfully generated "${app_dir_path}"' + } } // Write .AppDir to AppImage using `appimagetool` - output := opt.output - if opt.verbosity > 0 { - eprintln('Building AppImage "${output}"...') - } + opt.verbose(1, 'Building AppImage "${output}"...') appimagetool_cmd := [ appimagetool_exe, app_dir_path, output, ] - if opt.verbosity > 2 { - eprintln('Running "${appimagetool_cmd}"...') - } + opt.verbose(3, 'Running "${appimagetool_cmd}"...') ait_res := os.execute(appimagetool_cmd.join(' ')) if ait_res.exit_code != 0 { ait_cmd := appimagetool_cmd.join(' ') return error('${@MOD}.${@FN}: "${ait_cmd}" failed: ${ait_res.output}') } os.chmod(output, 0o775)! // make it executable + return Result{ + output: 'Successfully generated "${output}"' + } } pub fn appimage_exclude_list(verbosity int) ![]string { @@ -443,3 +547,164 @@ pub fn appimage_exclude_list(verbosity int) ![]string { return os.read_lines(excludes_path) or { []string{} }.filter(it.trim_space() != '' && !it.trim_space().starts_with('#')) } + +// pub struct Dependency{ +// path string +// // { 'so':so, 'path':path, 'realpath':realpath, 'dependants':set([executable]), 'type':'lib' } +// } + +struct ResolveDependenciesConfig { + verbosity int + indent int + exe string + excludes []string + skip_resolve []string +} + +fn resolve_dependencies_recursively(mut deps map[string]string, config ResolveDependenciesConfig) ! { + // Resolving shared object (.so) dependencies on Linux is not as straight forward as + // one could wish for. Using `objdump` alone gives us only the *names* of the + // shared objects, not the full path. Using only `ldd` *does* result in resolved lib paths BUT + // they're done recursively, in some cases by executing the exe/lib - and, on top, it's printed + // *in one stream* which makes it impossible to know which libs has dependencies on which, + // further more `ldd` has security issues and problems with cross-compiled binaries. + // The issues are mostly ignored in our case since we consider the input (v sources -> binary) + // "trusted" and we do not support V cross-compiled binaries anyway at this point + // (Not sure AppImages even support it?!). + // + // Digging even further and reading source code of programs like `lddtree` will reveal + // that it's not straight forward to know what `.so` will be loaded by `ld` upon execution + // due to LD_LIBRARY_PATH mess and misuse etc. + // + // So. For now we've chosen a solution using a mix of both `objdump` and `ldd` - it has pitfalls for sure - + // but how many and how severe - only time will tell. If we are to do this "correctly" it'll need a lot + // more development time and special-cases (and native V modules for reading ELF binaries etc.) than what + // is feasible right now; We really just want to be able to collect a bunch of shared object files that + // a given V executable rely on in-order for us to collect them and package them up, for example, in an AppImage. + // + // The strategy is thus the following: + // 1. Run `objdump` on the exe/so file (had to choose one; readelf lost: + // https://stackoverflow.com/questions/8979664/readelf-vs-objdump-why-are-both-needed) + // this gives us the immediate (1st level) dependencies of the app. + // 2. Run `ldd` on the same exe/so file to obtain the first encountered resolved path(s) to the 1st level exe/so dependency. + // 3. Do step 1 and 2 for all dependencies, recursively + // 4. Cross our fingers and assume that 99.99% of cases will end up having happy users. + // The remaining user pool will hopefully be tech savy enough to fix/extend things themselves. + + verbosity := config.verbosity + indent := config.indent + mut root_indents := ' '.repeat(indent) + ' ' + if indent == 0 { + root_indents = '' + } + indents := ' '.repeat(indent + 1) + ' ' + executable := config.exe + excludes := config.excludes + skip_resolve := config.skip_resolve + + if verbosity > 0 { + base := os.file_name(executable) + eprintln('${root_indents}${base} (include)') + } + objdump_cmd := [ + 'objdump', + '-x', + executable, + ] + od_res := os.execute(objdump_cmd.join(' ')) + if od_res.exit_code != 0 { + cmd := objdump_cmd.join(' ') + return error('${@MOD}.${@FN} "${cmd}" failed:\n${od_res.output}') + } + od_lines := od_res.output.split('\n').map(it.trim_space()) + mut exe_deps := []string{} + for line in od_lines { + if !line.contains('NEEDED') { + continue + } + parts := line.split(' ').map(it.trim_space()).filter(it != '') + if parts.len != 2 { + continue + } + so_name := parts[1] + if so_name in excludes { + if verbosity > 1 { + eprintln('${indents}${so_name} (exclude)') + } + continue + } + exe_deps << so_name + } + + mut resolved_deps := map[string]string{} + + ldd_cmd := [ + 'ldd', + // '-r', + executable, + ] + ldd_res := os.execute(ldd_cmd.join(' ')) + if ldd_res.exit_code != 0 { + cmd := ldd_cmd.join(' ') + return error('${@MOD}.${@FN} "${cmd}" failed:\n${ldd_res.output}') + } + ldd_lines := ldd_res.output.split('\n').map(it.trim_space()) + for line in ldd_lines { + if line.contains('statically linked') { + continue + } + if line.contains('not found') { + // TODO ?? - give error here? add an option to continue? + continue + } + parts := line.split(' ') + if parts.len == 0 || parts.len < 3 { + continue + } + // dump(parts) + so_name := parts[0] + path := parts[2] + + if so_name in exe_deps { + if existing := resolved_deps[so_name] { + if existing != path { + eprintln('${indents}${so_name} Warning: resolved path is ambiguous "${existing}" vs. "${path}"') + } + continue + } + resolved_deps[so_name] = path + } + + // if _ := deps[so_name] { + // // Could add to "dependants" here for further info + // continue + //} + } + + for so_name, path in resolved_deps { + deps[so_name] = path + + if so_name in skip_resolve { + if verbosity > 1 { + eprintln('${indents}${so_name} (skip resolve)') + } + continue + } + + conf := ResolveDependenciesConfig{ + ...config + exe: path + indent: indent + 1 + } + resolve_dependencies_recursively(mut deps, conf)! + } +} + +pub fn resolve_dependencies(config ResolveDependenciesConfig) !map[string]string { + mut deps := map[string]string{} + if config.verbosity > 0 { + eprintln('Resolving dependencies for executable "${config.exe}"...') + } + resolve_dependencies_recursively(mut deps, config)! + return deps +} diff --git a/export/compile.v b/export/compile.v new file mode 100644 index 0000000..c4ee39d --- /dev/null +++ b/export/compile.v @@ -0,0 +1,439 @@ +// Copyright(C) 2022 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module export + +import os +import shy.paths +import shy.cli +import shy.utils +import shy.vxt + +pub enum CompileType { + v_to_c + c_to_o + other +} + +pub struct CompileError { + Error +pub: + kind CompileType + err string +} + +fn (err CompileError) msg() string { + enum_to_text := match err.kind { + .v_to_c { + '.v to .c' + } + .c_to_o { + '.c to .o' + } + .other { + '.other' + } + } + return 'failed to compile ${enum_to_text}:\n${err.err}' +} + +pub struct VCompileOptions { +pub: + verbosity int // level of verbosity + cache bool = true + parallel bool = true + input string @[required] + output string + is_prod bool + cc string + os string + v_flags []string // flags to pass to the v compiler + c_flags []string // flags to pass to the C compiler(s) +} + +// verbose prints `msg` to STDOUT if `AppImageOptions.verbosity` level is >= `verbosity_level`. +pub fn (vco VCompileOptions) verbose(verbosity_level int, msg string) { + if vco.verbosity >= verbosity_level { + println(msg) + } +} + +// has_v_d_flag returns true if `d_flag` (-d ) can be found among the passed v flags. +pub fn (opt VCompileOptions) has_v_d_flag(d_flag string) bool { + for v_flag in opt.v_flags { + if v_flag.contains('-d ${d_flag}') { + return true + } + } + return false +} + +// is_debug_build returns true if either `-cg` or `-g` flags is found among the passed v flags. +pub fn (opt VCompileOptions) is_debug_build() bool { + return '-cg' in opt.v_flags || '-g' in opt.v_flags +} + +pub fn (opt VCompileOptions) cmd() []string { + vexe := vxt.vexe() + + mut v_cmd := [ + vexe, + ] + if !opt.uses_gc() { + v_cmd << '-gc none' + } + if opt.cc != '' { + v_cmd << '-cc ${opt.cc}' + } + if opt.os != '' { + v_cmd << '-os ${opt.os}' + } + if opt.is_prod { + v_cmd << '-prod' + } + if !opt.cache { + v_cmd << '-nocache' + } + v_cmd << opt.v_flags + if opt.c_flags.len > 0 { + v_cmd << "-cflags '${opt.c_flags.join(' ')}'" + } + + if opt.output != '' { + v_cmd << '-o ${opt.output}' + } + + v_cmd << opt.input + return v_cmd +} + +// uses_gc returns true if a `-gc` flag is found among the passed v flags. +pub fn (opt VCompileOptions) uses_gc() bool { + mut uses_gc := true // V default + for v_flag in opt.v_flags { + if v_flag.starts_with('-gc') { + if v_flag.ends_with('none') { + uses_gc = false + } + break + } + } + return uses_gc +} + +pub struct VDumpOptions { + VCompileOptions +pub: + work_dir string = os.join_path(paths.tmp_work(), 'export', 'v', 'dump') // temporary work directory +} + +pub struct VMetaInfo { +pub: + imports []string + c_flags []string +} + +// v_dump_meta returns the information dumped by +// -dump-modules and -dump-c-flags. +pub fn v_dump_meta(opt VDumpOptions) !VMetaInfo { + paths.ensure(opt.work_dir)! + + // Dump modules and C flags to files + v_cflags_file := os.join_path(opt.work_dir, 'v.cflags') + os.rm(v_cflags_file) or {} + v_dump_modules_file := os.join_path(opt.work_dir, 'v.modules') + os.rm(v_dump_modules_file) or {} + + mut v_flags := opt.v_flags.clone() + v_flags << [ + '-dump-modules "${v_dump_modules_file}"', + '-dump-c-flags "${v_cflags_file}"', + ] + + vco := VCompileOptions{ + ...opt.VCompileOptions + output: '' + v_flags: v_flags + } + + v_cmd := vco.cmd() + + opt.verbose(3, 'Running `${v_cmd.join(' ')}`...') + v_dump_res := cli.run(v_cmd) + opt.verbose(4, v_dump_res.output) + + // Read in the dumped cflags + cflags := os.read_file(v_cflags_file) or { + flat_cmd := v_cmd.join(' ') + return error('${@MOD}.${@FN}: failed reading C flags to "${v_cflags_file}". ${err}\nCompile output of `${flat_cmd}`:\n${v_dump_res}') + } + + // Parse imported modules from dump + mut imported_modules := os.read_file(v_dump_modules_file) or { + flat_cmd := v_cmd.join(' ') + return error('${@MOD}.${@FN}: failed reading module dump file "${v_dump_modules_file}". ${err}\nCompile output of `${flat_cmd}`:\n${v_dump_res}') + }.split('\n').filter(it != '') + imported_modules.sort() + opt.verbose(3, 'Imported modules: ${imported_modules}') + return VMetaInfo{ + imports: imported_modules + c_flags: cflags.split('\n') + } +} + +pub struct CCompileOptions { + verbosity int // level of verbosity + cache bool = true + parallel bool = true + input string @[required] + output string // @[required] + c string // @[required] + cc string @[required] + c_flags []string // flags to pass to the C compiler(s) +} + +// verbose prints `msg` to STDOUT if `CCompileOptions.verbosity` level is >= `verbosity_level`. +pub fn (cco CCompileOptions) verbose(verbosity_level int, msg string) { + if cco.verbosity >= verbosity_level { + println(msg) + } +} + +pub fn (opt CCompileOptions) cmd() []string { + mut c_cmd := [ + opt.cc, + ] + if opt.c_flags.len > 0 { + c_cmd << opt.c_flags + } + if opt.c != '' { + c_cmd << '-c "${opt.c}"' + } + c_cmd << opt.input + if opt.output != '' { + c_cmd << '-o "${opt.output}"' + } + + return c_cmd +} + +// compile_v_to_c compiles V sources to their compatible C counterpart. +pub fn compile_v_to_c(opt VCompileOptions) !VMetaInfo { + work_dir := os.dir(opt.output) + paths.ensure(work_dir)! + + v_dump_opt := VDumpOptions{ + VCompileOptions: opt + //...opt.VCompileOptions + work_dir: os.join_path(work_dir, 'dump') + } + + v_meta_dump := v_dump_meta(v_dump_opt)! + imported_modules := v_meta_dump.imports + + if imported_modules.len == 0 { + return error('${@MOD}.${@FN}: empty module dump') + } + opt.verbose(1, 'Compiling V to C' + if opt.v_flags.len > 0 { + '. V flags: `${opt.v_flags}`' + } else { + '' + }) + + // Boehm-Demers-Weiser Garbage Collector (bdwgc / libgc) + opt.verbose(2, 'Garbage collection is ${opt.uses_gc()}') + + // Compile to X compatible C file + v_cmd := opt.cmd() + + opt.verbose(3, 'Running "${v_cmd.join(' ')}"...') + v_dump_res := cli.run_or_error(v_cmd)! + opt.verbose(3, v_dump_res) + + return v_meta_dump +} + +struct VImportCDeps { +pub: + o_files []string + a_files []string +} + +pub struct VCCompileOptions { +pub: + verbosity int // level of verbosity + cache bool = true + parallel bool = true + is_prod bool + cc string @[required] + c_flags []string // flags to pass to the C compiler(s) + work_dir string = os.join_path(paths.tmp_work(), 'export', 'v', 'cdeps') // temporary work directory + v_meta VMetaInfo +} + +// verbose prints `msg` to STDOUT if `VCCompileOptions.verbosity` level is >= `verbosity_level`. +pub fn (vcco VCCompileOptions) verbose(verbosity_level int, msg string) { + if vcco.verbosity >= verbosity_level { + println(msg) + } +} + +// compile_v_c_dependencies compiles the C dependencies in the V code. +pub fn compile_v_c_dependencies(opt VCCompileOptions) !VImportCDeps { + opt.verbose(1, 'Compiling V import C dependencies (.c to .o)' + + if opt.parallel { ' in parallel' } else { '' }) + + // err_sig := @MOD + '.' + @FN + v_meta_info := opt.v_meta + imported_modules := v_meta_info.imports + + // The following detects `#flag /path/to/file.o` entries in V source code that matches a module. + // + // Find all "*.o" entries in the C flags dump (obtained via `-dump-c-flags`). + // Match the (file)name of these .o files with what modules are actually imported + // (imports are obtained via `-dump-modules`) + // If they are module .o files - look for the corresponding `.o.description.txt` that V produces + // as `~/.vmodules/cache//.o.description.txt`. + // If the file exists read in it's contents to obtain the exact flags passed to the C compiler. + mut v_module_o_files := map[string][][]string{} + for line in v_meta_info.c_flags { + line_trimmed := line.trim('\'"') + if line_trimmed.contains('.module.') && line_trimmed.ends_with('.o') { + module_name := line_trimmed.all_after('.module.').all_before_last('.o') + if module_name in imported_modules { + description_file := line_trimmed.all_before_last('.o') + '.description.txt' + if os.is_file(description_file) { + if description_contents := os.read_file(description_file) { + desc := description_contents.split(' ').map(it.trim_space()).filter(it != '') + index_of_at := desc.index('@') + index_of_o := desc.index('-o') + index_of_c := desc.index('-c') + if desc.len <= 3 || index_of_at <= 0 || index_of_o <= 0 || index_of_c <= 0 { + if opt.verbosity > 2 { + println('Description file "${description_file}" does not seem to have valid contents for object file generation') + println('Description file contents:\n---\n"${description_contents}"\n---') + } + continue + } + v_module_o_files[module_name] << (desc[index_of_at + 2..]) + } + } + } + } + } + + mut o_files := []string{} + mut a_files := []string{} + + // uses_gc := opt.uses_gc() + build_dir := opt.work_dir + // is_debug_build := opt.is_debug_build() + + mut cflags_common := opt.c_flags.clone() + // if opt.is_prod { + // cflags_common << ['-Os'] + // } else { + // cflags_common << ['-O0'] + // } + // cflags_common << ['-fPIC'] + // cflags_common << ['-Wall', '-Wextra'] + + // v_thirdparty_dir := os.join_path(vxt.home(), 'thirdparty') + + mut jobs := []utils.ShellJob{} + mut cflags := cflags_common.clone() + o_dir := os.join_path(build_dir, 'o') + paths.ensure(o_dir)! + + c_compiler := opt.cc + + // Support builtin libgc which is enabled by default in V or via explicit passed `-gc` flag. + // if uses_gc { + // if opt.verbosity > 1 { + // println('Compiling libgc (${arch}) via -gc flag') + // } + // + // mut defines := []string{} + // if is_debug_build { + // defines << '-DGC_ASSERTIONS' + // defines << '-DGC_ANDROID_LOG' + // } + // if !opt.has_v_d_flag('no_gc_threads') { + // defines << '-DGC_THREADS=1' + // } + // defines << '-DGC_BUILTIN_ATOMIC=1' + // defines << '-D_REENTRANT' + // // NOTE it's currently a little unclear why this is needed. + // // V UI can crash and with when the gc is built into the exe and started *without* GC_INIT() the error would occur: + // defines << '-DUSE_MMAP' // Will otherwise crash with a message with a path to the lib in GC_unix_mmap_get_mem+528 + // + // o_file := os.join_path(arch_o_dir, 'gc.o') + // build_cmd := [ + // compiler, + // cflags.join(' '), + // '-I"' + os.join_path(v_thirdparty_dir, 'libgc', 'include') + '"', + // defines.join(' '), + // '-c "' + os.join_path(v_thirdparty_dir, 'libgc', 'gc.c') + '"', + // '-o "${o_file}"', + // ] + // util.verbosity_print_cmd(build_cmd, opt.verbosity) + // o_res := util.run_or_error(build_cmd)! + // if opt.verbosity > 2 { + // eprintln(o_res) + // } + // + // o_files[arch] << o_file + // + // jobs << job_utils.ShellJob{ + // cmd: build_cmd + // } + // } + + // Compile all detected `#flag /path/to/xxx.o` V source code entires that matches an imported module. + // NOTE: currently there's no way in V source to pass flags for specific architectures so these flags need + // to be added specially here. It should probably be supported as compile options from commandline... + for module_name, mod_compile_lines in v_module_o_files { + opt.verbose(2, 'Compiling .o files from module ${module_name}...') + + for compile_line in mod_compile_lines { + index_o_arg := compile_line.index('-o') + 1 + mut o_file := '' + if path := compile_line[index_o_arg] { + file_name := os.file_name(path).trim_space().trim('\'"') + o_file = os.join_path(o_dir, '${file_name}') + } + mut build_cmd := [ + c_compiler, + //'--no-entry', // TODO: + cflags.join(' '), + ] + for i, entry in compile_line { + if i == index_o_arg { + build_cmd << '"${o_file}"' + continue + } + build_cmd << entry + } + + opt.verbose(2, 'Compiling "${o_file}" from module ${module_name}...') + + // Dafuq?? + opt.verbose(3, 'Running "${build_cmd.join(' ')}"...') + // o_res := cli.run_or_error(build_cmd)! + // opt.verbose(3, o_res) + + o_files << o_file + + jobs << utils.ShellJob{ + cmd: build_cmd + } + } + } + + utils.run_jobs(jobs, opt.parallel, opt.verbosity)! + + return VImportCDeps{ + o_files: o_files + a_files: a_files + } +} diff --git a/export/export.v b/export/export.v index e4070eb..0b5e7e4 100644 --- a/export/export.v +++ b/export/export.v @@ -4,341 +4,93 @@ module export import os -import shy.vxt -// import vab.cli +import shy.paths -pub const available_format_strings = ['zip', 'directory', 'appimage_dir', 'appimage', 'apk', 'aab'] +pub const supported_exporters = ['zip', 'dir', 'android', 'appimage', 'wasm']! -pub fn work_dir() string { - return os.join_path(os.temp_dir(), 'export') -} - -pub fn ensure_cache_dir() !string { - dir := os.join_path(os.cache_dir(), 'shy', 'export') - if !os.is_dir(dir) { - os.mkdir_all(dir)! - } - return dir -} - -pub enum Variant { - generic - steam - steam_runtime -} - -pub enum Format { - zip // .zip - directory // /path/to/output - appimage_dir // .AppDir - appimage // .AppImage - android_apk // .apk - android_aab // .aab -} - -pub fn (f Format) ext() string { - return match f { - .zip { - 'zip' - } - .directory { - '' - } - .appimage_dir { - 'AppDir' - } - .appimage { - 'AppImage' - } - .android_apk { - 'apk' - } - .android_aab { - 'aab' - } - } -} - -pub struct Options { +pub struct ExportOptions { pub: // These fields would make little sense to change during a run - verbosity int - work_dir string = work_dir() + verbosity int @[short: v; xdoc: 'Verbosity level 1-3'] + work_dir string = os.join_path(paths.tmp_work(), 'export', 'appimage') @[ignore] // - run bool - parallel bool = true // Run, what can be run, in parallel - compress bool // Run upx if the host has it installed - cache bool // defaults to false in os.args/flag parsing phase - gl_version string = '3' + run bool @[ignore] + parallel bool = true @[long: 'no-parallel'; xdoc: 'Do not run tasks in parallel.'] + cache bool = true @[long: 'no-cache'; xdoc: 'Do not use cache'] + // echo and exit + dump_usage bool @[long: help; short: h; xdoc: 'Show this help message and exit'] + show_version bool @[long: version; xdoc: 'Output version information and exit'] pub mut: // I/O - input string - output string - format Format - variant Variant - is_prod bool - c_flags []string // flags passed to the C compiler(s) - v_flags []string // flags passed to the V compiler - assets []string // list of (extra) paths to asset (roots) dirs to include + input string @[tail] + output string @[short: o; xdoc: 'Path to output (dir/file)'] + // variant Variant + c_flags []string @[long: 'cflag'; short: c; xdoc: 'Additional flags for the C compiler'] + v_flags []string @[long: 'flag'; short: f; xdoc: 'Additional flags for the V compiler'] + assets []string @[short: a; xdoc: 'Asset dir(s) to include in build'] +mut: + supported_v_flags SupportedVFlags @[ignore] // export supports a selected range of V flags, these are parsed and dealt with separately } -// resolve_input returns the resolved path/file of the input. -fn (opt &Options) resolve_input() !string { - mut input := opt.input - // If no specific output file is given, we use the input file as a base - if input == '' { - return error('${@MOD}.${@FN}: no input given') - } - - if input in ['.', '..'] { - input = os.real_path(input) +// verbose prints `msg` to STDOUT if `AppImageOptions.verbosity` level is >= `verbosity_level`. +pub fn (eo &ExportOptions) verbose(verbosity_level int, msg string) { + if eo.verbosity >= verbosity_level { + println(msg) } - return input } -pub fn (opt &Options) resolve_io() !(string, string, Format) { - mut input := opt.resolve_input()! - mut output := opt.output - // If no specific output file is given, we use the input file as a base - if output == '' { - output = input - } - mut format := opt.format - ext := os.file_ext(output).all_after('.').to_lower() - // If user has explicitly named the output. E.g.: '/tmp/out.apk' - if ext != '' { - format = string_to_export_format(ext)! - return input, output, format - } - return input, output + '.' + format.ext(), format +pub fn (opt ExportOptions) is_debug_build() bool { + return opt.supported_v_flags.v_debug || opt.supported_v_flags.c_debug || '-cg' in opt.v_flags || '-g' in opt.v_flags } -pub fn export(opt &Options) ! { - if vxt.vexe() == '' { - return error('${@MOD}.${@FN}: No V install could be detected') - } - - if !os.is_dir(opt.work_dir) { - os.mkdir_all(opt.work_dir)! - } - - // Determine output path/file and format. - input, output, format := opt.resolve_io()! - - resolved_options := Options{ - ...opt - input: input - output: output - format: format - } - - if opt.verbosity > 0 { - eprintln('Exporting "${opt.input}" as ${format} to "${output}"...') - } - uos := os.user_os() - match format { - .zip { - return error('${@MOD}.${@FN}: zip export is not working yet') - } - .directory { - return error('${@MOD}.${@FN}: directory export is not working yet') - } - .appimage, .appimage_dir { - if uos != 'linux' { - return error('${@MOD}.${@FN}: AppImage format is only supported on Linux hosts') - } - export_appimage(resolved_options)! - } - .android_apk, .android_aab { - export_android(resolved_options)! - } - } -} - -pub fn string_to_export_format(str string) !Format { - return match str { - 'zip' { - .zip - } - 'directory' { - .directory - } - 'appimage' { - .appimage - } - 'appimage_dir' { - .appimage_dir - } - 'android_apk', 'apk' { - .android_apk - } - 'android_aab', 'aab' { - .android_aab - } - else { - error('${@MOD}.${@FN}: unsupported format "${str}". Available: ${available_format_strings}') - } - } +pub struct Result { +pub: + output string } -// pub struct Dependency{ -// path string -// // { 'so':so, 'path':path, 'realpath':realpath, 'dependants':set([executable]), 'type':'lib' } -// } - -struct ResolveDependenciesConfig { - verbosity int - indent int - exe string - excludes []string - skip_resolve []string - format Format +struct SupportedVFlags { +pub: + autofree bool + gc string + v_debug bool @[long: g] + c_debug bool @[long: cg] + prod bool + showcc bool + skip_unused bool + no_bounds_checking bool } -fn resolve_dependencies_recursively(mut deps map[string]string, config ResolveDependenciesConfig) ! { - // Resolving shared object (.so) dependencies on Linux is not as straight forward as - // one could wish for. Using `objdump` alone gives us only the *names* of the - // shared objects, not the full path. Using only `ldd` *does* result in resolved lib paths BUT - // they're done recursively, in some cases by executing the exe/lib - and, on top, it's printed - // *in one stream* which makes it impossible to know which libs has dependencies on which, - // further more `ldd` has security issues and problems with cross-compiled binaries. - // The issues are mostly ignored in our case since we consider the input (v sources -> binary) - // "trusted" and we do not support V cross-compiled binaries anyway at this point - // (Not sure AppImages even support it?!). - // - // Digging even further and reading source code of programs like `lddtree` will reveal - // that it's not straight forward to know what `.so` will be loaded by `ld` upon execution - // due to LD_LIBRARY_PATH mess and misuse etc. - // - // So. For now we've chosen a solution using a mix of both `objdump` and `ldd` - it has pitfalls for sure - - // but how many and how severe - only time will tell. If we are to do this "correctly" it'll need a lot - // more development time and special-cases (and native V modules for reading ELF binaries etc.) than what - // is feasible right now; We really just want to be able to collect a bunch of shared object files that - // a given V executable rely on in-order for us to collect them and package them up, for example, in an AppImage. - // - // The strategy is thus the following: - // 1. Run `objdump` on the exe/so file (had to choose one; readelf lost: - // https://stackoverflow.com/questions/8979664/readelf-vs-objdump-why-are-both-needed) - // this gives us the immediate (1st level) dependencies of the app. - // 2. Run `ldd` on the same exe/so file to obtain the first encountered resolved path(s) to the 1st level exe/so dependency. - // 3. Do step 1 and 2 for all dependencies, recursively - // 4. Cross our fingers and assume that 99.99% of cases will end up having happy users. - // The remaining user pool will hopefully be tech savy enough to fix/extend things themselves. - - verbosity := config.verbosity - indent := config.indent - mut root_indents := ' '.repeat(indent) + ' ' - if indent == 0 { - root_indents = '' +fn (svf &SupportedVFlags) as_v_flags() []string { + mut v_flags := []string{} + if svf.autofree { + v_flags << '-autofree' } - indents := ' '.repeat(indent + 1) + ' ' - executable := config.exe - excludes := config.excludes - skip_resolve := config.skip_resolve - - if verbosity > 0 { - base := os.file_name(executable) - eprintln('${root_indents}${base} (include)') + if svf.gc != '' { + v_flags << '-gc ${svf.gc}' } - objdump_cmd := [ - 'objdump', - '-x', - executable, - ] - od_res := os.execute(objdump_cmd.join(' ')) - if od_res.exit_code != 0 { - cmd := objdump_cmd.join(' ') - return error('${@MOD}.${@FN} "${cmd}" failed:\n${od_res.output}') + if svf.v_debug { + v_flags << '-g' } - od_lines := od_res.output.split('\n').map(it.trim_space()) - mut exe_deps := []string{} - for line in od_lines { - if !line.contains('NEEDED') { - continue - } - parts := line.split(' ').map(it.trim_space()).filter(it != '') - if parts.len != 2 { - continue - } - so_name := parts[1] - if so_name in excludes { - if verbosity > 1 { - eprintln('${indents}${so_name} (exclude)') - } - continue - } - exe_deps << so_name + if svf.c_debug { + v_flags << '-cg' } - - mut resolved_deps := map[string]string{} - - ldd_cmd := [ - 'ldd', - // '-r', - executable, - ] - ldd_res := os.execute(ldd_cmd.join(' ')) - if ldd_res.exit_code != 0 { - cmd := ldd_cmd.join(' ') - return error('${@MOD}.${@FN} "${cmd}" failed:\n${ldd_res.output}') + if svf.prod { + v_flags << '-prod' } - ldd_lines := ldd_res.output.split('\n').map(it.trim_space()) - for line in ldd_lines { - if line.contains('statically linked') { - continue - } - if line.contains('not found') { - // TODO ?? - give error here? add an option to continue? - continue - } - parts := line.split(' ') - if parts.len == 0 || parts.len < 3 { - continue - } - // dump(parts) - so_name := parts[0] - path := parts[2] - - if so_name in exe_deps { - if existing := resolved_deps[so_name] { - if existing != path { - eprintln('${indents}${so_name} Warning: resolved path is ambiguous "${existing}" vs. "${path}"') - } - continue - } - resolved_deps[so_name] = path - } - - // if _ := deps[so_name] { - // // Could add to "dependants" here for further info - // continue - //} + if svf.showcc { + v_flags << '-showcc' } - - for so_name, path in resolved_deps { - deps[so_name] = path - - if so_name in skip_resolve { - if verbosity > 1 { - eprintln('${indents}${so_name} (skip resolve)') - } - continue - } - - conf := ResolveDependenciesConfig{ - ...config - exe: path - indent: indent + 1 - } - resolve_dependencies_recursively(mut deps, conf)! + if svf.skip_unused { + v_flags << '-skip-unused' + } + if svf.no_bounds_checking { + v_flags << '-no-bounds-checking' } + return v_flags } -pub fn resolve_dependencies(config ResolveDependenciesConfig) !map[string]string { - mut deps := map[string]string{} - if config.verbosity > 0 { - eprintln('Resolving dependencies for executable "${config.exe}"...') - } - resolve_dependencies_recursively(mut deps, config)! - return deps +pub fn ensure_cache_dir() !string { + dir := os.join_path(paths.cache(), 'export') + paths.ensure(dir)! + return dir } diff --git a/export/export_android.v b/export/export_android.v deleted file mode 100644 index c8970f6..0000000 --- a/export/export_android.v +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright(C) 2022 Lars Pontoppidan. All rights reserved. -// Use of this source code is governed by an MIT license -// that can be found in the LICENSE file. -module export - -fn export_android(opt Options) ! { - mut gl_version := opt.gl_version - match opt.format { - .android_apk, .android_aab { - if gl_version in ['3', '2'] { - mut auto_gl_version := 'es2' - if gl_version == '3' { - auto_gl_version = 'es3' - } - if opt.verbosity > 0 { - eprintln('Auto adjusting OpenGL version for Android from ${gl_version} to ${auto_gl_version}') - } - gl_version = auto_gl_version - } - } - else {} - } - adjusted_options := Options{ - ...opt - gl_version: gl_version - } - if opt.verbosity > 3 { - eprintln('--- ${@MOD}.${@FN} ---') - eprintln(adjusted_options) - } -} diff --git a/export/wasm.v b/export/wasm.v new file mode 100644 index 0000000..416bf8e --- /dev/null +++ b/export/wasm.v @@ -0,0 +1,334 @@ +// Copyright(C) 2022 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module export + +import os +import flag +import shy.paths +import shy.cli + +pub const wasm_exporter_version = '0.0.1' +pub const wasm_fn_description = 'shy export wasm +exports both plain V applications and shy-based applications to HTML5/WASM that can run in a browser. +' + +pub enum WasmFormat { + emscripten +} + +pub struct WasmOptions { + ExportOptions +pub: + work_dir string = os.join_path(paths.tmp_work(), 'export', 'wasm') @[ignore] + format WasmFormat = .emscripten + s_flags []string +} + +fn (wo WasmOptions) help_or_docs() ?Result { + if wo.show_version { + return Result{ + output: 'shy export wasm ${wasm_exporter_version}' + } + } + + if wo.dump_usage { + export_doc := flag.to_doc[ExportOptions]( + name: 'shy export wasm' + version: '${wasm_exporter_version}' + description: wasm_fn_description + ) or { return none } + + wasm_doc := flag.to_doc[WasmOptions]( + options: flag.DocOptions{ + show: .flags | .flag_type | .flag_hint | .footer + } + ) or { return none } + + return Result{ + output: '${export_doc}\n${wasm_doc}' + } + } + return none +} + +pub fn args_to_wasm_options(arguments []string) !WasmOptions { + // Parse out all V flags supported (-gc none, -skip-unused, etc.) + // Flags that could not be parsed are returned as `args` (unmatched) via the the `.relaxed` mode. + supported_v_flags, args := flag.to_struct[SupportedVFlags](arguments, + skip: 1 + style: .v + mode: .relaxed + )! + + export_options, unmatched := flag.to_struct[ExportOptions](args)! + options, no_match := flag.to_struct[WasmOptions](unmatched)! + if no_match.len > 0 { + return error('Unrecognized argument(s): ${no_match}') + } + return WasmOptions{ + ...options + ExportOptions: ExportOptions{ + ...export_options + supported_v_flags: supported_v_flags + } + } +} + +pub fn wasm(opt WasmOptions) !Result { + if result := opt.help_or_docs() { + return result + } + + if opt.input == '' { + return error('${@MOD}.${@FN}: no input') + } + + // Resolve and sanitize input and output + input, output, format := opt.resolve_io()! + + paths.ensure(opt.work_dir)! + + match format { + .emscripten { + opt.verbose(1, 'Exporting to ${format}') + } + } + + // shy_root="$(pwd)" + // pro="$HOME/Projects/puzzle_vibes" + // v -skip-unused -gc none -d wasm32_emscripten -os wasm32_emscripten -o /tmp/shyem/vc_src.c $pro + // + // emcc -flto -fPIC -fvisibility=hidden --preload-file $shy_root/assets@/ --preload-file $pro/assets@/ -sEXPORTED_FUNCTIONS="['_malloc', '_main']" -sSTACK_SIZE=1mb -sERROR_ON_UNDEFINED_SYMBOLS=0 -sASSERTIONS=1 -sUSE_WEBGL2=1 -sUSE_SDL=2 -sNO_EXIT_RUNTIME=1 -sALLOW_MEMORY_GROWTH=1 -O0 -g -D_DEBUG_ -D_DEBUG -D SOKOL_GLES3 -D SOKOL_NO_ENTRY -D MINIAUDIO_IMPLEMENTATION -D _REENTRANT -I "$shy_root/thirdparty/stb" -I "$shy_root/thirdparty/fontstash" -I "$shy_root/thirdparty/sokol" -I "$shy_root/thirdparty/sokol/util" -I "$shy_root/wraps/miniaudio/c/miniaudio" -I "$shy_root/shy" -Wno-enum-conversion -Wno-unused-value $shy_root/thirdparty/stb/stbi.c /tmp/shyem/vc_src.c -lm -lpthread -ldl -o /tmp/shyem/vc_src.html + // + + // build_dir := os.join_path(opt.work_dir, 'build') + + v_c_file := os.join_path(opt.work_dir, 'v', 'wasm.c') + + mut v_flags := opt.v_flags.clone() + v_flags << ['-skip-unused', '-gc none', '-d wasm32_emscripten'] + v_to_c_opt := VCompileOptions{ + verbosity: opt.verbosity + cache: opt.cache + input: input + output: v_c_file + os: 'wasm32_emscripten' + v_flags: v_flags + } + + v_meta_dump := compile_v_to_c(v_to_c_opt) or { + return IError(CompileError{ + kind: .v_to_c + err: err.msg() + }) + } + + v_cflags := v_meta_dump.c_flags + imported_modules := v_meta_dump.imports + + // v_thirdparty_dir := os.join_path(vxt.home(), 'thirdparty') + // + // uses_gc := opt.uses_gc() + // + // + // TODO: cache check? + // + // TODO: Remove any previous builds?? + v_c_deps := os.join_path(paths.tmp_work(), 'export', 'v', 'cdeps') + v_c_c_opt := VCCompileOptions{ + verbosity: opt.verbosity + cache: opt.cache + parallel: opt.parallel + is_prod: opt.supported_v_flags.prod + cc: 'emcc' + // c_flags: v_cflags + work_dir: v_c_deps + v_meta: v_meta_dump + } + + vicd := compile_v_c_dependencies(v_c_c_opt) or { + return IError(CompileError{ + kind: .c_to_o + err: err.msg() + }) + } + mut o_files := vicd.o_files.clone() + mut a_files := vicd.a_files.clone() + + dump(o_files) + + mut cflags := opt.c_flags.clone() + mut sflags := opt.s_flags.clone() + mut includes := []string{} + mut defines := []string{} + mut ldflags := []string{} + + // Grab any external C flags + for line in v_cflags { + if line.contains('.tmp.c') || line.ends_with('.o"') { + continue + } + if line.starts_with('-D') { + defines << line + } + if line.starts_with('-I') { + if line.contains('/usr/') { + continue + } + includes << line + } + if line.starts_with('-l') { + if line.contains('-lgc') { + // not used / compiled in + continue + } + if line.contains('-lSDL') { + // different via -sUSE_SDL=X + continue + } + ldflags << line + } + } + + // sflags << '-sEXPORTED_FUNCTIONS="[\'_malloc\', \'_main\']" -sSTACK_SIZE=1mb -sERROR_ON_UNDEFINED_SYMBOLS=0 -sASSERTIONS=1 -sUSE_WEBGL2=1 -sNO_EXIT_RUNTIME=1 -sALLOW_MEMORY_GROWTH=1' + sflags << ['-sEXPORTED_FUNCTIONS="[\'_malloc\',\'_main\']"', '-sSTACK_SIZE=1mb', + '-sERROR_ON_UNDEFINED_SYMBOLS=0', '-sASSERTIONS=1', '-sUSE_WEBGL2=1', '-sNO_EXIT_RUNTIME=1', + '-sALLOW_MEMORY_GROWTH=1'] + // sflags << ['-sWASM=1'] + if 'sdl' in imported_modules { + sflags << '-sUSE_SDL=2' + } + + sflags << '--preload-file ${input}/assets@/' // TODO: + sflags << '--preload-file /home/lmp/Projects/vdev/shy/assets@/' // TODO: + + custom_shell_file := os.join_path(os.home_dir(), '.vmodules', 'shy', 'platforms', + 'wasm', 'emscripten', 'shell_minimal.html') + if os.is_file(custom_shell_file) { + sflags << '--shell-file ${custom_shell_file}' + } + + ldflags << '-ldl' // TODO: + // for asset in opt.assets { + // sflags << '--preload-file ${asset}@/' + // } + // --preload-file $shy_root/assets@/ --preload-file $pro/assets@/ + // + // -I "$shy_root/shy" + // -Wno-enum-conversion -Wno-unused-value + // -lm + // -lpthread + // -ldl + // -o /tmp/shyem/vc_src.html + + // ... still a bit of a mess + is_debug_build := opt.is_debug_build() + if opt.supported_v_flags.prod { + cflags << ['-Os'] + } else { + cflags << ['-O0'] + if is_debug_build { + cflags << '-g -D_DEBUG_ -D_DEBUG -gsource-map' + } + } + // -flto + cflags << ['-fPIC', '-fvisibility=hidden'] + //, '-ffunction-sections', '-fdata-sections', '-ferror-limit=1'] + + // cflags << ['-Wall', '-Wextra'] + + cflags << ['-Wno-unused-parameter'] // sokol_app.h + + // TODO V compile warnings - here to make the compiler(s) shut up :/ + cflags << ['-Wno-unused-variable', '-Wno-unused-result', '-Wno-unused-function', + '-Wno-unused-label'] + cflags << ['-Wno-missing-braces', '-Werror=implicit-function-declaration'] + cflags << ['-Wno-enum-conversion', '-Wno-unused-value', '-Wno-pointer-sign', + '-Wno-incompatible-pointer-types'] + + // if uses_gc { + // includes << '-I"' + os.join_path(v_thirdparty_dir, 'libgc', 'include') + '"' + // } + + opt.verbose(1, 'Compiling C output' + if opt.parallel { ' in parallel' } else { '' }) + + // Cross compile v.c to v.o lib files + o_file := os.join_path('/tmp', output, '${output}.html') // TODO: + paths.ensure(os.dir(o_file))! + // Compile .o + cco := CCompileOptions{ + verbosity: opt.verbosity + cache: opt.cache + parallel: opt.parallel + input: v_c_file + output: o_file + cc: 'emcc' + c_flags: [ + cflags.join(' '), + sflags.join(' '), + includes.join(' '), + defines.join(' '), + o_files.join(' '), + ldflags.join(' '), + ] + } + + // jobs << job_util.ShellJob{ + // cmd: build_cmd + // } + + c_cmd := cco.cmd() + opt.verbose(3, 'Running `${c_cmd.join(' ')}`...') + v_dump_res := cli.run_or_error(c_cmd) or { + return IError(CompileError{ + kind: .c_to_o + err: err.msg() + }) + } + + opt.verbose(4, v_dump_res) + + // job_util.run_jobs(jobs, opt.parallel, opt.verbosity) or { + // return IError(CompileError{ + // kind: .c_to_o + // err: err.msg() + // }) + // } + + return Result{ + output: '' + } +} + +// resolve_input returns the resolved path/file of the input. +fn (opt WasmOptions) resolve_input() !string { + mut input := opt.input.trim_right(os.path_separator) + // If no specific output file is given, we use the input file as a base + if input == '' { + return error('${@MOD}.${@FN}: no input given') + } + if input in ['.', '..'] || os.is_dir(input) { + input = os.real_path(input) + } + return input +} + +// resolve_output returns output according to what `input` contains. +fn (opt WasmOptions) resolve_output(input string) !string { + // Resolve output + mut output_file := '' + // Generate from defaults: [-o ] + default_file_name := input.all_after_last(os.path_separator).replace(' ', '_').to_lower() + if opt.output != '' { + output_file = os.join_path(opt.output.trim_right(os.path_separator), default_file_name) + } else { + output_file = default_file_name + } + return output_file +} + +pub fn (opt WasmOptions) resolve_io() !(string, string, WasmFormat) { + input := opt.resolve_input()! + return input, opt.resolve_output(input)!, opt.format +} diff --git a/lib/api_d_wasm32_emscripten.c.v b/lib/api_d_wasm32_emscripten.c.v index 8b40091..61b7cfe 100644 --- a/lib/api_d_wasm32_emscripten.c.v +++ b/lib/api_d_wasm32_emscripten.c.v @@ -3,16 +3,39 @@ // that can be found in the LICENSE file. module lib -import time +// import time import shy.analyse -import shy.mth +// import shy.mth #include +#include + +// Types (em_types.h) + +// type EmResult = C.EMSCRIPTEN_RESULT // int + +enum EmResult { + success = C.EMSCRIPTEN_RESULT_SUCCESS // 0 + deferred = C.EMSCRIPTEN_RESULT_DEFERRED // 1 + not_supported = C.EMSCRIPTEN_RESULT_NOT_SUPPORTED // -1 + failed_not_deferred = C.EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED // -2 + invalid_target = C.EMSCRIPTEN_RESULT_INVALID_TARGET // -3 + unknown_target = C.EMSCRIPTEN_RESULT_UNKNOWN_TARGET // -4 + invalid_param = C.EMSCRIPTEN_RESULT_INVALID_PARAM // -5 + failed = C.EMSCRIPTEN_RESULT_FAILED // -6 + no_data = C.EMSCRIPTEN_RESULT_NO_DATA // -7 + timed_out = C.EMSCRIPTEN_RESULT_TIMED_OUT // -8 +} +// emscripten.h fn C.emscripten_set_main_loop_arg(fn (voidptr), voidptr, int, bool) fn C.emscripten_cancel_main_loop() fn C.emscripten_run_script_int(charptr) int +// html5.h +fn C.emscripten_get_canvas_element_size(const_target &char, width &int, height &int) EmResult +fn C.emscripten_set_canvas_element_size(const_target &char, width int, height int) EmResult + type FnWithVoidptrArgument = fn (voidptr) fn (mut a ShyAPI) emscripten_main[T](mut ctx T, mut s Shy) ! { diff --git a/lib/window.sdl.v b/lib/window.sdl.v index f83732e..cb24daa 100644 --- a/lib/window.sdl.v +++ b/lib/window.sdl.v @@ -874,6 +874,23 @@ pub fn (w &Window) canvas() Canvas { mut width := 0 mut height := 0 + // $if wasm32_emscripten ? { + // if w.state.frame % 100 == 0 + // && C.emscripten_get_canvas_element_size(c'#canvas', &width, &height) == .success { + // println('WH: ${w.wh()}') + // //sdl.set_window_size(w.handle, width, height) + // // sdl.gl_delete_context(w.gl_context) + // // gl_context := sdl.gl_create_context(w.handle) + // // if gl_context == sdl.null { + // // sdl_error_msg := unsafe { cstring_to_vstring(sdl.get_error()) } + // // w.shy.log.gerror('${@STRUCT}.${@FN}', 'SDL: ${sdl_error_msg}') + // // panic('Could not create OpenGL context, SDL says:\n${sdl_error_msg}') + // // } + // // mut mw := unsafe{ w} + // // mw.gl_context = gl_context + // } + // } + // mut linked_version := sdl.Version{} // sdl.get_version(mut linked_version) // if linked_version.minor_version >= 26 { diff --git a/paths/LICENSE b/paths/LICENSE new file mode 100644 index 0000000..85910b2 --- /dev/null +++ b/paths/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2024 Lars Pontoppidan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/paths/paths.v b/paths/paths.v new file mode 100644 index 0000000..eebc3d3 --- /dev/null +++ b/paths/paths.v @@ -0,0 +1,67 @@ +// Copyright(C) 2023 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license file distributed with this software package +module paths + +import os + +pub const namespace = $d('shy:paths:namespace', 'shy') + +const sanitized_exe_name = os.file_name(os.executable()).replace(' ', '_').replace('.exe', + '').to_lower() + +// ensure creates `path` if it does not already exist. +pub fn ensure(path string) ! { + if !os.exists(path) { + os.mkdir_all(path) or { + return error('${@MOD}.${@FN}: error while making directory "${path}":\n${err}') + } + } +} + +// data returns a `string` with the path to `shy`'s' data directory. +// NOTE: the returned path may not exist on disk. Use `ensure/1` to ensure it exists. +pub fn data() string { + return os.join_path(os.data_dir(), namespace) +} + +// config returns a `string` with the path to `shy`'s' configuration directory. +// NOTE: the returned path may not exist on disk. Use `ensure/1` to ensure it exists. +pub fn config() string { + return os.join_path(os.config_dir() or { panic('${@MOD}.${@FN}: ${err}') }, namespace) +} + +// tmp_work returns a `string` with the path to `shy`'s' temporary work directory. +// NOTE: the returned path may not exist on disk. Use `ensure/1` to ensure it exists. +pub fn tmp_work() string { + return os.join_path(os.temp_dir(), namespace) +} + +// cache returns a `string` with the path to `shy`'s' cache directory. +// NOTE: the returned path may not exist on disk. Use `ensure/1` to ensure it exists. +pub fn cache() string { + return os.join_path(os.cache_dir(), namespace) +} + +// exe_data returns a `string` with the path to the executable's data directory. +// NOTE: the returned path may not exist on disk. Use `ensure/1` to ensure it exists. +pub fn exe_data() string { + return os.join_path(os.data_dir(), sanitized_exe_name) +} + +// exe_config returns a `string` with the path to the executable's configuration directory. +// NOTE: the returned path may not exist on disk. Use `ensure/1` to ensure it exists. +pub fn exe_config() string { + return os.join_path(os.config_dir() or { panic('${@MOD}.${@FN}: ${err}') }, sanitized_exe_name) +} + +// exe_tmp_work returns a `string` with the path to the executable's temporary work directory. +// NOTE: the returned path may not exist on disk. Use `ensure/1` to ensure it exists. +pub fn exe_tmp_work() string { + return os.join_path(os.temp_dir(), sanitized_exe_name) +} + +// exe_cache returns a `string` with the path to the executable's cache directory. +// NOTE: the returned path may not exist on disk. Use `ensure/1` to ensure it exists. +pub fn exe_cache() string { + return os.join_path(os.cache_dir(), sanitized_exe_name) +} diff --git a/platforms/wasm/emscripten/shell_minimal.html b/platforms/wasm/emscripten/shell_minimal.html new file mode 100644 index 0000000..df5a90f --- /dev/null +++ b/platforms/wasm/emscripten/shell_minimal.html @@ -0,0 +1,145 @@ + + + + + + Emscripten-Generated Code + + + +
+
emscripten
+
Downloading...
+
+ +
+
+ +
+
+ + + +
+ + {{{ SCRIPT }}} + + diff --git a/utils/print.v b/utils/print.v new file mode 100644 index 0000000..0980986 --- /dev/null +++ b/utils/print.v @@ -0,0 +1,81 @@ +// Copyright(C) 2023 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license file distributed with this software package +module utils + +import term + +const term_has_color_support = term.can_show_color_on_stderr() && term.can_show_color_on_stdout() + +pub enum MessageKind { + neutral + error + warning + notice + details +} + +@[params] +pub struct Details { +pub: + details string +} + +// shy_error prints `msg` prefixed with `error:` in red + `details` to STDERR. +pub fn shy_error(msg string, details Details) { + eprintln('${color(.error, bold('error:'))} ${msg}') + if details.details != '' { + eprintln('${color(.details, bold('details:'))}\n${format_details(details.details)}') + } +} + +// shy_warning prints `msg` prefixed with `error:` in yellow + `details` to STDERR. +pub fn shy_warning(msg string, details Details) { + eprintln('${color(.warning, bold('warning:'))} ${msg}') + if details.details != '' { + eprintln('${color(.details, bold('details:'))}\n${format_details(details.details)}') + } +} + +// shy_notice prints `msg` prefixed with `notice:` in magenta + `details` to STDERR. +// shy_notice can be disabled with `-d shy_no_notice` at compile time. +@[if !shy_no_notices ?] +pub fn shy_notice(msg string, details Details) { + println('${color(.notice, bold('notice:'))} ${msg}') + if details.details != '' { + eprintln('${color(.details, bold('details:'))}\n${format_details(details.details)}') + } +} + +fn format_details(s string) string { + return ' ${s.replace('\n', '\n ')}' +} + +fn bold(msg string) string { + if !term_has_color_support { + return msg + } + return term.bold(msg) +} + +fn color(kind MessageKind, msg string) string { + if !term_has_color_support { + return msg + } + return match kind { + .error { + term.red(msg) + } + .warning { + term.yellow(msg) + } + .notice { + term.magenta(msg) + } + .details { + term.bright_blue(msg) + } + else { + msg + } + } +} diff --git a/utils/shelljob.v b/utils/shelljob.v new file mode 100644 index 0000000..4f0749e --- /dev/null +++ b/utils/shelljob.v @@ -0,0 +1,97 @@ +// Copyright(C) 2019-2022 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license file distributed with this software package +module utils + +import os +import sync.pool + +pub struct ShellJobMessage { +pub: + std_out string + std_err string +} + +pub struct ShellJob { +pub: + message ShellJobMessage + cmd []string + env_vars map[string]string +} + +pub struct ShellJobResult { +pub: + job ShellJob + result os.Result +} + +// async_run runs all the ShellJobs in `pp` asynchronously. +fn async_run(mut pp pool.PoolProcessor, idx int, wid int) &ShellJobResult { + job := pp.get_item[ShellJob](idx) + return sync_run(job) +} + +// sync_run runs the `job` ShellJob. +fn sync_run(job ShellJob) &ShellJobResult { + for key, value in job.env_vars { + os.setenv(key, value, true) + } + if job.message.std_out != '' { + println(job.message.std_out) + } + if job.message.std_err != '' { + eprintln(job.message.std_err) + } + res := sj_run(job.cmd) + return &ShellJobResult{ + job: job + result: res + } +} + +// run_jobs runs all `jobs` jobs either in `parallel` or one after another. +pub fn run_jobs(jobs []ShellJob, parallel bool, verbosity int) ! { + if parallel { + mut pp := pool.new_pool_processor(callback: async_run) + pp.work_on_items(jobs) + for job_res in pp.get_results[ShellJobResult]() { + sj_verbosity_print_cmd(job_res.job.cmd, verbosity) + if job_res.result.exit_code != 0 { + return error('${job_res.job.cmd[0]} failed with return code ${job_res.result.exit_code}:\n${job_res.result.output}') + } + if verbosity > 2 { + println('${job_res.result.output}') + } + } + } else { + for job in jobs { + sj_verbosity_print_cmd(job.cmd, verbosity) + job_res := sync_run(job) + if job_res.result.exit_code != 0 { + return error('${job_res.job.cmd[0]} failed with return code ${job_res.result.exit_code}:\n${job_res.result.output}') + } + if verbosity > 2 { + println('${job_res.result.output}') + } + } + } +} + +// sj_verbosity_print_cmd prints information about the `args` at certain `verbosity` levels. +fn sj_verbosity_print_cmd(args []string, verbosity int) { + if args.len > 0 && verbosity > 1 { + cmd_short := args[0].all_after_last(os.path_separator) + mut output := 'Running ${cmd_short} From: ${os.getwd()}' + if verbosity > 2 { + output += '\n' + args.join(' ') + } + println(output) + } +} + +fn sj_run(args []string) os.Result { + res := os.execute(args.join(' ')) + if res.exit_code < 0 { + return os.Result{1, ''} + } + return res +}