diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 82ff0ce8d..fac00a013 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -166,6 +166,30 @@ jobs: exit 1 fi + # A Lua module runs at build time and its answer is baked into the page that invoked it + # (#465). Module:Example answers with the title of the page it ran on, so this says both + # that Scribunto rendered at all -- without it the invocation is left in the page as + # "{{#invoke:Example|thisPage}}", which is what used to happen quietly -- and that the + # module saw which page it was on, per language. + # Spelled out per language rather than looped, because the file name has underscores where + # the title the module answers with has spaces. + if ! grep -q 'Lua modules' dist/Lua_modules.html; then + echo "::error::dist/Lua_modules.html does not carry Module:Example's answer" + exit 1 + fi + if ! grep -q 'Lua modules/ko' dist/Lua_modules/ko.html; then + echo "::error::dist/Lua_modules/ko.html does not carry Module:Example's answer for Korean" + exit 1 + fi + + # And the module itself is not a page of the site: Module: is not a content namespace, so + # nothing should export the Lua source. Without Scribunto it does, percent-encoded. + if ls dist/Module* >/dev/null 2>&1; then + echo "::error::the Lua module was exported as a page:" + ls dist/Module* + exit 1 + fi + # Every printfooter "Retrieved from" link must resolve from the page that carries it # (#394). Core builds it from the page's own URL and expands away the "./" wikven writes, # so a page exported into a subdirectory needs the "../" per level rename.php adds; the diff --git a/docs/.wikven.yml b/docs/.wikven.yml index c31002b44..6ef8456a8 100644 --- a/docs/.wikven.yml +++ b/docs/.wikven.yml @@ -3,6 +3,9 @@ extensions: - Gadgets - MobileFrontend - ParserFunctions + # This site has one Lua module, and Special:MyLanguage/Lua modules is about it. Listing Scribunto + # is all it takes: MediaWiki bundles the extension and the image compiles luasandbox into its PHP. + - Scribunto - SyntaxHighlight_GeSHi - TabberNeue - TemplateStyles diff --git a/docs/Development.wikitext b/docs/Development.wikitext index 5d356aed5..892983e38 100644 --- a/docs/Development.wikitext +++ b/docs/Development.wikitext @@ -68,6 +68,8 @@ The division is what makes the passes safe to run beside each other: the orchest === Phase 1: filling the wiki === +; checkLuaAgainstThisBuild +: Says what this site's [[Special:MyLanguage/Lua modules|Lua]] and this build's Lua make of each other, before anything else happens. A site that lists Scribunto where no engine can run it ends the build, because it asked for something this build cannot do; a site with Module: files that never listed Scribunto is only told what those files come to. Both used to pass quietly and publish pages with the invocation showing in them. It runs first so the last bake is still in place when a site is refused. ; clearOutputDirectory : Empties the output directory, so a bake never inherits a file from the last one. ; setMainPage diff --git a/docs/Development/ko.wikitext b/docs/Development/ko.wikitext index a5bb9cc81..c3188875f 100644 --- a/docs/Development/ko.wikitext +++ b/docs/Development/ko.wikitext @@ -46,7 +46,9 @@ === 1단계: 위키 채우기 === - + +; checkLuaAgainstThisBuild +: 이 사이트의 [[$1|Lua]]와 이 빌드의 Lua가 서로를 어떻게 보는지를, 다른 어떤 일이 벌어지기 전에 말합니다. Scribunto를 나열했지만 그것을 실행할 엔진이 없는 사이트는 빌드를 끝내는데, 이 빌드가 할 수 없는 일을 요구했기 때문입니다. 반면 Scribunto를 나열한 적 없이 Module: 파일만 있는 사이트에는 그 파일들이 무엇이 되는지를 알려주기만 합니다. 둘 다 예전에는 조용히 통과해 호출문이 그대로 보이는 문서를 게시했습니다. 가장 먼저 실행되므로, 사이트가 거부될 때 지난 빌드 결과가 아직 그대로 남아 있습니다. ; clearOutputDirectory : 출력 디렉터리를 비워, 이전 빌드의 파일이 남지 않도록 합니다. ; setMainPage diff --git a/docs/Extensions.wikitext b/docs/Extensions.wikitext index f3cbb69f0..478ac886a 100644 --- a/docs/Extensions.wikitext +++ b/docs/Extensions.wikitext @@ -81,8 +81,11 @@ An extension has to do its work during the build, because the published site is Extensions that only change how wikitext renders — parser tags, parser functions, styling, templating — are the ones that work best, because their whole job is over by the time the page is written out. + +Scribunto is one of those, and the one most wikis reach for: it has a page of its own at [[Special:MyLanguage/Lua modules|Lua modules]], because a Lua module is content as well as configuration and the [[Special:MyLanguage/Standalone binary|standalone binary]] cannot render it. + An extension of your own can also be made to survive a build deliberately; [[Special:MyLanguage/Writing an extension|Writing an extension]] is the contract for that. -{{prevnext|Skins|JavaScript}} +{{prevnext|Skins|Lua modules}} diff --git a/docs/Extensions/ko.wikitext b/docs/Extensions/ko.wikitext index 31b0c6260..76e2a86e2 100644 --- a/docs/Extensions/ko.wikitext +++ b/docs/Extensions/ko.wikitext @@ -55,3 +55,6 @@ 여러분의 확장 기능을 빌드에서 살아남도록 일부러 만들 수도 있습니다. [[$1|확장 기능 만들기]]가 그에 대한 계약입니다. + + +Scribunto가 그중 하나이며, 대부분의 위키가 손을 뻗는 것입니다. [[$1|Lua 모듈]]에 별도 문서가 있는데, Lua 모듈은 설정이면서 콘텐츠이기도 하고 [[$2|단독 실행 바이너리]]가 그것을 렌더링할 수 없기 때문입니다. diff --git a/docs/JavaScript.wikitext b/docs/JavaScript.wikitext index c92d4be9e..ff854f631 100644 --- a/docs/JavaScript.wikitext +++ b/docs/JavaScript.wikitext @@ -38,4 +38,4 @@ Only gadgets marked default are loaded: a static site has no logged These docs ship one such gadget, PersistTabber, which remembers whether you pick the Docker or the binary tab in the install steps. Every other tabber on the page switches to the tab you picked, and the choice is still there when you move to another page. -{{prevnext|Extensions|Searching}} +{{prevnext|Lua modules|Searching}} diff --git a/docs/Lua modules.wikitext b/docs/Lua modules.wikitext new file mode 100644 index 000000000..152fb5d5e --- /dev/null +++ b/docs/Lua modules.wikitext @@ -0,0 +1,121 @@ +__TOC__ + + + + +A great many wikis keep their templates' logic in Lua, and {{SITENAME}} runs a real MediaWiki at build time, so a module runs the way it always does. The page that invokes it is written out with the module's answer already in it; the module itself never becomes a page of the published site. Lua on a MediaWiki is [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Scribunto Scribunto], which MediaWiki bundles. + + +{{note|The [[Special:MyLanguage/Standalone binary|standalone binary]] cannot render Lua, and only the Docker image can. A build that is asked to do the impossible now stops and says so, rather than publishing pages with {{#invoke:}} showing in them.}} + + +== Enabling it == + + +Name it in [[Special:MyLanguage/Configuration|.wikven.yaml]] and that is all. Scribunto ships inside the image, so it needs no [[Special:MyLanguage/Configuration#WikvenRepositories|WikvenRepositories]] entry, and the engine needs no configuration: the image compiles luasandbox into its PHP and Scribunto finds it. + + + +extensions: + - Scribunto + + + + +== Writing a module == + + +A module is content, so it lives in your source tree beside your pages. Its file carries no .wikitext marker, because a Module: page's content model is Lua and not wikitext: + + + +src/ + Module:Example + Home.wikitext + + + + +A file named Module:Example.wikitext is imported too, but the bare name is the convention, and it is the one {{SITENAME}} maps a page back to when it needs the file that page came from. + + +The module is ordinary Lua. This one is the module this site really has, in full: + + + +local p = {} + +function p.thisPage( frame ) + return '' .. mw.title.getCurrentTitle().text .. '' +end + +return p + + + + +and the page invokes it the ordinary way: + + + +{{#invoke:Example|thisPage}} + + + + +Which, run on this page as you read it, answered: + + +{{#invoke:Example|thisPage}} + + + +The invocation sits outside the tags, because a module's return value is not a translation unit. It reaches a reader of any language exactly as the module wrote it, so the words belong in the sentence around it and not in the module. + + +== What reaches the published site == + + +The module's output, and nothing else. Module: is not a content namespace, so the module is never exported as a page and its Lua source is not published; what a reader downloads is the page with that answer already in it. Nothing runs in the reader's browser, because nothing is left to run: the module ran once, at build time. + + +That is worth knowing when you decide what to put in a module. Anything a module computes from the wiki's own content — a table assembled from other pages, a sorted list, a page's own order in a series — is computed once and frozen. Anything a module would need at read time, like the reader's clock or a query, has nowhere to run. + + +== When the build and the site disagree == + + +Two mismatches used to pass quietly and produce a broken site. One of them ends the build; the other is only reported, and the bake goes on. + + +'''Modules with no Scribunto.''' A source tree with Module: files and no Scribunto in extensions is built, and told what those files come to: a {{#invoke:}} is left in the page as its own source text, and a module named with the .wikitext marker is published as a page — your Lua, exported for the world to read. The build says what it sees rather than deciding for you, because a Module: file is a guess read off a name and may be on its way in, on its way out, or kept for something else. + + + +Wikven: the source has 1 Lua module file(s) (Module:Example) and Scribunto is not in +extensions, so a {{#invoke:}} is left in the page as its own source text, and a module +named with the .wikitext marker is exported as a page. Add Scribunto to extensions if +those modules are meant to run. + + + + +'''Scribunto with no engine.''' A site that lists Scribunto is refused wherever no Lua engine can run it, which is the [[Special:MyLanguage/Standalone binary|standalone binary]]. The PHP it is built from carries no luasandbox, and Scribunto's other engine runs an external lua program that a single executable has no way to ship. This one is refused rather than reported: the site asked for Lua in so many words, and this build cannot give it. + + + +Wikven: this site lists Scribunto and no Lua engine is available here. The Docker image +has one; the standalone binary has none, so it cannot bake a site that uses Lua modules. +Bake this one with the image, or drop Scribunto from extensions and the Module: pages +with it. + + + + +So a site with Lua modules is a site built with the image. If you need the binary, keep the logic in [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Templates wikitext templates] and [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ParserFunctions ParserFunctions], both of which every product renders. + + +This documentation site uses a module itself, which is what keeps the two answers above honest: it is built with the image, and it could not be built with the binary. + + +{{prevnext|Extensions|JavaScript}} diff --git a/docs/Lua modules/ko.wikitext b/docs/Lua modules/ko.wikitext new file mode 100644 index 000000000..ccb2e6b5b --- /dev/null +++ b/docs/Lua modules/ko.wikitext @@ -0,0 +1,62 @@ + +Lua 모듈 + + +많은 위키가 틀의 논리를 Lua로 관리하며, {{SITENAME}}은 빌드 시점에 진짜 MediaWiki를 실행하므로 모듈도 늘 그렇듯 작동합니다. 모듈을 호출한 문서는 모듈의 답이 이미 담긴 상태로 기록되고, 모듈 자체는 게시된 사이트의 문서가 되지 않습니다. MediaWiki에서 Lua는 [$1 Scribunto]이며, MediaWiki에 함께 들어 있습니다. + + +{{note|[[$1|단독 실행 바이너리]]는 Lua를 렌더링할 수 없고, Docker 이미지만 할 수 있습니다. 불가능한 일을 요구받은 빌드는 이제 {{#invoke:}}가 그대로 보이는 문서를 게시하는 대신 중단하고 그 사실을 말합니다.}} + + +== 켜기 == + + +[[$1|.wikven.yaml]]에 이름을 적으면 그것으로 끝입니다. Scribunto는 이미지 안에 들어 있으므로 [[$2|WikvenRepositories]] 항목이 필요 없고, 엔진도 설정할 것이 없습니다. 이미지는 luasandbox를 자신의 PHP에 컴파일해 넣으며 Scribunto가 그것을 찾아냅니다. + + +== 모듈 작성하기 == + + +모듈은 콘텐츠이므로 다른 문서와 나란히 소스 트리에 놓입니다. 파일에는 .wikitext 표시를 붙이지 않는데, Module: 문서의 콘텐츠 모델은 위키텍스트가 아니라 Lua이기 때문입니다: + + +Module:Example.wikitext라는 이름도 가져오기는 하지만, 표시 없는 이름이 관례이며 {{SITENAME}}이 어떤 문서가 어느 파일에서 왔는지 되짚을 때 쓰는 이름도 그것입니다. + + +모듈은 평범한 Lua입니다. 아래는 이 사이트가 실제로 가진 모듈 전체입니다: + + +그리고 문서는 평범한 방식으로 호출합니다: + + +지금 읽고 있는 이 문서에서 실행된 결과는 이렇습니다: + + +호출문은 태그 밖에 있습니다. 모듈의 반환값은 번역 단위가 아니기 때문입니다. 어떤 언어의 독자에게든 모듈이 쓴 그대로 전달되므로, 말은 모듈이 아니라 그것을 감싼 문장이 담당해야 합니다. + + +== 게시된 사이트에 남는 것 == + + +모듈의 출력뿐입니다. Module:은 콘텐츠 이름공간이 아니므로 모듈은 결코 문서로 내보내지지 않고 Lua 소스도 게시되지 않습니다. 독자가 받는 것은 그 답이 이미 담긴 문서입니다. 독자의 브라우저에서는 아무것도 실행되지 않는데, 실행될 것이 남아 있지 않기 때문입니다. 모듈은 빌드 시점에 한 번 실행되었습니다. + + +모듈에 무엇을 담을지 정할 때 알아둘 만한 점입니다. 모듈이 위키 자체의 내용에서 계산해 내는 것, 다른 문서들에서 모은 표나 정렬된 목록이나 연속물 안에서 그 문서가 놓인 순서 같은 것은 한 번 계산되어 고정됩니다. 독자의 시계나 질의처럼 읽는 시점에 필요한 것은 실행될 곳이 없습니다. + + +== 빌드와 사이트가 어긋날 때 == + + +조용히 통과해 깨진 사이트를 만들던 불일치가 둘 있었습니다. 그중 하나는 빌드를 끝내고, 다른 하나는 알리기만 하며 베이크는 계속됩니다. + + +'''Scribunto 없는 모듈.''' Module: 파일이 있는데 extensionsScribunto가 없는 소스 트리도 빌드되며, 그 파일들이 무엇이 되는지를 알려줍니다. {{#invoke:}}는 문서에 자기 소스 텍스트로 남고, .wikitext 표시를 붙인 모듈은 문서로 게시됩니다. 여러분의 Lua가 세상에 읽히도록 내보내지는 것입니다. 빌드는 대신 결정하지 않고 본 것을 말하는데, Module: 파일이라는 판단은 이름에서 읽어낸 짐작이고 그 파일은 들어오는 중일 수도, 나가는 중일 수도, 다른 용도로 두고 있는 것일 수도 있기 때문입니다. + + +'''엔진 없는 Scribunto.''' Scribunto를 나열한 사이트는 그것을 실행할 Lua 엔진이 없는 곳에서 거부되며, 그곳이 [[$1|단독 실행 바이너리]]입니다. 그것이 빌드된 PHP에는 luasandbox가 없고, Scribunto의 다른 엔진은 외부 lua 프로그램을 실행하는데 단일 실행 파일은 그것을 담을 방법이 없습니다. 이쪽은 알리는 데 그치지 않고 거부되는데, 사이트가 Lua를 분명하게 요구했고 이 빌드는 그것을 줄 수 없기 때문입니다. + + +그래서 Lua 모듈이 있는 사이트는 이미지로 빌드하는 사이트입니다. 바이너리가 필요하다면 논리를 [$1 위키텍스트 틀]과 [$2 ParserFunctions]에 두십시오. 둘은 모든 제품이 렌더링합니다. + + +이 문서 사이트도 모듈을 하나 쓰며, 그것이 위의 두 답을 정직하게 유지합니다. 이 사이트는 이미지로 빌드되고, 바이너리로는 빌드할 수 없습니다. diff --git a/docs/MediaWiki:Sidebar-lua-modules.wikitext b/docs/MediaWiki:Sidebar-lua-modules.wikitext new file mode 100644 index 000000000..8cbbd2913 --- /dev/null +++ b/docs/MediaWiki:Sidebar-lua-modules.wikitext @@ -0,0 +1 @@ +Lua modules diff --git a/docs/MediaWiki:Sidebar-lua-modules/ko.wikitext b/docs/MediaWiki:Sidebar-lua-modules/ko.wikitext new file mode 100644 index 000000000..cdf84ee90 --- /dev/null +++ b/docs/MediaWiki:Sidebar-lua-modules/ko.wikitext @@ -0,0 +1 @@ +Lua 모듈 diff --git a/docs/MediaWiki:Sidebar.wikitext b/docs/MediaWiki:Sidebar.wikitext index 3462374d1..4b93171d8 100644 --- a/docs/MediaWiki:Sidebar.wikitext +++ b/docs/MediaWiki:Sidebar.wikitext @@ -13,6 +13,7 @@ ** Special:MyLanguage/Images|sidebar-images ** Special:MyLanguage/Skins|sidebar-skins ** Special:MyLanguage/Extensions|sidebar-extensions +** Special:MyLanguage/Lua modules|sidebar-lua-modules ** Special:MyLanguage/JavaScript|sidebar-javascript ** Special:MyLanguage/Searching|sidebar-searching ** Special:MyLanguage/Translating|sidebar-translating diff --git a/docs/Module:Example b/docs/Module:Example new file mode 100644 index 000000000..250bf6412 --- /dev/null +++ b/docs/Module:Example @@ -0,0 +1,22 @@ +-- The one Lua module this documentation site has, and it is here to be read rather than to do work: +-- Special:MyLanguage/Lua modules shows this source and invokes it on the same page, so the example a +-- reader copies is the code that ran, and the smoke test asserts the answer it produced. +-- +-- It answers with a page name and no prose, because a module's return value is not a translation unit +-- and would reach a reader of any language in whatever language it was written in. The sentence around +-- the invocation carries the words, and is translated like every other sentence on that page. +-- +-- What it returns also has to be the same on every bake, since a module is run once at build time and +-- its answer is baked into the page. A title satisfies that; a clock or a request would not, and the +-- reproducibility check would catch it. + +local p = {} + +--- The title of the page being rendered. +-- @param frame the invocation, unused: the question is about the page, not the arguments +-- @return the page's title, as wikitext +function p.thisPage( frame ) + return '' .. mw.title.getCurrentTitle().text .. '' +end + +return p diff --git a/docs/Standalone binary.wikitext b/docs/Standalone binary.wikitext index f3f286a24..333b8d098 100644 --- a/docs/Standalone binary.wikitext +++ b/docs/Standalone binary.wikitext @@ -108,7 +108,16 @@ WIKVEN_WORKDIR=/path/to/project wikven build -The output is the same as the Docker image's: the binary embeds MediaWiki, the bundled extensions and skins, and every interface language, and it reads the same [[Special:MyLanguage/Configuration|.wikven.yaml]] layered over {{SITENAME}}'s defaults. +The output is the same as the Docker image's, with one exception below: the binary embeds MediaWiki, the bundled extensions and skins, and every interface language, and it reads the same [[Special:MyLanguage/Configuration|.wikven.yaml]] layered over {{SITENAME}}'s defaults. + + +== What it cannot do == + + +'''[[Special:MyLanguage/Lua modules|Lua modules]].''' The PHP this binary is built from carries no luasandbox, and Scribunto's other engine runs an external lua program that a single executable has no way to ship. A site that lists Scribunto is refused here, with a message saying so, rather than baked into pages with {{#invoke:}} showing in them. Build that site with the image. + + +{{note|This documentation site is one of those: it has a Lua module of its own, so the image builds it and this binary cannot.}} == Optional host tools == diff --git a/docs/Standalone binary/ko.wikitext b/docs/Standalone binary/ko.wikitext index 08928abb3..709abf448 100644 --- a/docs/Standalone binary/ko.wikitext +++ b/docs/Standalone binary/ko.wikitext @@ -52,8 +52,17 @@ root 권한이 없다면, 대신 PATH에 있는 개인 디렉터리 렌더링된 사이트는 dist/에 기록됩니다. wikven serve로 미리 볼 수 있습니다 ([[$1|배포하기]] 참고). 작업 디렉터리는 기본값이 현재 디렉터리이며, 다른 곳의 프로젝트를 빌드하려면 WIKVEN_WORKDIR을 설정하십시오: - -출력물은 Docker 이미지의 것과 동일합니다. 바이너리는 MediaWiki, 번들된 확장 기능과 스킨, 그리고 모든 인터페이스 언어를 내장하며, {{SITENAME}}의 기본값 위에 겹쳐지는 동일한 [[$1|.wikven.yaml]]을 읽습니다. + +출력물은 아래 한 가지 예외를 빼면 Docker 이미지의 것과 동일합니다. 바이너리는 MediaWiki, 번들된 확장 기능과 스킨, 그리고 모든 인터페이스 언어를 내장하며, {{SITENAME}}의 기본값 위에 겹쳐지는 동일한 [[$1|.wikven.yaml]]을 읽습니다. + + +== 할 수 없는 것 == + + +'''[[$1|Lua 모듈]].''' 이 바이너리가 빌드된 PHP에는 luasandbox가 없고, Scribunto의 다른 엔진은 외부 lua 프로그램을 실행하는데 단일 실행 파일은 그것을 담을 방법이 없습니다. Scribunto를 나열한 사이트는 여기서 거부되며, {{#invoke:}}가 그대로 보이는 문서로 구워지는 대신 그 이유를 말합니다. 그런 사이트는 이미지로 빌드하십시오. + + +{{note|이 문서 사이트가 그중 하나입니다. 자체 Lua 모듈이 있으므로 이미지는 이 사이트를 빌드할 수 있고 이 바이너리는 할 수 없습니다.}} == 선택적 호스트 도구 == diff --git a/includes/Scribunto.php b/includes/Scribunto.php new file mode 100644 index 000000000..587a2e457 --- /dev/null +++ b/includes/Scribunto.php @@ -0,0 +1,122 @@ + 3) { + $shown .= ', ...'; + } + return ( + "Wikven: the source has $count Lua module file(s) ($shown) and " + . self::EXTENSION + . ' is not in extensions, so a {{#invoke:}} is left in the page as its own source text,' + . ' and a module named with the .wikitext marker is exported as a page. Add ' + . self::EXTENSION + . ' to extensions if those modules are meant to run.' + ); + } +} diff --git a/maintenance/build.php b/maintenance/build.php index c03273d66..fe3c7e331 100644 --- a/maintenance/build.php +++ b/maintenance/build.php @@ -49,6 +49,9 @@ public function execute() { $ip = $GLOBALS['IP']; $own = __DIR__; + // Before the output directory is emptied, because a site this build cannot render is better + // told so with its last bake still in place. + $this->checkLuaAgainstThisBuild(); $this->clearOutputDirectory(); $this->setMainPage(); $this->importImages("$ip/maintenance/importImages.php"); @@ -80,6 +83,76 @@ public function execute() { $this->renderSkinPasses(array_values($skins)); } + /** + * Say what this site's Lua and this build's Lua make of each other; see Scribunto for the ways + * they used to pass quietly and produce a site with braces in it. + * + * Only one of the two ends the build, and it is the one the site asked for: Scribunto listed where + * nothing can run it. The other is a remark about Module: files the site never asked to run. + */ + private function checkLuaAgainstThisBuild(): void { + $source = rtrim((string)( $GLOBALS['wgWikvenSourceDirectory'] ?? '' ), '/'); + $listed = ExtensionRegistry::getInstance()->isLoaded(Scribunto::EXTENSION); + + $problem = Scribunto::problem($listed, self::luaEngineAvailable()); + if ($problem !== null) { + $this->fatalError($problem); + } + + $modules = $source !== '' && is_dir($source) ? Scribunto::modulePages(self::sourcePaths($source)) : []; + $warning = Scribunto::warning($listed, $modules); + if ($warning !== null) { + $this->output("$warning\n"); + } + } + + /** + * Every file under the source directory, relative to it. + * + * Not ImportWikitext's list, which is filtered by SourceFile::isPageFile() -- and that asks + * MediaWiki for the title's content model, so with Scribunto absent a module file is not a page + * file and would be missing from exactly the case worth catching. + * + * @return string[] + */ + private static function sourcePaths(string $source): array { + $paths = []; + $entries = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($source, \FilesystemIterator::SKIP_DOTS) + ); + foreach ($entries as $entry) { + if ($entry->isFile()) { + $paths[] = substr($entry->getPathname(), strlen($source) + 1); + } + } + sort($paths); + return $paths; + } + + /** + * Whether anything here can run Lua. + * + * luasandbox is the engine the image carries. Scribunto's other engine runs an external lua, which + * the standalone binary has no way to ship but a hand-rolled install may well have. + */ + private static function luaEngineAvailable(): bool { + if (extension_loaded('luasandbox')) { + return true; + } + $configured = (string)( $GLOBALS['wgScribuntoEngineConf']['luastandalone']['luaPath'] ?? '' ); + if ($configured !== '' && is_executable($configured)) { + return true; + } + foreach (explode(PATH_SEPARATOR, (string)getenv('PATH')) as $dir) { + foreach (['lua5.1', 'lua'] as $name) { + if ($dir !== '' && is_executable("$dir/$name")) { + return true; + } + } + } + return false; + } + /** * Run the queue one type at a time, in name order: the runner otherwise shuffles the types. * diff --git a/tests/phpunit/unit/ScribuntoTest.php b/tests/phpunit/unit/ScribuntoTest.php new file mode 100644 index 000000000..a47839207 --- /dev/null +++ b/tests/phpunit/unit/ScribuntoTest.php @@ -0,0 +1,92 @@ +assertSame( + ['Module:Greet', 'Module:Order.wikitext'], + Scribunto::modulePages($paths), + 'both namings count, and nothing else does' + ); + } + + public function testAPageMerelyMentioningModulesIsNotOne() { + $paths = ['Modules of wikven.wikitext', 'docs/Module:Nested', 'Talk:Module:Greet.wikitext']; + $this->assertSame( + [], + Scribunto::modulePages($paths), + 'the prefix has to start the path, so a nested or prefixed name is somebody else' + ); + } + + public function testALocalisedPrefixCanBeAskedFor() { + $paths = ['모듈:Greet', 'Module:Greet']; + $this->assertSame(['모듈:Greet'], Scribunto::modulePages($paths, ['모듈:'])); + } + + public function testNothingIsWrongWhenTheEngineIsThere() { + $this->assertNull(Scribunto::problem(true, true)); + } + + public function testNothingIsWrongWithNoLuaAtAll() { + $this->assertNull(Scribunto::problem(false, false)); + $this->assertNull(Scribunto::problem(false, true)); + } + + public function testListingScribuntoWithNoEngineIsRefused() { + $problem = Scribunto::problem(true, false); + $this->assertNotNull($problem); + $this->assertStringContainsString('no Lua engine', $problem); + $this->assertStringContainsString('standalone binary', $problem, 'the message names the product'); + } + + /** + * The site asked for Lua and cannot have it either way, so the modules make no difference to the + * answer: this is the one case that ends the build. + */ + public function testTheEngineComplaintDoesNotDependOnTheModules() { + $this->assertNotNull(Scribunto::problem(true, false)); + $this->assertNull(Scribunto::warning(true, ['Module:Greet']), 'and it is not also warned about'); + } + + public function testModulesWithoutScribuntoAreWarnedAbout() { + $warning = Scribunto::warning(false, ['Module:Greet']); + $this->assertNotNull($warning); + $this->assertStringContainsString('1 Lua module file(s)', $warning); + $this->assertStringContainsString('Module:Greet', $warning, 'and says which'); + } + + /** + * The whole point of the change: a source tree with modules and no Scribunto still bakes. The + * warning says what those files come to; it does not decide for the site. + */ + public function testModulesWithoutScribuntoAreNotAProblem() { + $this->assertNull(Scribunto::problem(false, true)); + $this->assertNull(Scribunto::problem(false, false)); + } + + public function testItNamesAtMostThreeModules() { + $many = ['Module:A', 'Module:B', 'Module:C', 'Module:D']; + $warning = Scribunto::warning(false, $many); + $this->assertStringContainsString('4 Lua module file(s)', $warning); + $this->assertStringContainsString('Module:A, Module:B, Module:C, ...', $warning); + $this->assertStringNotContainsString('Module:D', $warning, 'the count carries the rest'); + } + + /** + * A site with no engine and no Scribunto is the ordinary case for the binary, and it must not be + * told anything: most sites have no Lua and the binary is the product they are told to use. + */ + public function testTheBinaryIsQuietForASiteWithoutLua() { + $this->assertNull(Scribunto::problem(false, false)); + $this->assertNull(Scribunto::warning(false, [])); + } +}