New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Switch luvit to use luvit-loader instead of luvit/require package #932
base: master
Are you sure you want to change the base?
Conversation
fc4c699
to
a9591a4
Compare
a9591a4
to
9c0023a
Compare
Hmm, looks like something is wrong with windows. Perhaps luvit-loader assumed unix paths somewhere. |
With this breaking change, I think it might also be worth re-evaluating relative requires, as they won't behave as expected with luvit-loader. See:
That is, relative requires are only truly relative on their first require, and if you require a module with the same relative path from a new location, you will get the cached value of the first require rather than the return from requiring the intended relative module. Off the top of my head, this could be addressed by making relative requires relative to the bundle root rather than the current file. |
Also, feel free to use the luvit-loader require tests I adapted for luver (it's mostly the same as the Luvit require tests, with a few extra tests). The method I used to fake the currently executing file is a bit strange, but it works. EDIT: Also worth noting that luvit-loader removes the circular dependency support that Luvit 2.0's require had. |
Here's the error I'm getting on Windows when doing
|
The Windows build error was due to the require auto-register in Luvi + require being included through package.lua: https://github.com/luvit/luvi/blob/master/src/lua/luvibundle.lua#L327-L333 Weirdly enough, the same thing that affected Lit a while back is the reason why it still worked in the Linux CI (note also this might mean that the require auto-register in Luvi never worked in the Linux CI). (EDIT: Actually, unsure about this one--it is probably something similar but not exactly the same issue) |
Thanks @squeek502 |
@@ -115,7 +117,7 @@ return require('./init')(function (...) | |||
if startRepl == nil and not script then startRepl = true end | |||
|
|||
if script then | |||
require(luvi.path.join(uv.cwd(), script)) | |||
dofile(luvi.path.join(uv.cwd(), script)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs some special handling to maintain compatibility with things like luvit .
redirecting to ./init.lua
. Currently getting this error when doing luvit .
:
C:\luvit-relative-require>luvit .
Uncaught exception:
cannot open C:\luvit-relative-require: Permission denied
stack traceback:
[C]: in function 'dofile'
[string "bundle:main.lua"]:120: in function 'main'
[string "bundle:init.lua"]:51: in function <[string "bundle:init.lua"]:49>
[C]: in function 'xpcall'
[string "bundle:init.lua"]:49: in function <[string "bundle:init.lua"]:20>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's an edge case I don't mind dropping support for. The closer we can get to vanilla lua behavior the better.
To attempt to demonstrate the issue I brought up with relative requires, here's a simple (and somewhat likely) example: File structure:
Where the root init.lua contains: local one = require('one')
local two = require('two')
print("one version: ", one.version())
print("two version: ", two.version()) each init.lua in deps contains: return {
version = function()
local meta = require('./package')
return meta.version
end
} and one's package.lua has Here's the output of running Luvit 2 and Luvit 3 on the root init.lua:
There's really no way to get around this problem with a Lua package loader, as the |
:/ |
So basically, there is no sane way to have relative requires in lua. I guess we could just write a library that doesn't touch require at all (it can |
Yeah, I think that'd make the most sense--add a separate module specifically for relative requires. Another alternative would be to add a function that resolves a relative path to an absolute path based on the currently executing script, and then chain that into a require, e.g.:
so that would then make the module name the absolute path, thereby making sure each module is unique. |
Try this: mymodule.lua local modname, modpath = ...
print(modname)
if not modpath then
print("No modpath: Lua 5.1 detected")
else
print(modpath)
end
return {helloworld = function() print("Hello World!") end} something.lua local mymodule = require"mymodule"
mymodule.helloworld() Run with This is how you should do relative requires in Lua. |
@squeek502 I don't think any of the default loaders support absolute paths. But I guess I could add a simple loader that does. |
@creationix the current loader has basic support for Linux absolute paths (but not Windows): https://github.com/luvit/luvit/pull/932/files#diff-b511e047bb8a8ec669ceeedd21cc8a73R176 @SoniEx2 not sure how that's relevant. |
@squeek502 The suggestion by @SoniEx2 is a replacement for the debug.* hack and greatly simplifies the custom loader (in fact we will only need one for bundle paths) |
So this works for basic relative requires without any loader at all for files on disk:
-- main.lua
package.path = "./?/init.lua;" .. package.path
require('libs.foo')
require('libs.bar')
-- libs/foo/init.lua
print("Loading foo")
require(... .. ".local")
--- libs/foo/local.lua
print("loading foo.local")
-- libs/bar/init.lua
print("Loading bar")
require(... .. ".local")
--- libs/bar/local.lua
print("loading bar.local") Output
|
This above solution is very simple and works for some of the use cases. But there are still some problems.
> cd ..
> luajit test/main.lua
luajit: test/main.lua:2: module 'libs.foo' not found:
no field package.preload['libs.foo']
no file './libs/foo/init.lua'
no file './libs/foo.lua'
no file '/usr/local/Cellar/luajit/2.0.4_3/share/luajit-2.0.4/libs/foo.lua'
no file '/usr/local/share/lua/5.1/libs/foo.lua'
no file '/usr/local/share/lua/5.1/libs/foo/init.lua'
no file '/usr/local/Cellar/luajit/2.0.4_3/share/lua/5.1/libs/foo.lua'
no file '/usr/local/Cellar/luajit/2.0.4_3/share/lua/5.1/libs/foo/init.lua'
no file './libs/foo.so'
no file '/usr/local/lib/lua/5.1/libs/foo.so'
no file '/usr/local/Cellar/luajit/2.0.4_3/lib/lua/5.1/libs/foo.so'
no file '/usr/local/lib/lua/5.1/loadall.so'
no file './libs.so'
no file '/usr/local/lib/lua/5.1/libs.so'
no file '/usr/local/Cellar/luajit/2.0.4_3/lib/lua/5.1/libs.so'
no file '/usr/local/lib/lua/5.1/loadall.so'
stack traceback:
[C]: in function 'require'
test/main.lua:2: in main chunk
[C]: at 0x010352e320 The luvit require system works independent of the cwd and thus this is a serious regression. |
Also we can't do more advanced searches like find |
Try setting the module root in package.path. You can do that in luvi, right? |
@squeek502 how about this:
|
I like that. It seems Luvit has been moving towards plain Lua conventions for a while, so this just seems like another leap towards that. What about deps/libs special handling? |
That would handled by the resolve library, not in core or a loader. |
That seems like it might be a bit odd, as that would mean you'd need to write different code to require a Lit-style dependency as opposed to a Lua-style one, right? Wouldn't basically all requires in a Lit-style package have to use the resolve helper, thereby making them either 1) incompatible with plain Lua or 2) always depend on the resolve helper? I feel like I'm probably thinking about this wrong. Could you give an example of what the require calls of something like lit's libs/stats.lua would look like? |
You're right that require alone can't do lit style path resolving exactly because of the problem with the cache you found. We have to resolve the relative path to a full path before calling require or the key in the cache will be too ambiguous. We can either write a resolve library that returns the path and chain it to native require or write a library that replaces require and does the resolve internally. |
I don't think I've said there's a problem with
Could you expand on this a bit? Are you talking about something like The only benefit to resolving that sort of thing beforehand I see would be to allow multiple versions of the same module to be |
Yes. Though lit doesn't support automatically making such deep trees (unlike npm which used to make deep trees by default). I think strongly preferring the shallow style makes most applications saner. I just want to support the edge case of conflicting packages with the same name through manual means for people who really need/want it. A resolve library can implement this logic and pass on lua style module names to require. |
Absolutely, I just don't think it should be the only way to require things in a Lit-style package. |
How do you versioning? |
I think it is entirely possible to make relative requires work without patching the top level require itself or adding a helper. Presumably, if something is doing a relative require in the luvit convention, it is either the root package, or a package in the luvit ecosystem that was loaded by a luvit loader. The cache of resolved paths is exposed in package.loaded and is editable. The luvit loader or loaders can keep a hidden state. Whenever a luvit loader is invoked to resolve a relative path, it can unset the package.loaded value for the package stored in the given state if any, then write its own package name into that state. Any relative requires required from that included file will get recorded into the frame on top of the state. When the file being included returns to the loader but before the loader returns to require, the loader unsets the package.loaded values corresponding to the loads recorded in the top frame and pops it from the stack. This guarantees that the cache values for relative path requires won't be present outside of the file that originally required them. Unfortunately, that doesn't allow the loaded cache and relative resolvers to be kept working for code in the file that doesn't run at first load, but I think it may work well enough or be straightforward to fix. And how many libraries need to lazy-load things with require? Injecting an identically behaving require with a separate package.loaded cache into the code loaded by luvit relative loaders would allow that to work. |
@aiverson sounds interesting, I'm not sure I quite understand your proposal. |
I think he's basically suggesting what luvit's Here's the
So, to use a separate cache, we'd need to either hook before the first step, to invalidate lookups so that we can use our cache as the lookup, or after the last step, to nil out |
Here is some pseudocode to show my suggestion in more detail. local held_package
local reldirs = {process_path()}
package.luvitloaded = {}
local function relative_loader(name)
if held_package then package.loaded[held_package] = nil end
local source, dir = search_package(name, reldirs[#reldirs])
local res
if source then
reldirs[#reldirs + 1] = dir
res = package.luvitloaded[source] or dofile(source, name)
package.luvitloaded[source] = res
reldirs[#reldirs] = nil
else
held_package = nil
end
if held_package then package.loaded[held_package] = nil end
held_package = name
return res
end
local function absolute_loader(name)
if held_package then package.loaded[held_package] = nil end
held_package = nil
--Don't invalidate the cache on absolute paths
local source, dir = search_package(name)
local res
if source then
reldirs[#reldirs + 1] = dir
res = package.luvitloaded[source] or dofile(source, name)
package.luvitloaded[source] = res
reldirs[#reldirs] = nil
end
if held_package then package.loaded[held_package] = nil end
held_package = nil
return res
end I hope that helps explain it. This scheme ensures that all of the loaded cache entries for relative paths based on any directory besides the current one are invalidated before |
Could you go into a bit more detail about what problem you're trying to solve? If it's the problem I outlined in this comment, then I'm confused about how it's addressed by your code. I might be missing something, though, as I'm unsure what Just to be clear, this is the problem as I see it (if we're only adding loaders and not implementing a full require replacement): -- this is the C require function loosely converted to Lua code
function require(name)
if package.loaded[name] then
return package.loaded[name]
end
-- this is where the loaders get called,
-- and one loader **must** return a result, or require will error
local res = ...
-- the C code then sets package.loaded[name] to the returned value (or true)
package.loaded[name] = res or true
end
-- so, calling require with the same module name from different files
-- will just return the first one required
-- from /foo/bar.lua
-- returns the value from /foo/relative.lua
-- and sets package.loaded['./relative']
require('./relative')
-- from /bar/foo.lua
-- should return the value from /bar/relative.lua,
-- but actually returns the cached /foo/relative.lua
-- from package.loaded['./relative']
require('./relative') Unless I'm missing something, this is intractable whenever the same module name is passed to require, as the C require code will bypass the loaders entirely if |
In this case, the loader that is loading foo/bar.lua will unset |
This isn't possible.
|
Yes it is. The call stack will look like this.
When |
Ah, ok, I see now, thanks for explaining. |
Interesting technique. |
This is the start for the proposed luvit 3.0 change.
This change in particular is a breaking change for some edge cases of require usage.