diff --git a/flake-modules/wrappers.nix b/flake-modules/wrappers.nix index 1323e2bb3a..30fa85be2a 100644 --- a/flake-modules/wrappers.nix +++ b/flake-modules/wrappers.nix @@ -15,6 +15,13 @@ inherit (inputs) home-manager; nixvim = self; }).activationPackage; + home-manager-extra-files-byte-compiling = + import ../tests/modules/hm-extra-files-byte-compiling.nix + { + inherit pkgs; + inherit (inputs) home-manager; + nixvim = self; + }; } // pkgs.lib.optionalAttrs (!pkgs.stdenv.isDarwin) { nixos-module = diff --git a/lib/builders.nix b/lib/builders.nix index dcd2713d1d..ec7443af2c 100644 --- a/lib/builders.nix +++ b/lib/builders.nix @@ -1,5 +1,5 @@ { lib, pkgs }: -{ +rec { /* Write a lua file to the nix store, formatted using stylua. @@ -26,4 +26,110 @@ --indent-width 4 \ "$out" ''; + + /* + Write a byte compiled lua file to the nix store. + + # Type + + ``` + writeByteCompiledLua :: String -> String -> Derivation + ``` + + # Arguments + + - [name] The name of the derivation + - [text] The content of the lua file + */ + writeByteCompiledLua = + name: text: + pkgs.runCommandLocal name { inherit text; } '' + echo -n "$text" > "$out" + + ${lib.getExe' pkgs.luajit "luajit"} -bd -- "$out" "$out" + ''; + + /* + Get a source lua file and write a byte compiled copy of it + to the nix store. + + # Type + + ``` + byteCompileLuaFile :: String -> String -> Derivation + ``` + + # Arguments + + - [name] The name of the derivation + - [src] The path to the source lua file + */ + byteCompileLuaFile = + name: src: + pkgs.runCommandLocal name { inherit src; } '' + ${lib.getExe' pkgs.luajit "luajit"} -bd -- "$src" "$out" + ''; + + # Setup hook to byte compile all lua files in the output directory. + # Invalid lua files are ignored. + byteCompileLuaHook = pkgs.makeSetupHook { name = "byte-compile-lua-hook"; } ( + let + luajit = lib.getExe' pkgs.luajit "luajit"; + in + pkgs.writeText "byte-compile-lua-hook.sh" # bash + '' + byteCompileLuaPostFixup() { + # Target is a single file + if [[ -f $out ]]; then + if [[ $out = *.lua ]]; then + tmp=$(mktemp) + ${luajit} -bd -- "$out" "$tmp" + mv "$tmp" "$out" + fi + return + fi + + # Target is a directory + while IFS= read -r -d "" file; do + tmp=$(mktemp -u "$file.XXXX") + # Ignore invalid lua files + if ${luajit} -bd -- "$file" "$tmp"; then + mv "$tmp" "$file" + else + echo "WARNING: Ignoring byte compiling error for '$file' lua file" >&2 + fi + done < <(find "$out" -type f,l -name "*.lua" -print0) + } + + postFixupHooks+=(byteCompileLuaPostFixup) + '' + ); + + /* + Returns an overridden derivation with all lua files byte compiled. + + # Type + + ``` + byteCompileLuaDrv :: Derivation -> Derivation + ``` + + # Arguments + + - [drv] Input derivation + */ + byteCompileLuaDrv = + drv: + drv.overrideAttrs ( + prev: + { + nativeBuildInputs = prev.nativeBuildInputs or [ ] ++ [ byteCompileLuaHook ]; + } + // lib.optionalAttrs (prev ? buildCommand) { + buildCommand = '' + ${prev.buildCommand} + runHook postFixup + ''; + } + ); } diff --git a/modules/files.nix b/modules/files.nix index 68606af002..6b5b3def03 100644 --- a/modules/files.nix +++ b/modules/files.nix @@ -2,14 +2,16 @@ lib, helpers, pkgs, + config, ... }: let - fileType = lib.types.submodule ( + fileTypeModule = { name, config, options, + topConfig, ... }: { @@ -41,6 +43,14 @@ let type = lib.types.path; description = "Path of the source file."; }; + + finalSource = lib.mkOption { + type = lib.types.path; + description = "Path to the final source file."; + readOnly = true; + visible = false; + internal = true; + }; }; config = @@ -54,9 +64,29 @@ let # This means our `source` definition has the same priority as `text`. lib.mkDerivedConfig options.text (pkgs.writeText derivationName) ); + finalSource = + # Byte compile lua files if performance.byteCompileLua option is enabled + if + lib.hasSuffix ".lua" config.target + && topConfig.performance.byteCompileLua.enable + && topConfig.performance.byteCompileLua.configs + then + if lib.isDerivation config.source then + # Source is a derivation + helpers.byteCompileLuaDrv config.source + else + # Source is a path or string + helpers.byteCompileLuaFile derivationName config.source + else + config.source; }; - } - ); + }; + + fileType = lib.types.submoduleWith { + shorthandOnlyDefinesConfig = true; + modules = [ fileTypeModule ]; + specialArgs.topConfig = config; + }; # TODO: Added 2024-07-07, remove after 24.11 # Before we had a fileType, we used types.str. diff --git a/modules/performance.nix b/modules/performance.nix index db0953d452..2d08615866 100644 --- a/modules/performance.nix +++ b/modules/performance.nix @@ -4,6 +4,28 @@ let in { options.performance = { + byteCompileLua = { + enable = lib.mkEnableOption "byte compiling of lua files"; + initLua = lib.mkOption { + description = "Whether to byte compile init.lua."; + type = types.bool; + default = true; + example = false; + }; + configs = lib.mkOption { + description = "Whether to byte compile lua configuration files."; + type = types.bool; + default = true; + example = false; + }; + plugins = lib.mkEnableOption "plugins" // { + description = "Whether to byte compile lua plugins."; + }; + nvimRuntime = lib.mkEnableOption "nvimRuntime" // { + description = "Whether to byte compile lua files in Nvim runtime."; + }; + }; + combinePlugins = { enable = lib.mkEnableOption "combinePlugins" // { description = '' diff --git a/modules/top-level/files/default.nix b/modules/top-level/files/default.nix index c9ec06f828..d23c2726cb 100644 --- a/modules/top-level/files/default.nix +++ b/modules/top-level/files/default.nix @@ -84,11 +84,11 @@ in mkdir -p "$out" ${lib.concatMapStringsSep "\n" ( - { target, source, ... }: + { target, finalSource, ... }: lib.escapeShellArgs [ "makeEntry" # Force local source paths to be added to the store - "${source}" + "${finalSource}" target ] ) extraFiles} diff --git a/modules/top-level/output.nix b/modules/top-level/output.nix index 6f70876099..d828cd080f 100644 --- a/modules/top-level/output.nix +++ b/modules/top-level/output.nix @@ -87,8 +87,27 @@ in defaultPlugin // (if p ? plugin then p else { plugin = p; }); normalizePluginList = plugins: map normalize plugins; - # Normalized plugin list - normalizedPlugins = normalizePluginList config.extraPlugins; + # Byte compiling of normalized plugin list + byteCompilePlugins = + plugins: + let + byteCompile = + p: + (helpers.byteCompileLuaDrv p).overrideAttrs ( + prev: lib.optionalAttrs (prev ? dependencies) { dependencies = map byteCompile prev.dependencies; } + ); + in + map (p: p // { plugin = byteCompile p.plugin; }) plugins; + + # Normalized and optionally byte compiled plugin list + normalizedPlugins = + let + normalized = normalizePluginList config.extraPlugins; + in + if config.performance.byteCompileLua.enable && config.performance.byteCompileLua.plugins then + byteCompilePlugins normalized + else + normalized; # Plugin list extended with dependencies allPlugins = @@ -206,7 +225,17 @@ in config.content ]; - init = helpers.writeLua "init.lua" customRC; + textInit = helpers.writeLua "init.lua" customRC; + byteCompiledInit = helpers.writeByteCompiledLua "init.lua" customRC; + init = + if + config.type == "lua" + && config.performance.byteCompileLua.enable + && config.performance.byteCompileLua.initLua + then + byteCompiledInit + else + textInit; extraWrapperArgs = builtins.concatStringsSep " " ( (optional ( @@ -215,7 +244,28 @@ in ++ (optional config.wrapRc ''--add-flags -u --add-flags "${init}"'') ); - wrappedNeovim = pkgs.wrapNeovimUnstable config.package ( + package = + if config.performance.byteCompileLua.enable && config.performance.byteCompileLua.nvimRuntime then + # Using symlinkJoin to avoid rebuilding neovim + pkgs.symlinkJoin { + name = "neovim-byte-compiled-${lib.getVersion config.package}"; + paths = [ config.package ]; + # Required attributes from original neovim package + inherit (config.package) lua; + nativeBuildInputs = [ helpers.byteCompileLuaHook ]; + postBuild = '' + # Replace Nvim's binary symlink with a regular file, + # or Nvim will use original runtime directory + rm $out/bin/nvim + cp ${config.package}/bin/nvim $out/bin/nvim + + runHook postFixup + ''; + } + else + config.package; + + wrappedNeovim = pkgs.wrapNeovimUnstable package ( neovimConfig // { wrapperArgs = lib.escapeShellArgs neovimConfig.wrapperArgs + " " + extraWrapperArgs; @@ -232,7 +282,7 @@ in name = "nixvim-print-init"; runtimeInputs = [ pkgs.bat ]; text = '' - bat --language=lua "${init}" + bat --language=lua "${textInit}" ''; }; diff --git a/tests/fetch-tests.nix b/tests/fetch-tests.nix index c454655d01..ea37a6698d 100644 --- a/tests/fetch-tests.nix +++ b/tests/fetch-tests.nix @@ -14,7 +14,7 @@ let file = "${root}/${relativePath}/${name}"; in if type == "regular" then - [ + lib.optional (lib.hasSuffix ".nix" name) [ { namespace = namespace ++ [ (lib.strings.removeSuffix ".nix" name) ]; cases = import file; diff --git a/tests/modules/hm-extra-files-byte-compiling.nix b/tests/modules/hm-extra-files-byte-compiling.nix new file mode 100644 index 0000000000..3cb6b5b389 --- /dev/null +++ b/tests/modules/hm-extra-files-byte-compiling.nix @@ -0,0 +1,104 @@ +{ + nixvim, + pkgs, + home-manager, +}: +let + config = { + home = { + username = "nixvim"; + homeDirectory = "/invalid/dir"; + stateVersion = "24.05"; + }; + + programs.nixvim = { + enable = true; + + performance.byteCompileLua.enable = true; + + extraFiles = { + "extra-files/test1.lua".text = "vim.opt.tabstop = 2"; + "extra-files/test2.lua".source = builtins.toFile "file_source.lua" "vim.opt.tabstop = 2"; + "extra-files/test3.lua".source = pkgs.writeText "test3.lua" "vim.opt.tabstop = 2"; + "extra-files/test.vim".text = "set tabstop=2"; + "extra-files/test.json".text = builtins.toJSON { a = 1; }; + }; + + files = { + "files/test.lua".opts.tabstop = 2; + "files/test.vim".opts.tabstop = 2; + }; + }; + }; + + homeFilesByteCompilingEnabled = + (home-manager.lib.homeManagerConfiguration { + inherit pkgs; + + modules = [ + nixvim.homeManagerModules.nixvim + config + { programs.nixvim.performance.byteCompileLua.configs = true; } + ]; + }).config.home-files; + + homeFilesByteCompilingDisabled = + (home-manager.lib.homeManagerConfiguration { + inherit pkgs; + + modules = [ + nixvim.homeManagerModules.nixvim + config + { programs.nixvim.performance.byteCompileLua.configs = false; } + ]; + }).config.home-files; +in +pkgs.runCommand "home-manager-extra-files-byte-compiling" { } '' + is_binary() { + ! grep -qI . "$1" + } + test_byte_compiled() { + if ! is_binary "$home_files/.config/nvim/$1"; then + echo "File $1 is expected to be byte compiled, but it's not" + exit 1 + fi + } + test_not_byte_compiled() { + if is_binary "$home_files/.config/nvim/$1"; then + echo "File $1 is not expected to be byte compiled, but it is" + exit 1 + fi + } + + # Test directory with extraFiles byte compiling enabled + home_files="${homeFilesByteCompilingEnabled}" + + echo "Testing home-files with extraFiles byte compiling enabled" + + # extraFiles + test_byte_compiled extra-files/test1.lua + test_byte_compiled extra-files/test2.lua + test_byte_compiled extra-files/test3.lua + test_not_byte_compiled extra-files/test.vim + test_not_byte_compiled extra-files/test.json + # files + test_byte_compiled files/test.lua + test_not_byte_compiled files/test.vim + + # Test directory with extraFiles byte compiling disabled + home_files="${homeFilesByteCompilingDisabled}" + + echo "Testing home-files with extraFiles byte compiling disabled" + + # extraFiles + test_not_byte_compiled extra-files/test1.lua + test_not_byte_compiled extra-files/test2.lua + test_not_byte_compiled extra-files/test3.lua + test_not_byte_compiled extra-files/test.vim + test_not_byte_compiled extra-files/test.json + # files + test_not_byte_compiled files/test.lua + test_not_byte_compiled files/test.vim + + touch $out +'' diff --git a/tests/test-sources/modules/performance/byte-compile-lua.nix b/tests/test-sources/modules/performance/byte-compile-lua.nix new file mode 100644 index 0000000000..3ed2945047 --- /dev/null +++ b/tests/test-sources/modules/performance/byte-compile-lua.nix @@ -0,0 +1,287 @@ +{ pkgs, helpers, ... }: +let + isByteCompiledFun = '' + local function is_byte_compiled(filename) + local f = assert(io.open(filename, "rb")) + local data = assert(f:read("*a")) + -- Assume that file is binary if it contains null bytes + for i = 1, #data do + if data:byte(i) == 0 then + return true + end + end + return false + end + + local function test_rtp_file(name, is_compiled) + local file = assert(vim.api.nvim_get_runtime_file(name, false)[1], "file " .. name .. " not found in runtime") + if is_compiled then + assert(is_byte_compiled(file), name .. " is expected to be byte compiled, but it's not") + else + assert(not is_byte_compiled(file), name .. " is not expected to be byte compiled, but it is") + end + end + ''; +in +{ + default.module = + { config, ... }: + { + performance.byteCompileLua.enable = true; + + extraFiles = { + # By text + "plugin/file_text.lua".text = "vim.opt.tabstop = 2"; + # By simple source derivation using buildCommand + "plugin/file_source.lua".source = helpers.writeLua "file_source.lua" "vim.opt.tabstop = 2"; + # By standard derivation, it needs to execute fixupPhase + "plugin/file_drv.lua".source = pkgs.stdenvNoCC.mkDerivation { + name = "file_drv.lua"; + src = pkgs.emptyDirectory; + buildPhase = '' + echo "vim.opt.tabstop = 2" > $out + ''; + }; + # By path + "plugin/file_path.lua".source = ./files/file.lua; + # By string + "plugin/file_string.lua".source = builtins.toFile "file_path.lua" "vim.opt.tabstop = 2"; + # By derivation converted to string + "plugin/file_drv_string.lua".source = toString ( + helpers.writeLua "file_drv_string.lua" "vim.opt.tabstop = 2" + ); + # Non-lua files + "plugin/test.vim".text = "set tabstop=2"; + "plugin/test.json".text = builtins.toJSON { a = 1; }; + # Lua file with txt extension won't be byte compiled + "test.txt".source = helpers.writeLua "test.txt" "vim.opt.tabstop = 2"; + }; + + files = { + "plugin/file.lua" = { + opts.tabstop = 2; + }; + "plugin/file.vim" = { + opts.tabstop = 2; + }; + }; + + extraPlugins = [ pkgs.vimPlugins.nvim-lspconfig ]; + + extraConfigLua = '' + -- The test will search for this string in nixvim-print-init output: VALIDATING_STRING. + -- Since this is the comment, it won't appear in byte compiled file. + ''; + + # Using plugin for the test code to avoid infinite recursion + extraFiles."plugin/test.lua".text = '' + ${isByteCompiledFun} + + -- vimrc is byte compiled + local init = vim.env.MYVIMRC or vim.fn.getscriptinfo({name = "init.lua"})[1].name + assert(is_byte_compiled(init), "MYVIMRC is expected to be byte compiled, but it's not") + + -- nixvim-print-init prints text + local init_content = vim.fn.system("${config.printInitPackage}/bin/nixvim-print-init") + assert(init_content:find("VALIDATING_STRING"), "nixvim-print-init's output is byte compiled") + + -- lua extraFiles are byte compiled + test_rtp_file("plugin/file_text.lua", true) + test_rtp_file("plugin/file_source.lua", true) + test_rtp_file("plugin/file_drv.lua", true) + test_rtp_file("plugin/file_path.lua", true) + test_rtp_file("plugin/file_string.lua", true) + test_rtp_file("plugin/file_drv_string.lua", true) + test_rtp_file("plugin/test.vim", false) + test_rtp_file("plugin/test.json", false) + test_rtp_file("test.txt", false) + + -- lua files are byte compiled + test_rtp_file("plugin/file.lua", true) + test_rtp_file("plugin/file.vim", false) + + -- Plugins and neovim runtime aren't byte compiled by default + test_rtp_file("lua/vim/lsp.lua", false) + test_rtp_file("lua/lspconfig.lua", false) + ''; + + }; + + disabled.module = + { config, ... }: + { + performance.byteCompileLua.enable = false; + + extraFiles."plugin/test1.lua".text = "vim.opt.tabstop = 2"; + + files."plugin/test2.lua".opts.tabstop = 2; + + extraPlugins = [ pkgs.vimPlugins.nvim-lspconfig ]; + + extraConfigLua = '' + -- The test will search for this string in nixvim-print-init output: VALIDATING_STRING. + -- Since this is the comment, it won't appear in byte compiled file. + ''; + + # Using plugin for the test code to avoid infinite recursion + extraFiles."plugin/test.lua".text = '' + ${isByteCompiledFun} + + -- vimrc + local init = vim.env.MYVIMRC or vim.fn.getscriptinfo({name = "init.lua"})[1].name + assert(not is_byte_compiled(init), "MYVIMRC is not expected to be byte compiled, but it is") + + -- nixvim-print-init prints text + local init_content = vim.fn.system("${config.printInitPackage}/bin/nixvim-print-init") + assert(init_content:find("VALIDATING_STRING"), "nixvim-print-init's output is byte compiled") + + -- Nothing is byte compiled + -- extraFiles + test_rtp_file("plugin/test1.lua", false) + -- files + test_rtp_file("plugin/test2.lua", false) + -- Plugins + test_rtp_file("lua/lspconfig.lua", false) + -- Neovim runtime + test_rtp_file("lua/vim/lsp.lua", false) + ''; + + }; + + init-lua-disabled = { + performance.byteCompileLua = { + enable = true; + initLua = false; + }; + + extraConfigLuaPost = '' + ${isByteCompiledFun} + + -- vimrc is not byte compiled + local init = vim.env.MYVIMRC or vim.fn.getscriptinfo({name = "init.lua"})[1].name + assert(not is_byte_compiled(init), "MYVIMRC is not expected to be byte compiled, but it is") + ''; + }; + + configs-disabled = { + performance.byteCompileLua = { + enable = true; + configs = false; + }; + + extraFiles."plugin/test1.lua".text = "vim.opt.tabstop = 2"; + + files."plugin/test2.lua".opts.tabstop = 2; + + extraConfigLuaPost = '' + ${isByteCompiledFun} + + -- extraFiles + test_rtp_file("plugin/test1.lua", false) + -- files + test_rtp_file("plugin/test2.lua", false) + ''; + }; + + nvim-runtime = { + performance.byteCompileLua = { + enable = true; + nvimRuntime = true; + }; + + extraPlugins = [ + # Python 3 dependencies + (pkgs.vimPlugins.nvim-lspconfig.overrideAttrs { passthru.python3Dependencies = ps: [ ps.pyyaml ]; }) + ]; + + extraConfigLuaPost = '' + ${isByteCompiledFun} + + -- vim namespace is working + vim.opt.tabstop = 2 + vim.api.nvim_get_runtime_file("init.lua", false) + vim.lsp.get_clients() + vim.treesitter.language.get_filetypes("nix") + vim.iter({}) + + test_rtp_file("lua/vim/lsp.lua", true) + test_rtp_file("lua/vim/iter.lua", true) + test_rtp_file("lua/vim/treesitter/query.lua", true) + test_rtp_file("lua/vim/lsp/buf.lua", true) + test_rtp_file("plugin/editorconfig.lua", true) + test_rtp_file("plugin/tutor.vim", false) + test_rtp_file("ftplugin/vim.vim", false) + + -- Python3 packages are importable + vim.cmd.py3("import yaml") + ''; + }; +} +// + # Two equal tests, one with combinePlugins.enable = true + pkgs.lib.genAttrs + [ + "plugins" + "plugins-combined" + ] + (name: { + performance = { + byteCompileLua = { + enable = true; + plugins = true; + }; + + combinePlugins.enable = pkgs.lib.hasSuffix "combined" name; + }; + + extraPlugins = with pkgs.vimPlugins; [ + nvim-lspconfig + # Depends on plenary-nvim + telescope-nvim + # buildCommand plugin with python3 dependency + ((pkgs.writeTextDir "/plugin/test.lua" "vim.opt.tabstop = 2").overrideAttrs { + passthru.python3Dependencies = ps: [ ps.pyyaml ]; + }) + # Plugin with invalid lua file tests/indent/lua/cond.lua (should be ignored) + nvim-treesitter + ]; + + extraConfigLuaPost = '' + ${isByteCompiledFun} + + -- Plugins are loadable + require("lspconfig") + require("telescope") + require("plenary") + require("nvim-treesitter") + + -- Python modules are importable + vim.cmd.py3("import yaml") + + -- nvim-lspconfig + test_rtp_file("lua/lspconfig.lua", true) + test_rtp_file("lua/lspconfig/server_configurations/nixd.lua", true) + test_rtp_file("plugin/lspconfig.lua", true) + test_rtp_file("doc/lspconfig.txt", false) + + -- telescope-nvim + test_rtp_file("lua/telescope/init.lua", true) + test_rtp_file("lua/telescope/builtin/init.lua", true) + test_rtp_file("plugin/telescope.lua", true) + test_rtp_file("autoload/health/telescope.vim", false) + test_rtp_file("doc/telescope.txt", false) + + -- Dependency of telescope-nvim (plenary-nvim) + test_rtp_file("lua/plenary/init.lua", true) + test_rtp_file("plugin/plenary.vim", false) + + -- Test plugin + test_rtp_file("plugin/test.lua", true) + + -- nvim-treesitter + test_rtp_file("lua/nvim-treesitter/health.lua", true) + test_rtp_file("lua/nvim-treesitter/install.lua", true) + test_rtp_file("plugin/nvim-treesitter.lua", true) + test_rtp_file("queries/nix/highlights.scm", false) + ''; + }) diff --git a/tests/test-sources/modules/performance/files/file.lua b/tests/test-sources/modules/performance/files/file.lua new file mode 100644 index 0000000000..0ba6a7d950 --- /dev/null +++ b/tests/test-sources/modules/performance/files/file.lua @@ -0,0 +1 @@ +vim.opt.tabstop = 2 diff --git a/wrappers/_shared.nix b/wrappers/_shared.nix index af9848b5f3..a707e3ee5a 100644 --- a/wrappers/_shared.nix +++ b/wrappers/_shared.nix @@ -53,11 +53,11 @@ in setAttrByPath filesOpt ( listToAttrs ( map ( - { target, source, ... }: + { target, finalSource, ... }: { name = filesPrefix + target; value = { - inherit source; + source = finalSource; }; } ) extraFiles