From 5155816b7b925dec5d5feb1568b1d7ceb00938b9 Mon Sep 17 00:00:00 2001 From: Mike Vink Date: Mon, 3 Feb 2025 21:29:42 +0100 Subject: fetch tarball --- .busted | 9 + .codecov.yml | 6 + .editorconfig | 17 + .luacheckrc | 36 + CHANGELOG.md | 694 +++++++ CODE_OF_CONDUCT.md | 46 + COPYING | 20 + GNUmakefile | 187 ++ Makefile | 7 + README.md | 33 + SECURITY.md | 21 + binary/Makefile.windows | 76 + binary/all_in_one | 503 +++++ binary/lua-bz2-0.2.1-1.rockspec | 44 + binary/lua-zlib-1.2-0.rockspec | 39 + binary/luaposix-35.1-1.rockspec | 61 + binary/luasec-1.3.2-1.rockspec | 98 + binary/luasocket-3.1.0-1.rockspec | 135 ++ binary/static-gcc | 170 ++ config.ld | 6 + configure | 513 +++++ luarocks-3.11.1-1.rockspec | 38 + mergerelease | 33 + publishrelease | 213 ++ smoke_test.sh | 111 ++ spec/README.md | 58 + spec/add_spec.lua | 41 + spec/build_spec.lua | 456 +++++ spec/cmd_spec.lua | 105 + spec/config_spec.lua | 253 +++ spec/deps_spec.lua | 114 ++ spec/doc_spec.lua | 154 ++ spec/download_spec.lua | 55 + spec/dummy_spec.lua | 24 + spec/external_spec.lua | 32 + spec/fixtures/a_repo/a_build_dep-1.0-1.rockspec | 18 + spec/fixtures/a_repo/a_build_dep-1.0-1.src.rock | Bin 0 -> 575 bytes spec/fixtures/a_repo/a_rock-1.0-1.rockspec | 18 + spec/fixtures/a_repo/a_rock-1.0-1.src.rock | Bin 0 -> 446 bytes spec/fixtures/a_repo/a_rock-2.0-1.src.rock | Bin 0 -> 541 bytes spec/fixtures/a_repo/busted_project-0.1-1.rockspec | 19 + spec/fixtures/a_repo/busted_project-0.1-1.src.rock | Bin 0 -> 937 bytes .../has_another_namespaced_dep-1.0-1.rockspec | 19 + .../has_another_namespaced_dep-1.0-1.src.rock | Bin 0 -> 603 bytes spec/fixtures/a_repo/has_build_dep-1.0-1.all.rock | Bin 0 -> 1010 bytes spec/fixtures/a_repo/has_build_dep-1.0-1.rockspec | 22 + spec/fixtures/a_repo/has_build_dep-1.0-1.src.rock | Bin 0 -> 584 bytes .../a_repo/has_namespaced_dep-1.0-1.rockspec | 19 + .../a_repo/has_namespaced_dep-1.0-1.src.rock | Bin 0 -> 580 bytes spec/fixtures/a_repo/manifest | 90 + spec/fixtures/a_repo/manifest-5.1 | 90 + spec/fixtures/a_repo/manifest-5.1.zip | Bin 0 -> 343 bytes spec/fixtures/a_repo/manifest-5.2 | 90 + spec/fixtures/a_repo/manifest-5.2.zip | Bin 0 -> 343 bytes spec/fixtures/a_repo/manifest-5.3 | 90 + spec/fixtures/a_repo/manifest-5.3.zip | Bin 0 -> 343 bytes spec/fixtures/a_repo/manifest-5.4 | 90 + spec/fixtures/a_repo/manifest-5.4.zip | Bin 0 -> 343 bytes .../a_repo/manifests/a_user/a_rock-2.0-1.rockspec | 17 + .../a_repo/manifests/a_user/a_rock-2.0-1.src.rock | Bin 0 -> 446 bytes spec/fixtures/a_repo/manifests/a_user/manifest | 14 + spec/fixtures/a_repo/manifests/a_user/manifest-5.1 | 14 + spec/fixtures/a_repo/manifests/a_user/manifest-5.2 | 14 + spec/fixtures/a_repo/manifests/a_user/manifest-5.3 | 14 + spec/fixtures/a_repo/manifests/a_user/manifest-5.4 | 14 + .../manifests/another_user/a_rock-3.0-1.rockspec | 17 + .../manifests/another_user/a_rock-3.0-1.src.rock | Bin 0 -> 449 bytes .../a_repo/manifests/another_user/manifest | 14 + .../a_repo/manifests/another_user/manifest-5.1 | 14 + .../a_repo/manifests/another_user/manifest-5.2 | 14 + .../a_repo/manifests/another_user/manifest-5.3 | 14 + .../a_repo/manifests/another_user/manifest-5.4 | 14 + spec/fixtures/a_repo/non_lua_file-1.0-1.rockspec | 22 + spec/fixtures/a_repo/non_lua_file-1.0-1.src.rock | Bin 0 -> 852 bytes spec/fixtures/a_repo/non_lua_file-1.0-2.rockspec | 22 + spec/fixtures/a_repo/non_lua_file-1.0-2.src.rock | Bin 0 -> 852 bytes spec/fixtures/a_rock-1.0-1.rockspec | 17 + spec/fixtures/a_rock-1.0-1.src.rock | Bin 0 -> 446 bytes spec/fixtures/a_rock.lua | 1 + spec/fixtures/abc.bz2 | Bin 0 -> 66 bytes spec/fixtures/an_upstream_tarball-0.1.tar.gz | Bin 0 -> 260 bytes spec/fixtures/bad_pack-0.1-1.rockspec | 19 + spec/fixtures/build_only_deps-0.1-1.rockspec | 18 + spec/fixtures/build_only_deps-0.1-1.src.rock | Bin 0 -> 547 bytes spec/fixtures/busted_project-0.1-1.rockspec | 19 + spec/fixtures/busted_project-0.1.tar.gz | Bin 0 -> 345 bytes spec/fixtures/busted_project/spec/sum_spec.lua | 10 + spec/fixtures/busted_project/sum.lua | 7 + spec/fixtures/double_deploy_type/ddt.c | 6 + spec/fixtures/double_deploy_type/ddt1.lua | 1 + spec/fixtures/double_deploy_type/ddt2.lua | 1 + spec/fixtures/double_deploy_type/ddt_file | 1 + .../double_deploy_type-0.1.0-1.rockspec | 22 + .../double_deploy_type-0.2.0-1.rockspec | 22 + spec/fixtures/fixturedep.c | 4 + spec/fixtures/git_repo/LICENSE | 19 + spec/fixtures/git_repo/README.md | 3 + .../5D2D3F97B88B18604D819EA9DF5B730C75D71B60.key | Bin 0 -> 977 bytes .../B71C36B4EDEB72A047FED1C01BCFF4D08837E3B1.key | Bin 0 -> 978 bytes spec/fixtures/gpg/pubring.kbx | Bin 0 -> 1494 bytes spec/fixtures/gpg/trustdb.gpg | Bin 0 -> 1240 bytes spec/fixtures/invalid_patch-0.1-1.rockspec | 29 + spec/fixtures/invalid_say-1.3-1.rockspec | 23 + spec/fixtures/legacyexternalcommand-0.1-1.rockspec | 17 + spec/fixtures/legacyexternalcommand.lua | 34 + spec/fixtures/luajit-fail-1.0-1.rockspec | 22 + spec/fixtures/luajit-success-1.0-1.rockspec | 23 + spec/fixtures/mixed_deploy_type/mdt.c | 6 + spec/fixtures/mixed_deploy_type/mdt.lua | 1 + spec/fixtures/mixed_deploy_type/mdt_file | 1 + .../mixed_deploy_type-0.1.0-1.rockspec | 21 + .../mixed_deploy_type-0.2.0-1.rockspec | 21 + spec/fixtures/patch_create_delete-0.1-1.rockspec | 34 + spec/fixtures/patch_create_delete/a_file.txt | 1 + spec/fixtures/renamed_upstream_tarball-0.1.tar.gz | Bin 0 -> 260 bytes spec/fixtures/with_external_dep-0.1-1.rockspec | 25 + spec/fixtures/with_external_dep.c | 10 + spec/fixtures/with_external_dep/foo/foo.h | 1 + spec/fs_spec.lua | 76 + spec/help_spec.lua | 25 + spec/init_spec.lua | 235 +++ spec/install_spec.lua | 235 +++ spec/lint_spec.lua | 109 + spec/list_spec.lua | 34 + spec/loader_spec.lua | 73 + spec/make_spec.lua | 387 ++++ spec/pack_spec.lua | 102 + spec/path_spec.lua | 58 + spec/quick/admin_make_manifest.q | 46 + spec/quick/build.q | 405 ++++ spec/quick/cmd.q | 36 + spec/quick/config.q | 97 + spec/quick/doc.q | 13 + spec/quick/install.q | 688 +++++++ spec/quick/list.q | 45 + spec/quick/make.q | 88 + spec/quick/new_version.q | 213 ++ spec/quick/pack.q | 128 ++ spec/quick/path.q | 22 + spec/quick/purge.q | 103 + spec/quick/test.q | 51 + spec/quick_spec.lua | 22 + spec/refresh_cache_spec.lua | 13 + spec/remove_spec.lua | 129 ++ spec/search_spec.lua | 33 + spec/show_spec.lua | 105 + spec/test_spec.lua | 93 + spec/unit/build_spec.lua | 365 ++++ spec/unit/deps_spec.lua | 143 ++ spec/unit/dir_spec.lua | 70 + spec/unit/fetch_spec.lua | 486 +++++ spec/unit/fs_spec.lua | 1595 +++++++++++++++ spec/unit/fun_spec.lua | 128 ++ spec/unit/loader_spec.lua | 18 + spec/unit/persist_spec.lua | 74 + spec/unit/rockspecs_spec.lua | 126 ++ spec/unit/sysdetect_spec.lua | 79 + spec/unit/test_spec.lua | 173 ++ spec/unit/tools_spec.lua | 251 +++ spec/unit/util_spec.lua | 160 ++ spec/unpack_spec.lua | 69 + spec/upload_spec.lua | 63 + spec/util/git_repo.lua | 108 + spec/util/mock-server.lua | 98 + spec/util/quick.lua | 472 +++++ spec/util/test_env.lua | 1187 +++++++++++ spec/util/versions.lua | 17 + spec/util_spec.lua | 55 + spec/which_spec.lua | 39 + spec/write_rockspec_spec.lua | 104 + src/bin/luarocks | 35 + src/bin/luarocks-admin | 18 + src/luarocks/admin/cache.lua | 88 + src/luarocks/admin/cmd/add.lua | 134 ++ src/luarocks/admin/cmd/make_manifest.lua | 50 + src/luarocks/admin/cmd/refresh_cache.lua | 31 + src/luarocks/admin/cmd/remove.lua | 95 + src/luarocks/admin/index.lua | 185 ++ src/luarocks/build.lua | 495 +++++ src/luarocks/build/builtin.lua | 395 ++++ src/luarocks/build/cmake.lua | 78 + src/luarocks/build/command.lua | 41 + src/luarocks/build/make.lua | 98 + src/luarocks/cmd.lua | 781 ++++++++ src/luarocks/cmd/build.lua | 198 ++ src/luarocks/cmd/config.lua | 392 ++++ src/luarocks/cmd/doc.lua | 153 ++ src/luarocks/cmd/download.lua | 51 + src/luarocks/cmd/init.lua | 219 ++ src/luarocks/cmd/install.lua | 271 +++ src/luarocks/cmd/lint.lua | 50 + src/luarocks/cmd/list.lua | 96 + src/luarocks/cmd/make.lua | 163 ++ src/luarocks/cmd/new_version.lua | 228 +++ src/luarocks/cmd/pack.lua | 36 + src/luarocks/cmd/path.lua | 83 + src/luarocks/cmd/purge.lua | 73 + src/luarocks/cmd/remove.lua | 72 + src/luarocks/cmd/search.lua | 84 + src/luarocks/cmd/show.lua | 314 +++ src/luarocks/cmd/test.lua | 48 + src/luarocks/cmd/unpack.lua | 169 ++ src/luarocks/cmd/upload.lua | 128 ++ src/luarocks/cmd/which.lua | 40 + src/luarocks/cmd/write_rockspec.lua | 408 ++++ src/luarocks/config.lua | 36 + src/luarocks/core/cfg.lua | 940 +++++++++ src/luarocks/core/dir.lua | 98 + src/luarocks/core/manif.lua | 114 ++ src/luarocks/core/path.lua | 157 ++ src/luarocks/core/persist.lua | 81 + src/luarocks/core/sysdetect.lua | 419 ++++ src/luarocks/core/util.lua | 322 +++ src/luarocks/core/vers.lua | 207 ++ src/luarocks/deplocks.lua | 106 + src/luarocks/deps.lua | 831 ++++++++ src/luarocks/dir.lua | 63 + src/luarocks/download.lua | 68 + src/luarocks/fetch.lua | 610 ++++++ src/luarocks/fetch/cvs.lua | 55 + src/luarocks/fetch/git.lua | 165 ++ src/luarocks/fetch/git_file.lua | 19 + src/luarocks/fetch/git_http.lua | 26 + src/luarocks/fetch/git_https.lua | 7 + src/luarocks/fetch/git_ssh.lua | 32 + src/luarocks/fetch/hg.lua | 65 + src/luarocks/fetch/hg_http.lua | 24 + src/luarocks/fetch/hg_https.lua | 8 + src/luarocks/fetch/hg_ssh.lua | 8 + src/luarocks/fetch/sscm.lua | 44 + src/luarocks/fetch/svn.lua | 64 + src/luarocks/fs.lua | 148 ++ src/luarocks/fs/linux.lua | 50 + src/luarocks/fs/lua.lua | 1307 ++++++++++++ src/luarocks/fs/macosx.lua | 50 + src/luarocks/fs/tools.lua | 222 +++ src/luarocks/fs/unix.lua | 266 +++ src/luarocks/fs/unix/tools.lua | 353 ++++ src/luarocks/fs/win32.lua | 384 ++++ src/luarocks/fs/win32/tools.lua | 330 +++ src/luarocks/fun.lua | 143 ++ src/luarocks/loader.lua | 269 +++ src/luarocks/manif.lua | 225 +++ src/luarocks/manif/writer.lua | 498 +++++ src/luarocks/pack.lua | 184 ++ src/luarocks/path.lua | 263 +++ src/luarocks/persist.lua | 259 +++ src/luarocks/queries.lua | 217 ++ src/luarocks/remove.lua | 135 ++ src/luarocks/repos.lua | 697 +++++++ src/luarocks/require.lua | 2 + src/luarocks/results.lua | 62 + src/luarocks/rockspecs.lua | 183 ++ src/luarocks/search.lua | 393 ++++ src/luarocks/signing.lua | 48 + src/luarocks/test.lua | 100 + src/luarocks/test/busted.lua | 53 + src/luarocks/test/command.lua | 52 + src/luarocks/tools/patch.lua | 716 +++++++ src/luarocks/tools/tar.lua | 191 ++ src/luarocks/tools/zip.lua | 531 +++++ src/luarocks/type/manifest.lua | 80 + src/luarocks/type/rockspec.lua | 199 ++ src/luarocks/type_check.lua | 213 ++ src/luarocks/upload/api.lua | 265 +++ src/luarocks/upload/multipart.lua | 109 + src/luarocks/util.lua | 634 ++++++ src/luarocks/vendor/argparse.lua | 2103 ++++++++++++++++++++ src/luarocks/vendor/dkjson.lua | 749 +++++++ test_regression.sh | 86 + 270 files changed, 38312 insertions(+) create mode 100644 .busted create mode 100644 .codecov.yml create mode 100644 .editorconfig create mode 100644 .luacheckrc create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 COPYING create mode 100644 GNUmakefile create mode 100644 Makefile create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 binary/Makefile.windows create mode 100755 binary/all_in_one create mode 100644 binary/lua-bz2-0.2.1-1.rockspec create mode 100644 binary/lua-zlib-1.2-0.rockspec create mode 100644 binary/luaposix-35.1-1.rockspec create mode 100644 binary/luasec-1.3.2-1.rockspec create mode 100644 binary/luasocket-3.1.0-1.rockspec create mode 100755 binary/static-gcc create mode 100644 config.ld create mode 100755 configure create mode 100644 luarocks-3.11.1-1.rockspec create mode 100755 mergerelease create mode 100755 publishrelease create mode 100755 smoke_test.sh create mode 100644 spec/README.md create mode 100644 spec/add_spec.lua create mode 100644 spec/build_spec.lua create mode 100644 spec/cmd_spec.lua create mode 100644 spec/config_spec.lua create mode 100644 spec/deps_spec.lua create mode 100644 spec/doc_spec.lua create mode 100644 spec/download_spec.lua create mode 100644 spec/dummy_spec.lua create mode 100644 spec/external_spec.lua create mode 100644 spec/fixtures/a_repo/a_build_dep-1.0-1.rockspec create mode 100644 spec/fixtures/a_repo/a_build_dep-1.0-1.src.rock create mode 100644 spec/fixtures/a_repo/a_rock-1.0-1.rockspec create mode 100644 spec/fixtures/a_repo/a_rock-1.0-1.src.rock create mode 100644 spec/fixtures/a_repo/a_rock-2.0-1.src.rock create mode 100644 spec/fixtures/a_repo/busted_project-0.1-1.rockspec create mode 100644 spec/fixtures/a_repo/busted_project-0.1-1.src.rock create mode 100644 spec/fixtures/a_repo/has_another_namespaced_dep-1.0-1.rockspec create mode 100644 spec/fixtures/a_repo/has_another_namespaced_dep-1.0-1.src.rock create mode 100644 spec/fixtures/a_repo/has_build_dep-1.0-1.all.rock create mode 100644 spec/fixtures/a_repo/has_build_dep-1.0-1.rockspec create mode 100644 spec/fixtures/a_repo/has_build_dep-1.0-1.src.rock create mode 100644 spec/fixtures/a_repo/has_namespaced_dep-1.0-1.rockspec create mode 100644 spec/fixtures/a_repo/has_namespaced_dep-1.0-1.src.rock create mode 100644 spec/fixtures/a_repo/manifest create mode 100644 spec/fixtures/a_repo/manifest-5.1 create mode 100644 spec/fixtures/a_repo/manifest-5.1.zip create mode 100644 spec/fixtures/a_repo/manifest-5.2 create mode 100644 spec/fixtures/a_repo/manifest-5.2.zip create mode 100644 spec/fixtures/a_repo/manifest-5.3 create mode 100644 spec/fixtures/a_repo/manifest-5.3.zip create mode 100644 spec/fixtures/a_repo/manifest-5.4 create mode 100644 spec/fixtures/a_repo/manifest-5.4.zip create mode 100644 spec/fixtures/a_repo/manifests/a_user/a_rock-2.0-1.rockspec create mode 100644 spec/fixtures/a_repo/manifests/a_user/a_rock-2.0-1.src.rock create mode 100644 spec/fixtures/a_repo/manifests/a_user/manifest create mode 100644 spec/fixtures/a_repo/manifests/a_user/manifest-5.1 create mode 100644 spec/fixtures/a_repo/manifests/a_user/manifest-5.2 create mode 100644 spec/fixtures/a_repo/manifests/a_user/manifest-5.3 create mode 100644 spec/fixtures/a_repo/manifests/a_user/manifest-5.4 create mode 100644 spec/fixtures/a_repo/manifests/another_user/a_rock-3.0-1.rockspec create mode 100644 spec/fixtures/a_repo/manifests/another_user/a_rock-3.0-1.src.rock create mode 100644 spec/fixtures/a_repo/manifests/another_user/manifest create mode 100644 spec/fixtures/a_repo/manifests/another_user/manifest-5.1 create mode 100644 spec/fixtures/a_repo/manifests/another_user/manifest-5.2 create mode 100644 spec/fixtures/a_repo/manifests/another_user/manifest-5.3 create mode 100644 spec/fixtures/a_repo/manifests/another_user/manifest-5.4 create mode 100644 spec/fixtures/a_repo/non_lua_file-1.0-1.rockspec create mode 100644 spec/fixtures/a_repo/non_lua_file-1.0-1.src.rock create mode 100644 spec/fixtures/a_repo/non_lua_file-1.0-2.rockspec create mode 100644 spec/fixtures/a_repo/non_lua_file-1.0-2.src.rock create mode 100644 spec/fixtures/a_rock-1.0-1.rockspec create mode 100644 spec/fixtures/a_rock-1.0-1.src.rock create mode 100644 spec/fixtures/a_rock.lua create mode 100644 spec/fixtures/abc.bz2 create mode 100644 spec/fixtures/an_upstream_tarball-0.1.tar.gz create mode 100644 spec/fixtures/bad_pack-0.1-1.rockspec create mode 100644 spec/fixtures/build_only_deps-0.1-1.rockspec create mode 100644 spec/fixtures/build_only_deps-0.1-1.src.rock create mode 100644 spec/fixtures/busted_project-0.1-1.rockspec create mode 100644 spec/fixtures/busted_project-0.1.tar.gz create mode 100644 spec/fixtures/busted_project/spec/sum_spec.lua create mode 100644 spec/fixtures/busted_project/sum.lua create mode 100644 spec/fixtures/double_deploy_type/ddt.c create mode 100644 spec/fixtures/double_deploy_type/ddt1.lua create mode 100644 spec/fixtures/double_deploy_type/ddt2.lua create mode 100644 spec/fixtures/double_deploy_type/ddt_file create mode 100644 spec/fixtures/double_deploy_type/double_deploy_type-0.1.0-1.rockspec create mode 100644 spec/fixtures/double_deploy_type/double_deploy_type-0.2.0-1.rockspec create mode 100644 spec/fixtures/fixturedep.c create mode 100644 spec/fixtures/git_repo/LICENSE create mode 100644 spec/fixtures/git_repo/README.md create mode 100644 spec/fixtures/gpg/private-keys-v1.d/5D2D3F97B88B18604D819EA9DF5B730C75D71B60.key create mode 100644 spec/fixtures/gpg/private-keys-v1.d/B71C36B4EDEB72A047FED1C01BCFF4D08837E3B1.key create mode 100644 spec/fixtures/gpg/pubring.kbx create mode 100644 spec/fixtures/gpg/trustdb.gpg create mode 100644 spec/fixtures/invalid_patch-0.1-1.rockspec create mode 100644 spec/fixtures/invalid_say-1.3-1.rockspec create mode 100644 spec/fixtures/legacyexternalcommand-0.1-1.rockspec create mode 100644 spec/fixtures/legacyexternalcommand.lua create mode 100644 spec/fixtures/luajit-fail-1.0-1.rockspec create mode 100644 spec/fixtures/luajit-success-1.0-1.rockspec create mode 100644 spec/fixtures/mixed_deploy_type/mdt.c create mode 100644 spec/fixtures/mixed_deploy_type/mdt.lua create mode 100644 spec/fixtures/mixed_deploy_type/mdt_file create mode 100644 spec/fixtures/mixed_deploy_type/mixed_deploy_type-0.1.0-1.rockspec create mode 100644 spec/fixtures/mixed_deploy_type/mixed_deploy_type-0.2.0-1.rockspec create mode 100644 spec/fixtures/patch_create_delete-0.1-1.rockspec create mode 100644 spec/fixtures/patch_create_delete/a_file.txt create mode 100644 spec/fixtures/renamed_upstream_tarball-0.1.tar.gz create mode 100644 spec/fixtures/with_external_dep-0.1-1.rockspec create mode 100644 spec/fixtures/with_external_dep.c create mode 100644 spec/fixtures/with_external_dep/foo/foo.h create mode 100644 spec/fs_spec.lua create mode 100644 spec/help_spec.lua create mode 100644 spec/init_spec.lua create mode 100644 spec/install_spec.lua create mode 100644 spec/lint_spec.lua create mode 100644 spec/list_spec.lua create mode 100644 spec/loader_spec.lua create mode 100644 spec/make_spec.lua create mode 100644 spec/pack_spec.lua create mode 100644 spec/path_spec.lua create mode 100644 spec/quick/admin_make_manifest.q create mode 100644 spec/quick/build.q create mode 100644 spec/quick/cmd.q create mode 100644 spec/quick/config.q create mode 100644 spec/quick/doc.q create mode 100644 spec/quick/install.q create mode 100644 spec/quick/list.q create mode 100644 spec/quick/make.q create mode 100644 spec/quick/new_version.q create mode 100644 spec/quick/pack.q create mode 100644 spec/quick/path.q create mode 100644 spec/quick/purge.q create mode 100644 spec/quick/test.q create mode 100644 spec/quick_spec.lua create mode 100644 spec/refresh_cache_spec.lua create mode 100644 spec/remove_spec.lua create mode 100644 spec/search_spec.lua create mode 100644 spec/show_spec.lua create mode 100644 spec/test_spec.lua create mode 100644 spec/unit/build_spec.lua create mode 100644 spec/unit/deps_spec.lua create mode 100644 spec/unit/dir_spec.lua create mode 100644 spec/unit/fetch_spec.lua create mode 100644 spec/unit/fs_spec.lua create mode 100644 spec/unit/fun_spec.lua create mode 100644 spec/unit/loader_spec.lua create mode 100644 spec/unit/persist_spec.lua create mode 100644 spec/unit/rockspecs_spec.lua create mode 100644 spec/unit/sysdetect_spec.lua create mode 100644 spec/unit/test_spec.lua create mode 100644 spec/unit/tools_spec.lua create mode 100644 spec/unit/util_spec.lua create mode 100644 spec/unpack_spec.lua create mode 100644 spec/upload_spec.lua create mode 100644 spec/util/git_repo.lua create mode 100644 spec/util/mock-server.lua create mode 100644 spec/util/quick.lua create mode 100644 spec/util/test_env.lua create mode 100644 spec/util/versions.lua create mode 100644 spec/util_spec.lua create mode 100644 spec/which_spec.lua create mode 100644 spec/write_rockspec_spec.lua create mode 100755 src/bin/luarocks create mode 100755 src/bin/luarocks-admin create mode 100644 src/luarocks/admin/cache.lua create mode 100644 src/luarocks/admin/cmd/add.lua create mode 100644 src/luarocks/admin/cmd/make_manifest.lua create mode 100644 src/luarocks/admin/cmd/refresh_cache.lua create mode 100644 src/luarocks/admin/cmd/remove.lua create mode 100644 src/luarocks/admin/index.lua create mode 100644 src/luarocks/build.lua create mode 100644 src/luarocks/build/builtin.lua create mode 100644 src/luarocks/build/cmake.lua create mode 100644 src/luarocks/build/command.lua create mode 100644 src/luarocks/build/make.lua create mode 100644 src/luarocks/cmd.lua create mode 100644 src/luarocks/cmd/build.lua create mode 100644 src/luarocks/cmd/config.lua create mode 100644 src/luarocks/cmd/doc.lua create mode 100644 src/luarocks/cmd/download.lua create mode 100644 src/luarocks/cmd/init.lua create mode 100644 src/luarocks/cmd/install.lua create mode 100644 src/luarocks/cmd/lint.lua create mode 100644 src/luarocks/cmd/list.lua create mode 100644 src/luarocks/cmd/make.lua create mode 100644 src/luarocks/cmd/new_version.lua create mode 100644 src/luarocks/cmd/pack.lua create mode 100644 src/luarocks/cmd/path.lua create mode 100644 src/luarocks/cmd/purge.lua create mode 100644 src/luarocks/cmd/remove.lua create mode 100644 src/luarocks/cmd/search.lua create mode 100644 src/luarocks/cmd/show.lua create mode 100644 src/luarocks/cmd/test.lua create mode 100644 src/luarocks/cmd/unpack.lua create mode 100644 src/luarocks/cmd/upload.lua create mode 100644 src/luarocks/cmd/which.lua create mode 100644 src/luarocks/cmd/write_rockspec.lua create mode 100644 src/luarocks/config.lua create mode 100644 src/luarocks/core/cfg.lua create mode 100644 src/luarocks/core/dir.lua create mode 100644 src/luarocks/core/manif.lua create mode 100644 src/luarocks/core/path.lua create mode 100644 src/luarocks/core/persist.lua create mode 100644 src/luarocks/core/sysdetect.lua create mode 100644 src/luarocks/core/util.lua create mode 100644 src/luarocks/core/vers.lua create mode 100644 src/luarocks/deplocks.lua create mode 100644 src/luarocks/deps.lua create mode 100644 src/luarocks/dir.lua create mode 100644 src/luarocks/download.lua create mode 100644 src/luarocks/fetch.lua create mode 100644 src/luarocks/fetch/cvs.lua create mode 100644 src/luarocks/fetch/git.lua create mode 100644 src/luarocks/fetch/git_file.lua create mode 100644 src/luarocks/fetch/git_http.lua create mode 100644 src/luarocks/fetch/git_https.lua create mode 100644 src/luarocks/fetch/git_ssh.lua create mode 100644 src/luarocks/fetch/hg.lua create mode 100644 src/luarocks/fetch/hg_http.lua create mode 100644 src/luarocks/fetch/hg_https.lua create mode 100644 src/luarocks/fetch/hg_ssh.lua create mode 100644 src/luarocks/fetch/sscm.lua create mode 100644 src/luarocks/fetch/svn.lua create mode 100644 src/luarocks/fs.lua create mode 100644 src/luarocks/fs/linux.lua create mode 100644 src/luarocks/fs/lua.lua create mode 100644 src/luarocks/fs/macosx.lua create mode 100644 src/luarocks/fs/tools.lua create mode 100644 src/luarocks/fs/unix.lua create mode 100644 src/luarocks/fs/unix/tools.lua create mode 100644 src/luarocks/fs/win32.lua create mode 100644 src/luarocks/fs/win32/tools.lua create mode 100644 src/luarocks/fun.lua create mode 100644 src/luarocks/loader.lua create mode 100644 src/luarocks/manif.lua create mode 100644 src/luarocks/manif/writer.lua create mode 100644 src/luarocks/pack.lua create mode 100644 src/luarocks/path.lua create mode 100644 src/luarocks/persist.lua create mode 100644 src/luarocks/queries.lua create mode 100644 src/luarocks/remove.lua create mode 100644 src/luarocks/repos.lua create mode 100644 src/luarocks/require.lua create mode 100644 src/luarocks/results.lua create mode 100644 src/luarocks/rockspecs.lua create mode 100644 src/luarocks/search.lua create mode 100644 src/luarocks/signing.lua create mode 100644 src/luarocks/test.lua create mode 100644 src/luarocks/test/busted.lua create mode 100644 src/luarocks/test/command.lua create mode 100644 src/luarocks/tools/patch.lua create mode 100644 src/luarocks/tools/tar.lua create mode 100644 src/luarocks/tools/zip.lua create mode 100644 src/luarocks/type/manifest.lua create mode 100644 src/luarocks/type/rockspec.lua create mode 100644 src/luarocks/type_check.lua create mode 100644 src/luarocks/upload/api.lua create mode 100644 src/luarocks/upload/multipart.lua create mode 100644 src/luarocks/util.lua create mode 100644 src/luarocks/vendor/argparse.lua create mode 100644 src/luarocks/vendor/dkjson.lua create mode 100755 test_regression.sh diff --git a/.busted b/.busted new file mode 100644 index 0000000..a8d40be --- /dev/null +++ b/.busted @@ -0,0 +1,9 @@ +return { + default = { + output = "htest", + verbose = true, + ["exclude-pattern"] = "sum_spec", -- do not run spec files inside fixture + helper = "spec.util.test_env", + ["auto-insulate"] = false + } +} diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..f3b7139 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,6 @@ +coverage: + notify: + gitter: + default: + url: "https://webhooks.gitter.im/e/c8ee7e8a380f711aea39" + threshold: 1% diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dc5519a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +charset = utf-8 + +[*.lua] +indent_style = space +indent_size = 3 + +[Makefile] +indent_style = tab + +[*.bat] +end_of_line = crlf \ No newline at end of file diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..cf70fdf --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,36 @@ +codes = true + +ignore = { + "6..", + "542", + "212", + "213", + "421/ok", + "421/err", + "421/errcode", + "411/ok", + "411/err", + "411/errcode", + "113/unpack", + "211/require", + "211/ok", + "211/err", + "211/errcode", + "431/ok", + "431/err", + "431/errcode", + "311/ok", + "311/err", + "311/errcode", + "143/table.unpack", +} + +exclude_files = { + "src/luarocks/vendor/**/*.lua", +} + +include_files = { + "src/luarocks/**/*.lua" +} + +unused_secondaries = false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e8c5394 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,694 @@ +## What's new in LuaRocks 3.11.0 + +* Features: + * `luarocks build` and `luarocks install` no longer rebuild + or reinstall if the version is already installed + (`--force` overrides). + * More aggressive caching of the manifest file (does not + hit `luarocks.org` again if the cached manifest is younger + than 10 seconds). + * Drops stale lock files (older than 1 hour). + * More informative error reports on bad configurations of + Lua paths (`LUA_INCDIR`, `LUA_LIBDIR`). + * Better error messages when lacking permissions. + * Bumps vendored dkjson dependency to 2.7. + * `--verbose` output now prints the LuaRocks configuration, + for more informative bug reports. +* Fixes: + * Passing `--global` always LuaRocks target the system tree. + * Does not crash if `root_dir` is a table. + * Does not try to lock rocks trees when using `--pack-binary-rock` + or `--no-install`. + * Checks permissions ahead of trying to lock trees, + to provide better error messages. + * Avoids LuaSec version mismatch by refusing to use LuaSec + versions below 1.1. + * Does not set up a "project environment" when running + `make` on the LuaRocks sources. + * Windows: + * Avoid excessive calls to `icacls`, resulting in + performance improvements. + * Parses slashes correctly when reading a rock's `rock_manifest`. + * Fix setting of environment variables. + * install.bat sets LUALIB. + * Improved help for `luarocks path`. + +## What's new in LuaRocks 3.10.0 + +* Features: + * Introduce file-based locking for concurrent access + control. Previously, LuaRocks would produce undefined behavior + when running two instances at the same time. + * Rockspec quality-of-life improvements: + * Using an unknown `build.type` now automatically + implies a build dependency for `luarocks-build-`. + * Improve `rockspec.source.dir` autodetection. + * `builtin` build mode now automatically inherits include + and libdirs from `external_dependencies` if not set + explicitly. + * improved and simplified Lua interpreter search. + * `lua_interpreter` config value is deprecated in favor + of `variables.LUA` which contains the full interpreter path. + * `luarocks-admin remove` now supports the `file://` + protocol for managing local rocks servers. + * Bundled dkjson library, so that `luarocks upload` does not + require an external JSON library. + * New flags for `luarocks init`: `--no-gitignore`, + `--no-wrapper-scripts`, `--wrapper-dir`. + * `luarocks config` now attempts updating the system config + by default when `local_by_default` is `false`. + * New flag for `luarocks path`: `--full`, for use with + `--lr-path` and `--lr-cpath`. +* Fixes: + * various Windows-specific fixes: + * `build.install_command` now works correctly on Windows. + * do not attempt to set "executable" permissions for folders + on Windows. + * better handling of Windows backslash paths. + * fix program search when using absolute paths and `.exe` files. + * improved lookup order for library dependencies. + * `LUALIB` filename detection is now done dynamically at + runtime and not hardcoded by the Windows installer. + * prevent LuaRocks from blocking `luafilesystem` from being + removed on Windows. + * `luarocks build` no longer looks for Lua headers when installing + pure-Lua rocks. + * `luarocks build` table in rockspecs now gets some additional validation + to prevent crashes on malformed rockspecs. + * `build.builtin` now compiles C modules in a temporary directory, + avoiding name clashes + * `build_dependencies` now correctly installs dependencies + for the Lua version that LuaRocks is running on, and not + the one it is building for with `--lua-version`. + * `build_dependencies` can now use a dependency available + in any rocks tree (system, user, project). + * `luarocks config` now prints boolean values correctly on Lua 5.1. + * `luarocks config` now ensures the target directory exists when saving + a configuration. + * `luarocks init` now injects the project's `package.(c)path` in the + Lua wrapper. + * `luarocks lint` no longer crashes if a rockspec misses a `description` field. + * `luarocks test` now handles malformed `command` entries gracefully. + * if `--lua-*` flags are given in the CLI, the hardcoded values + are never used. + * the "no downloader" error is now shown only once, and not + once per failed mirror. + * project dir is always presented normalized + * catch the failure to setup `LUA_BINDIR` early. + * when using `--pack-binary-rock` and a `zip` program is + unavailable, report that instead of failing cryptically. + * More graceful handling when failing to create a local cache. + * Avoid confusion with macOS multiarch binaries on system detection. + * Add `--tree` to the rocks trees list. + * Better support for LuaJIT versions with extra + suffixes in their version numbers. + * Don't use floats to parse Lua version number. + * Various fixes related to path normalization. + +## What's new in LuaRocks 3.9.2 + +* Configuration now honors typical compiler environment variables + for all build backends: + * `MAKE`, `CC`, `AR`, `RANLIB` on Unix + * `MAKE`, `CC`, `AR`, `WINDRES`, `LINK`, `MT` on Windows +* `builtin` build mode now supports Clang on Windows +* `luarocks test` now checks/installs all dependency kinds + (build, runtime, test), so you don't need to run + `luarocks make --only-deps` in CI environments to get all + dependencies needed to run a test +* MinGW: default to x86_64 compiler on 64-bit platforms +* Fixed crash if `variables.LUA*` are unset in configuration +* Fix `luarocks test --prepare` behavior for non-Busted tests +* Internal API fixes + * `path.path_to_module`: accept custom file extensions in + package path variables + * `persist.save_from_table`: ensure directory exists when + saving a file + +## What's new in LuaRocks 3.9.1 + +* Fixed error message when Lua library is not found +* Fixed build of Windows binary +* A couple of minor feature additions: + * API: `loader.which` has a new mode for searching `package.path/cpath` + * Adds a new second argument, `where`, a string which indicates places + to search for the module. If `where` contains `"l"`, it will search + using the LuaRocks loader; if it contains `"p"`, it will look in the + filesystem using `package.path` and `package.cpath`. You can use both + at the same time. + * `--no-project` flag can be used to override `.luarocks` project directory + detection + +## What's new in LuaRocks 3.9.0 + +* `builtin` build mode now always respects CC, CFLAGS and LDFLAGS +* Check that lua.h version matches the desired Lua version +* Check that the version of the Lua C library matches the desired Lua version +* Fixed deployment of non-wrapped binaries +* Fixed crash when `--lua-version` option is malformed +* Fixed help message for `--pin` option +* Unix: use native methods and don't always rely on $USER to determine user +* Windows: use native CLI tooling more +* macOS: support .tbd extension when checking for libraries +* macOS: add XCode SDK path to search paths +* macOS: add best-effort heuristic for library search using Homebrew paths +* macOS: avoid quoting issues with LIBFLAG +* macOS: deployment target is now 11.0 on macOS 11+ +* added DragonFly BSD support +* LuaRocks test suite now runs on Lua 5.4 and LuaJIT +* Internal dependencies of standalone LuaRocks executable were bumped + +## What's new in LuaRocks 3.8.0 + +* Support GitHub's protocol security changes transparently. + * The raw git:// protocol will stop working on GitHub. LuaRocks already + supports git+https:// as an alternative, but to avoid having to update + every rockspec in the repository that uses git://github.com, which would + require a large coordinated effort, LuaRocks now auto-converts github.com + and www.github.com URLs that use git:// to git+https:// +* `luarocks test` has a new flag `--prepare` that checks, downloads and + installs the tool requirements and rockspec dependencies but does not + run the test suite for the rockspec being tested. +* Code tweaks so that LuaRocks can run on a Lua interpreter built without + the `debug` library. +* `luarocks upload` supports uploading pre-packaged `.src.rock` files. +* Configuration fixes for OpenBSD. +* Respect the existing value for the `variables.LUALIB` configuration + variable if given explicitly by the user in the config file, rather + than trying to override it with auto-detection. +* Windows fixes for setting file permissions: + * Revert the use of `Everyone` back to `*S-1-1-0` + * Quote the use of the `%USERNAME%` variable to support names with spaces + +## What's new in LuaRocks 3.7.0 + +* Improved connectivity resiliency + * LuaRocks can now use mirrors for downloading rocks even if downloading + the manifest from the main server succeeds. + In previous versions, LuaRocks would check whether to use a mirror in the first + download operation, when it fetches the manifest. Once the server + (luarocks.org or one of its default mirrors) was chosen, it would stick with + it for the rest of the command. + The resulting behavior was that if the manifest fails to load, it switches to + a mirror and continues from there. But if the manifest fetches ok and the then + actual rock download fails, it would give up, instead of trying that in a + mirror as well. + Now, it retries every download on a mirror whenever the base URL matches one + configured in cfg.rocks_servers. The original behavior was satisfactory if + there was complete downtime in the main server, but this new behavior should + make the CLI much more resilient with regard to any intermittent failures + happening on the main server. +* On Unix, it now respects environment variables $XDG_CACHE_HOME and $XDG_CONFIG_HOME + * This means the user's configuration typically resides in ~/.config/luarocks/ + as per the XDG standard + * The legacy path ~/.luarocks/ continues to be tested first, for backwards + compatibility +* Fixes check for the default Lua version set in the user's home configuration +* Fixes an issue on Windows where it would incorrectly revoke permissions + from the current user when installing + +## What's new in LuaRocks 3.6.0 + +* Adds a double-check step to verify that all files from a rock are installed +* Improve resilience of the manifest reader to deal with manifests + written with older versions of LuaRocks lower than 3.0 +* `luarocks pack` now checks that the directory inside the archive being packed + as a `.src.rock` actually exists, refusing to pack an invalid rock from + a badly configured rockspec. +* Fixes behavior of `luarocks pack` when the `url` entry of a rockspec + points to a bare file. +* Remove an entry from the manifest if the rock itself is already missing +* The `configure` script now checks that the version of `lua.h` + found matches that of the Lua interpreter detected or configured +* Fixes the renaming of scripts when multiple versions are installed +* Fixes availability check for `svn` for rockspecs using Subversion +* Fixes for running with an empty PATH environment variable +* Portability improvements: + * Windows: vcvarsall.bat output is now properly redirected to NUL + meaning that the output of `luarocks path` can be used in scripts + * Fixes autodetection for Cygwin + * Handles macOS versions greater than 10.10 + * Adds platform specific configurations for NetBSD + * Respects CC/CFLAGS/LDFLAGS on FreeBSD +* Luacheck now runs on the LuaRocks CI +* Distributed binaries are built using Lua 5.3 + +## What's new in LuaRocks 3.5.0 + +This is a small release: + +* Added support for MSYS2 and Mingw-w64 +* Reverted the change in MSVC environment variable set up script +* Fixes a bug where `--verbose` raised an exception with a nil argument +* Added proper error messages when lua.h is invalid + + +## What's new in LuaRocks 3.4.0 + +### Features + +* `luarocks make` now supports `--only-deps` +* `luarocks make` new flag: `--no-install`, which only performs + the compilation step +* `--deps-only` is now an alias for `--only-deps` (useful in case + you always kept getting it wrong, like me!) +* `luarocks build` and `luarocks make` now support using + `--pin` and `--only-deps` at the same time, to produce a lock + file of dependencies in use without installing the main package. +* `luarocks show` can now accept a substring of the rock's name, + like `list`. +* `luarocks config`: when running without system-wide permissions, + try storing the config locally by default. + Also, if setting both lua_dir and --lua-version explicitly, + auto-switch the default Lua version. +* `luarocks` with no arguments now prints more info about the + location of the Lua interpreter which is being used +* `luarocks new_version` now keeps the old URL if the MD5 doesn't + change. +* `DEPS_DIR` is now accepted as a generic variable for dependency + directories (e.g. `luarocks install foo DEPS_DIR=/usr/local`) +* Handle quoting of arguments at the application level, for + improved Windows support +* All-in-one binary bundles `dkjson`, so it runs `luarocks upload` + without requiring any additional dependencies. +* Tweaks for Terra compatibility + +### Fixes + +* win32: generate proper temp filename +* No longer assume that Lua 5.3 is built with compat libraries and + bundles `bit32` +* `luarocks show`: do not crash when rockspec description is empty +* When detecting the location of `lua.h`, check that its version + matches the version of Lua being used +* Fail gracefully when a third-party tool (wget, etc.) is missing +* Fix logic for disabling mirrors that return network errors +* Fix detection of Lua path based on arg variable +* Fix regression on dependency matching of luarocks.loader + + +## What's new in LuaRocks 3.3.1 + +This is a bugfix release: + +* Fix downgrades of rocks containing directories: stop it + from creating spurious 0-byte files where directories have been +* Fix error message when attempting to copy a file that is missing +* Detect OpenBSD-specific dependency paths + +## What's new in LuaRocks 3.3.0 + +### Features + +* **Dependency pinning** + * Adds a new flag called `--pin` which creates a `luarocks.lock` + when building a rock with `luarocks build` or `luarocks make`. + This lock file contains the exact version numbers of every + direct or indirect dependency of the rock (in other words, + it is the transitive closure of the dependencies.) + For `make`, the `luarocks.lock` file is created in the current + directory. + The lock file is also installed as part of the rock in + its metadata directory alongside its rockspec. + When using `--pin`, if a lock file already exists, it is + ignored and overwritten. + * When building a rock with `luarocks make`, if there is a + `luarocks.lock` file in the current directory, the exact + versions specified there will be used for resolving dependencies. + * When building a rock with `luarocks build`, if there is a + `luarocks.lock` file in root of its sources, the exact + versions specified there will be used for resolving dependencies. + * When installing a `.rock` file with `luarocks install`, if the + rock contains a `luarocks.lock` file (i.e., if its dependencies + were pinned with `--pin` when the rock was built), the exact + versions specified there will be used for resolving dependencies. +* Improved VM type detection to support moonjit +* git: Support for shallow recommendations +* Initial support for Windows on ARM +* Support for building 64-bit Windows all-in-one binary +* More filesystem debugging output when using `--verbose` (now it + reports operations even when using LuaFileSystem-backed implementations) +* `--no-manifest` flag for creating a package without updating the + manifest files +* `--no-doc` flag is now supported by `luarocks make` + +### Performance improvements + +* Speed up dependency checks +* Speed up installation and deletion when deploying files +* build: do not download sources when when building with `--only-deps` +* New flag `--check-lua-versions`: when a rock name is not found, only + checks for availability in other Lua versions if this flag is given + +### Fixes + +* safer rollback on installation failure +* config: fix `--unset` flag +* Fix command name invocations with dashes (e.g. `luarocks-admin make-manifest`) +* Fix fallback to PATH search when Lua interpreter is not configured +* Windows: support usernames with spaces +* Windows: fix generation of temporary filenames (#1058) +* Windows: force `.lib` over `.dll` extension when resolving `LUALIB` + +## What's new in LuaRocks 3.2.1 + +* fix installation of LuaRocks via rockspec (`make bootstrap` and +`luarocks install`): correct a problem in the initialization of the +luarocks.fs module and its interaction with the cfg module. +* fix luarocks build --pack-binary-rock --no-doc +* fix luarocks build --branch +* luarocks init: fix Lua wrapper for interactive mode +* fix compatibility issues with command add-ons loaded via +luarocks.cmd.external modules +* correct override of config values via CLI flags + +## What's new in LuaRocks 3.2.0 + +LuaRocks 3.2.0 now uses argument parsing based on argparse +instead of a homegrown parser. This was implemented by Paul +Ouellette as his Google Summer of Code project, mentored by +Daurnimator. + +Release highlights: + +* Bugfix: luarocks path does not change the order of pre-existing path +items when prepending or appending to path variables +* Bugfix: fix directory detection on the Mac +* When building with --force-config, LuaRocks now never uses the +"project" directory, but only the forced configuration +* Lua libdir is now only checked for commands/platforms that really +need to link Lua explicitly +* LuaJIT is now detected dynamically +* RaptorJIT is now detected as a LuaJIT variant +* Improvements in Lua autodetection at runtime +* luarocks new_version: new option --dir +* luarocks which: report modules found via package.path and +package.cpath as well +* install.bat: Improved detection for Visual Studio 2017 and higher +* Bundled LuaSec in all-in-one binary bumped to version 0.8.1 + +## What's new in LuaRocks 3.1.3 + +This is another bugfix release, that incldes a couple of fixes, +including better Lua detection, and fixes specific to MacOS and +FreeBSD. + +## What's new in LuaRocks 3.1.2 + +This is again a small fix release. + +## What's new in LuaRocks 3.1.1 + +This is a hotfix release fixing an issue that affected initialization +in some scenarios. + +## What's new in LuaRocks 3.1.0 + +### More powerful `luarocks config` + +The `luarocks config` command used to only list the current +configuration. It is now able to query and also _set_ individual +values, like `git config`. You can now do things such as: + + luarocks config variables.OPENSSL_DIR /usr/local/openssl + luarocks config lua_dir /usr/local + luarocks config lua_version 5.3 + +and it will rewrite your luarocks configuration to store that value +for later reuse. Note that setting `lua_version` will make that Lua +version the default for `luarocks` invocations (you can always +override on a per-call basis with `--lua-version`. + +You can specify the scope where you will apply the configuration +change: system-wide, to the user's home config (with --local), or +specifically to a project, if you run the command from within a +project directory initialized with `luarocks init`. + +### New `--global` flag + +Some users prefer that LuaRocks default to system-wide installations, +some users prefer to install everything to their home directory. The +`local_by_default` configuration file controls this preference: when +it is off, the `--local` file triggers user-specific. Before 3.1.0 +there was no convenient way to trigger system-wide installations when +`local_by_default` was set to true. LuaRocks 3.1.0 adds a `--global` +flag to this purpose. To enable local-by-default, you can now do: + + luarocks config local_by_default true + +### `luarocks make` can deal with patches + +A rockspec can include embedded patch files, which are applied when a +source rock is built. Now, when you run `luarocks make` on a source +tree unpacked with `luarocks unpack`, the patches will be applied as +well (and a hidden lockfile is created to avoid the patches to be +re-applied incorrectly). + +### Smarter defaults when working with projects + +When working on a project initialized with `luarocks init`, the +presence of a ./.luarocks/config-5.x.lua file will be enough to detect +the project-based workflow and have `luarocks` default to that 5.x +version. That means the `./luarocks` wrapper becomes less necessary; +the `luarocks` from your $PATH will deal with the project just fine, +git-style. + +### And more! + +There are also other improvements. LuaRocks uses the manifest cache a +bit more aggressively, resulting in increased performance. Also, it no +longer complains with a warning message if the home cache cannot be +created (it just uses a temporary dir instead). And of course, the +release includes multiple bugfixes. + +## What's new in LuaRocks 3.0.4 + +* Fork-free platform detection at startup +* Improved detection of the default rockspec in commands such as `luarocks test` +* Various minor bugfixes + +## What's new in LuaRocks 3.0.3 + +LuaRocks 3.0.3 is a minor bugfix release, fixing a regression in +luarocks.loader introduced in 3.0.2. + +## What's new in LuaRocks 3.0.2 + +* Improvements in luarocks init, new --reset flag +* write_rockspec: --lua-version renamed to --lua-versions +* Improved behavior in module autodetection +* Bugfixes in luarocks show +* Fix upgrade/downgrade when a single rock has clashing module +filenames (should fix the issue when downgrading luasec) +* Fix for autodetected external dependencies with non-alphabetic +characters (should fix the libstdc++ issue when installing xml) + + +## What's new in LuaRocks 3.0.1 + +* Numerous bugfixes including: + * Handle missing global `arg` + * Fix umask behavior + * Do not overwrite paths in format 5.x.y when cleaning up path +variables (#868) + * Do not detect files under lua_modules as part of your sources +when running `luarocks write_rockspec` + * Windows: do not hardcode MINGW in the all-in-one binary: instead +it properly detects when running from a Visual Studio Developer +Console and uses that compiler instead + * configure: --sysconfdir was fixed to its correct meaning: it now +defaults to /etc and not /etc/luarocks (`/luarocks` is appended to the +value of sysconfdir) + * configure: fixed --force-config +* Store Lua location in config file, so that a user can run `luarocks +init --lua-dir=/my/lua/location` and have that location remain active +for that project +* Various improvements to the Unix makefile, including $(DESTDIR) +support and an uninstall rule +* Autodetect FreeBSD-style include paths (/usr/include/lua5x/) + +## What's new in LuaRocks 3.0.0 + +- [New rockspec format](#new-rockspec-format) +- [New commands](#new-commands), including [luarocks init](https://github.com/luarocks/luarocks/wiki/Project:-LuaRocks-per-project-workflow) for per-project workflows +- [New flags](#new-flags), including `--lua-dir` and `--lua-version` for using multiple Lua installs with a single LuaRocks +- [New build system](#new-build-system) +- [General improvements](#general-improvements), including [namespaces](https://github.com/luarocks/luarocks/wiki/Namespaces) +- [User-visible changes](#user-visible-changes), including some **breaking changes** +- [Internal changes](#internal-changes) + +### New rockspec format + +**New rockspec format:** if you add `rockspec_format = "3.0"` to your rockspec, +you can use a number of new features. Note that these rockspecs will only work +with LuaRocks 3.0 and above, but older versions will detect that directive and +fail gracefully, giving the user a message telling them to upgrade. Rockspecs +without the `rockspec_format` directive are interpreted as having format 1.0 +(the same format from LuaRocks series 1.x and 2.x) and are still supported. + +The following features are only enabled if `rockspec_format = "3.0"` is set in +the rockspec: + +* Build type `builtin` is the default if `build.type` is not specified. +* The `builtin` type auto-detects modules using the same heuristics as + `write_rockspec` (for example, if you have a `src` directory). With + auto-detection of the build type and modules, many rockspecs don't + even need an explicit `build` table anymore. +* New table `build_dependencies`: dependencies used only for running + `luarocks build` but not when installing binary rocks. +* New table `test_dependencies`: dependencies used only for running `luarocks test` +* New table `test`: settings for configuring the behavior of `luarocks test`. + Supports a `test.type` field so that the test backend can be specified. + Currently supported test backends are: + * `"busted"`, for running [Busted](https://olivinelabs.com/busted) + * `"command"`, for running a plain command. + * Custom backends can be loaded via `test_dependencies` +* New field `build.macosx_deployment_target = "10.9"` is supported in Mac platforms, + and adjusts `$(CC)` and `$(LD)` variables to export the corresponding + environment variable. +* LuaJIT can be detected in dependencies and uses version reported by the + running interpreter: e.g. `"luajit >= 2.1"`. +* Auto-detection of `source.dir` is improved: when the tarball contains + only one directory at the root, assume that is where the sources are. +* New `description` fields: + * `labels`, an array of strings; + * `issues_url`, URL to the project's bug tracker. +* `cmake` build type now supports `build.build_pass` and `build_install_pass` + to disable `make` passes. +* `git` fetch type fetches submodules by default. +* Patches added in `patches` can create and delete files, following standard + patch rules. + +### New commands + +* **New command:** `luarocks init`. This command performs the setup for using + LuaRocks in a "project directory": + * it creates a `lua_modules` directory in the current directory for + storing rocks + * it creates a `.luarocks/config-5.x.lua` local configuration file + * it creates `lua` and `luarocks` wrapper scripts in the current + directory that are configured to use `lua_modules` and + `.luarocks/config-5.x.lua` + * if there are no rockspecs in the current directory, it creates one + based on the directory name and contents. +* **New command:** `luarocks test`. It runs a rock's test suite, as specified + in the new `test` section of the rockspec file. It also does some + autodetection, so it already works with many existing rocks as well. +* **New command:** `luarocks which`. Given the name of an installed, it tells + you which rock it is a part of. For example, `luarocks which lfs` + will tell you it is a part of `luafilesystem` (and give the full + path name to the module). In this sense, `luarocks which` is the + dual command to `luarocks show`. + +### New flags + +* **New flags** `--lua-dir` and `--lua-version` which can be used with + all commands. This allows you to specify a Lua version and installation + prefix at runtime, so a single LuaRocks installation can be used + to manage packages for any Lua version. It is no longer necessary to + install separate copies of LuaRocks to manage packages for Lua 5.x + and 5.y. +* **New flags** added to `luarocks show`: `--porcelain`, giving a stable + script-friendly output (named after the Git `--porcelain` flag that + serves the same purpose) and `--rock-license`. +* **New flag** `--temp-key` for `luarocks upload`, allowing you to easily + upload rocks into an alternate account without disrupting the + stored configuration of your main account. +* **New flag** `--dev`, for enabling development-branch sub-repositories. + This adds support for easily requesting `dev` modules from LuaRocks.org, as in: + `luarocks install --dev luafilesystem`. The list of URLs configured + in `rocks_servers` is prepended with a list containing "/dev" in their paths. +* `luarocks config`, when called with no arguments, now displays your + entire active configuration, using the same Lua syntax as the configuration + file. It is sensitive to the flags given to it (`--tree`, `--lua-dir`, etc.) + so it presents the resulting configuration produced by loading the + currently-active configuration files and the given flags. + +### New build system + +**New build system**: the `configure` and `Makefile` scripts were completely +overhauled, making use of LuaRocks 3 features to greatly simplify them: + +* Much of the detection and configuration work they performed were moved + to runtime, to make LuaRocks more dynamic and resilient to environment + changes +* The system-package-manager-friendly mode is still available, as the + default target (`make`, formerly `make build`). +* The LuaRocks-as-a-rock mode (`make bootstrap`) is also still available, + and was greatly simplified: it no longer uses custom Makefiles: + LuaRocks installs itself using `luarocks make`, and its own rockspec + uses the `builtin` build mode. +* A new build mode: `make binary` compiles all of LuaRocks into a single + executable, bundling various Lua modules to make it self-sufficient, + such as LuaFileSystem, LuaSocket and LuaSec. + * For version 3.0, this will remain as an option, as we evaluate + its suitability moving forward to become the default mode of + distribution. + * The goal is to eventually use this mode to produce the Windows + version of LuaRocks. We currently include an experimental + `make windows-binary` target which builds a Windows version + using the MinGW-w64 cross-compiler on Linux. + +### General improvements + +* **New feature:** [namespaces](https://github.com/luarocks/luarocks/wiki/Namespaces): + you can use `luarocks install user/package` to install a package from a + specific user of the repository. +* Improved defaults for finding external libraries on Linux and Windows. +* Detection of the Lua library and header directories is now done at runtime. + This uses the same machinery that LuaRocks employs for `external_dependencies` + in general (with some added logic to cope with the unfortunate + rampant inconsistency in naming of Lua libraries and header paths + due to lack of upstream standardization). +* `luarocks-admin add` now works with `file://` repositories +* some UI improvements in `luarocks list` and `luarocks search`. +* Preliminary support for the upcoming Lua 5.4: LuaRocks is written in + the common dialect supporting Lua 5.1-5.3 and LuaJIT, but since a + single installation can manage packages for any Lua version now, + it can already manage packages for Lua 5.4 even though that's not + out yet. + +### User-visible changes + +* **Breaking change:** The support for deprecated unversioned paths + (e.g. `/usr/local/lib/luarocks/rocks/` and `/etc/luarocks/config.lua`) + was removed, LuaRocks will now only create and use paths versioned + to the specific Lua version in use + (e.g. `/usr/local/lib/luarocks/rocks-5.3/` and `/etc/luarocks/config-5.3.lua`). +* **Breaking changes:** `luarocks path` now exports versioned variables + `LUA_PATH_5_x` and `LUA_CPATH_5_x` instead of `LUA_PATH` and `LUA_CPATH` + when those are in use in your system. +* Package paths are sanitized to only reference the current Lua version. + For example, if you have `/some/dir/lua/5.1/` in your `$LUA_PATH` and + you are running Lua 5.2, `luarocks.loader` and the `luarocks` command-line + tool will convert it to `/some/dir/lua/5.2/`. +* LuaRocks now uses `dev` instead of `scm` as the favored version identifier + to describe development versions of a rock, aligning it with the terminology + used in https://luarocks.org. It still understands `scm` as a + compatibility fallback. +* LuaRocks no longer conflates modules `foo` and `foo.init` as being the + same in its internal manifest. Instead, the `luarocks.loader` module + is adapted to handle the `.init` case. +* Wrappers installed using `--tree` now prepend the tree's prefix to their + package paths. +* `luarocks-admin` commands no longer creates an `index.html` file in the + repository by default (it does update it if it already exists) + +### Internal changes + +* Major improvements in the test suite done by @georgeroman as part of the ongoing + Google Summer of Code 2018 program. The coverage improvements and test suite + speed-ups have been essential in getting the sprint towards LuaRocks 3.0 more + efficient and reliable! +* Modules needed by `luarocks.loader` were moved below the `luarocks.core` namespace. + Modules in `luarocks.core` only depend on other `luarocks.core` modules. + (Notably, `luarocks.core` does not use `luarocks.fs`.) +* Modules representing `luarocks` commands were moved into the `luarocks.cmd` namespace, + and `luarocks.command_line` was renamed to `luarocks.cmd`. Eventually, all CLI-related + code will live under `luarocks.cmd`, as we move towards a clean CLI-API separation, + in preparation for a stable public API. +* Likewise, modules representing `luarocks-admin` commands were moved into the + `luarocks.admin.cmd` namespace. +* New internal objects for representing interaction with the repostories: + `luarocks.queries` and `luarocks.results` +* Type checking rules of file formats were moved into the `luarocks.type` namespace. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ba74839 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# LuaRocks Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers of LuaRocks pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at hisham@gobolinux.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..1d1ed74 --- /dev/null +++ b/COPYING @@ -0,0 +1,20 @@ +Copyright 2007-2011, Kepler Project. +Copyright 2011-2022, the LuaRocks project authors. + +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/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..dade806 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,187 @@ +MAKEFLAGS += --jobs=1 + +-include config.unix + +datarootdir = $(prefix)/share +bindir = $(prefix)/bin +INSTALL = install +INSTALL_DATA = $(INSTALL) -m 644 +BINARY_PLATFORM = unix + +SHEBANG = \#!$(LUA) +luarocksconfdir = $(sysconfdir)/luarocks +luadir = $(datarootdir)/lua/$(LUA_VERSION) +builddir = ./build +buildbinarydir = ./build-binary + +LUAROCKS_FILES = $(shell find src/luarocks/ -type f -name '*.lua') + +LUA_ENV_VARS = LUA_PATH LUA_PATH_5_2 LUA_PATH_5_3 LUA_PATH_5_4 LUA_CPATH LUA_CPATH_5_2 LUA_CPATH_5_3 LUA_CPATH_5_4 + +all: build + +# ---------------------------------------- +# Base build +# ---------------------------------------- + +build: config.unix $(builddir)/config-$(LUA_VERSION).lua $(builddir)/luarocks $(builddir)/luarocks-admin + +config.unix: + @echo Please run the "./configure" script before building. + @echo + @exit 1 + +$(builddir)/config-$(LUA_VERSION).lua: config.unix + mkdir -p "$(@D)" + @printf -- '-- LuaRocks configuration\n\n'\ + 'rocks_trees = {\n'\ + ' { name = "user", root = home .. "/.luarocks" };\n'\ + "$$([ "$(rocks_tree)" != "$(HOME)/.luarocks" ] && printf ' { name = "system", root = "'"$(rocks_tree)"'" };\\n')"\ + '}\n'\ + 'variables = {\n'\ + "$$([ -n "$(LUA_DIR)" ] && printf ' LUA_DIR = "%s";\\n' "$(LUA_DIR)")"\ + "$$([ -n "$(LUA_INCDIR)" ] && printf ' LUA_INCDIR = "%s";\\n' "$(LUA_INCDIR)")"\ + "$$([ -n "$(LUA_BINDIR)" ] && printf ' LUA_BINDIR = "%s";\\n' "$(LUA_BINDIR)")"\ + "$$([ -n "$(LUA_LIBDIR)" ] && printf ' LUA_LIBDIR = "%s";\\n' "$(LUA_LIBDIR)")"\ + "$$([ -n "$(LUA_VERSION)" ] && printf ' LUA_VERSION = "%s";\\n' "$(LUA_VERSION)")"\ + "$$([ -n "$(LUA)" ] && printf ' LUA = "%s";\\n' "$(LUA)")"\ + '}\n'\ + > $@ + +luarocks: config.unix $(builddir)/config-$(LUA_VERSION).lua + mkdir -p .luarocks + cp $(builddir)/config-$(LUA_VERSION).lua .luarocks/config-$(LUA_VERSION).lua + rm -f src/luarocks/core/hardcoded.lua + echo "#!/bin/sh" > luarocks + echo "unset $(LUA_ENV_VARS)" >> luarocks + echo 'LUAROCKS_SYSCONFDIR="$(luarocksconfdir)" LUA_PATH="$(CURDIR)/src/?.lua;;" exec "$(LUA)" "$(CURDIR)/src/bin/luarocks" --project-tree="$(CURDIR)/lua_modules" "$$@"' >> luarocks + chmod +rx ./luarocks + ./luarocks init + +luarocks-admin: config.unix + rm -f src/luarocks/core/hardcoded.lua + echo "#!/bin/sh" > luarocks-admin + echo "unset $(LUA_ENV_VARS)" >> luarocks-admin + echo 'LUAROCKS_SYSCONFDIR="$(luarocksconfdir)" LUA_PATH="$(CURDIR)/src/?.lua;;" exec "$(LUA)" "$(CURDIR)/src/bin/luarocks-admin" --project-tree="$(CURDIR)/lua_modules" "$$@"' >> luarocks-admin + chmod +rx ./luarocks-admin + +$(builddir)/luarocks: src/bin/luarocks config.unix + mkdir -p "$(@D)" + (printf '$(SHEBANG)\n'\ + 'package.loaded["luarocks.core.hardcoded"] = { '\ + "$$([ -n "$(FORCE_CONFIG)" ] && printf 'FORCE_CONFIG = true, ')"\ + 'SYSCONFDIR = [[$(luarocksconfdir)]] }\n'\ + 'package.path=[[$(luadir)/?.lua;]] .. package.path\n'\ + 'local list = package.searchers or package.loaders; table.insert(list, 1, function(name) if name:match("^luarocks%%.") then return loadfile([[$(luadir)/]] .. name:gsub([[%%.]], [[/]]) .. [[.lua]]) end end)\n'; \ + tail -n +2 src/bin/luarocks \ + )> "$@" + +$(builddir)/luarocks-admin: src/bin/luarocks-admin config.unix + mkdir -p "$(@D)" + (printf '$(SHEBANG)\n'\ + 'package.loaded["luarocks.core.hardcoded"] = { '\ + "$$([ -n "$(FORCE_CONFIG)" ] && printf 'FORCE_CONFIG = true, ')"\ + 'SYSCONFDIR = [[$(luarocksconfdir)]] }\n'\ + 'package.path=[[$(luadir)/?.lua;]] .. package.path\n'\ + 'local list = package.searchers or package.loaders; table.insert(list, 1, function(name) if name:match("^luarocks%%.") then return loadfile([[$(luadir)/]] .. name:gsub([[%%.]], [[/]]) .. [[.lua]]) end end)\n'; \ + tail -n +2 src/bin/luarocks-admin \ + )> "$@" + +# ---------------------------------------- +# Base build +# ---------------------------------------- + +binary: luarocks $(buildbinarydir)/luarocks.exe $(buildbinarydir)/luarocks-admin.exe + +$(buildbinarydir)/luarocks.exe: src/bin/luarocks $(LUAROCKS_FILES) + (unset $(LUA_ENV_VARS); \ + "$(LUA)" binary/all_in_one "$<" "$(LUA_DIR)" "^src/luarocks/admin/" "$(luarocksconfdir)" "$(@D)" "$(FORCE_CONFIG)" $(BINARY_PLATFORM) $(CC) $(NM) $(BINARY_SYSROOT)) + +$(buildbinarydir)/luarocks-admin.exe: src/bin/luarocks-admin $(LUAROCKS_FILES) + (unset $(LUA_ENV_VARS); \ + "$(LUA)" binary/all_in_one "$<" "$(LUA_DIR)" "^src/luarocks/cmd/" "$(luarocksconfdir)" "$(@D)" "$(FORCE_CONFIG)" $(BINARY_PLATFORM) $(CC) $(NM) $(BINARY_SYSROOT)) + +# ---------------------------------------- +# Regular install +# ---------------------------------------- + +INSTALL_FILES = + +install: all install-config + mkdir -p '$(DESTDIR)$(bindir)/' + $(INSTALL) '$(builddir)/luarocks' '$(DESTDIR)$(bindir)/luarocks' + $(INSTALL) '$(builddir)/luarocks-admin' '$(DESTDIR)$(bindir)/luarocks-admin' + find src/luarocks/ -type d | while read f; \ + do \ + mkdir -p '$(DESTDIR)$(luadir)'/`echo $$f | sed 's,^src/,,'`; \ + done + find src/luarocks/ -type f -name '*.lua' | while read f; \ + do \ + $(INSTALL_DATA) "$$f" '$(DESTDIR)$(luadir)'/`echo $$f | sed 's,^src/,,'`; \ + done + +install-config: + mkdir -p '$(DESTDIR)$(luarocksconfdir)/' + $(INSTALL_DATA) '$(builddir)/config-$(LUA_VERSION).lua' '$(DESTDIR)$(luarocksconfdir)/config-$(LUA_VERSION).lua' + +uninstall: + rm -rf $(DESTDIR)$(bindir)/luarocks \ + $(DESTDIR)$(bindir)/luarocks-admin \ + $(DESTDIR)$(luarocksconfdir)/config-$(LUA_VERSION).lua \ + $(patsubst src/%, $(DESTDIR)$(luadir)/%, $(LUAROCKS_FILES)) + +# ---------------------------------------- +# Binary install +# ---------------------------------------- + +install-binary: binary install-config + mkdir -p '$(DESTDIR)$(bindir)/' + $(INSTALL) "$(buildbinarydir)/luarocks.exe" "$(DESTDIR)$(bindir)/luarocks" + $(INSTALL) "$(buildbinarydir)/luarocks-admin.exe" "$(DESTDIR)$(bindir)/luarocks-admin" + mkdir -p '$(DESTDIR)$(luadir)/luarocks/core' + for f in src/luarocks/core/*.lua src/luarocks/loader.lua; \ + do \ + $(INSTALL_DATA) "$$f" '$(DESTDIR)$(luadir)'/`echo $$f | sed 's,^src/,,'`; \ + done + +# ---------------------------------------- +# Bootstrap install +# ---------------------------------------- + +bootstrap: luarocks install-config + ./luarocks make --tree="$(DESTDIR)$(rocks_tree)" + +# ---------------------------------------- +# Windows binary build +# ---------------------------------------- + +windows-binary: windows-binary-32 windows-binary-64 + +windows-clean: windows-clean-32 windows-clean-64 + +windows-binary-32: luarocks + $(MAKE) -f binary/Makefile.windows windows-binary MINGW_PREFIX=i686-w64-mingw32 OPENSSL_PLATFORM=mingw + +windows-clean-32: + $(MAKE) -f binary/Makefile.windows windows-clean MINGW_PREFIX=i686-w64-mingw32 OPENSSL_PLATFORM=mingw + +windows-binary-64: luarocks + $(MAKE) -f binary/Makefile.windows windows-binary MINGW_PREFIX=x86_64-w64-mingw32 OPENSSL_PLATFORM=mingw64 + +windows-clean-64: + $(MAKE) -f binary/Makefile.windows windows-clean MINGW_PREFIX=x86_64-w64-mingw32 OPENSSL_PLATFORM=mingw64 + +# ---------------------------------------- +# Clean +# ---------------------------------------- + +clean: windows-clean + rm -rf ./config.unix \ + ./luarocks \ + ./luarocks-admin \ + $(builddir)/ \ + $(buildbinarydir)/ \ + ./.luarocks \ + ./lua_modules + +.PHONY: all build install install-config binary install-binary bootstrap clean windows-binary windows-clean diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d0f0d47 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.POSIX: + +all: + +gmake -f GNUmakefile all + +.DEFAULT: + +gmake -f GNUmakefile $< diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a74822 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +

LuaRocks

+ +A package manager for Lua modules. + +[![Build Status](https://github.com/luarocks/luarocks/actions/workflows/test.yml/badge.svg)](https://github.com/luarocks/luarocks/actions) +[![Luacheck](https://github.com/luarocks/luarocks/actions/workflows/luacheck.yml/badge.svg)](https://github.com/luarocks/luarocks/actions/workflows/luacheck.yml) +[![Build Status](https://ci.appveyor.com/api/projects/status/4x4630tcf64da48i/branch/master?svg=true)](https://ci.appveyor.com/project/hishamhm/luarocks/branch/master) +[![Coverage Status](https://codecov.io/gh/luarocks/luarocks/coverage.svg?branch=master)](https://codecov.io/gh/luarocks/luarocks/branch/master) +[![Join the chat at https://gitter.im/luarocks/luarocks](https://badges.gitter.im/luarocks/luarocks.svg)](https://gitter.im/luarocks/luarocks) + +Main website: [luarocks.org](http://www.luarocks.org) + +It allows you to install Lua modules as self-contained packages called +[*rocks*][1], which also contain version [dependency][2] information. This +information can be used both during installation, so that when one rock is +requested all rocks it depends on are installed as well, and also optionally +at run time, so that when a module is required, the correct version is loaded. +LuaRocks supports both local and [remote][3] repositories, and multiple local +rocks trees. + +## Installing + +* [Installation instructions for Unix](http://luarocks.org/en/Installation_instructions_for_Unix) (Linux, BSDs, etc.) +* [Installation instructions for macOS](http://luarocks.org/en/Installation_instructions_for_macOS) +* [Installation instructions for Windows](http://luarocks.org/en/Installation_instructions_for_Windows) + +## License + +LuaRocks is free software and uses the [MIT license](http://luarocks.org/en/License), the same as Lua 5.x. + +[1]: http://luarocks.org/en/Types_of_rocks +[2]: http://luarocks.org/en/Dependencies +[3]: http://luarocks.org/en/Rocks_repositories diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..de2b983 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +The LuaRocks project supports the _latest version_ of the tool +for bugfixes and security updates. In other words, if an +issue is reported and we produce a fix, it will appear in a subsequent +patch version (x.y.Z) of the tool, but we do not backport fixes +to previous minor (x.Y.z) or major (X.y.z) versions. + +## Reporting a Vulnerability + +To report a vulnerability on the LuaRocks CLI tool, email +Hisham Muhammad at hisham@luarocks.org. + +To report a vulnerability on the https://luarocks.org website, +email Leaf Corcoran at leafot@gmail.com. + +We will acknowledge your contact as soon as the message is +received, then assess the vulnerability and get back to you +with further feedback once analysis on our end is done. diff --git a/binary/Makefile.windows b/binary/Makefile.windows new file mode 100644 index 0000000..e749cb0 --- /dev/null +++ b/binary/Makefile.windows @@ -0,0 +1,76 @@ + +# "i686-w64-mingw32" or "x86_64-w64-mingw32" +MINGW_PREFIX?=i686-w64-mingw32 +# sysroot of your mingw-w64 installation +MINGW_SYSROOT=/usr/lib/mingw-w64-sysroot/$(MINGW_PREFIX) +# "mingw" or "mingw64" +OPENSSL_PLATFORM=mingw +# Versions of dependencies +LIBLUA_VERSION=5.4.3 +OPENSSL_VERSION=1.0.2o +ZLIB_VERSION=1.3.1 +BZIP2_VERSION=1.0.6 + +WINDOWS_DEPS_DIR=windows-deps-$(MINGW_PREFIX) +BUILD_WINDOWS_DEPS_DIR=build-windows-deps-$(MINGW_PREFIX) +BUILD_WINDOWS_BINARY_DIR=build-windows-binary-$(MINGW_PREFIX) + +windows-binary: $(WINDOWS_DEPS_DIR)/lib/liblua.a $(WINDOWS_DEPS_DIR)/lib/libssl.a $(WINDOWS_DEPS_DIR)/lib/libz.a $(WINDOWS_DEPS_DIR)/lib/libbz2.a + STATIC_GCC_AR=$(MINGW_PREFIX)-ar \ + STATIC_GCC_RANLIB=$(MINGW_PREFIX)-ranlib \ + STATIC_GCC_CC=$(MINGW_PREFIX)-gcc \ + LUAROCKS_CROSS_COMPILING=1 \ + $(MAKE) binary LUA_DIR=$(CURDIR)/$(WINDOWS_DEPS_DIR) CC=$(MINGW_PREFIX)-gcc NM=$(MINGW_PREFIX)-nm BINARY_PLATFORM=windows buildbinarydir=$(BUILD_WINDOWS_BINARY_DIR) BINARY_SYSROOT=$(MINGW_SYSROOT) + +$(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION).tar.gz: + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR) && curl -OL https://www.lua.org/ftp/lua-$(LIBLUA_VERSION).tar.gz +$(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION): $(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION).tar.gz + cd $(BUILD_WINDOWS_DEPS_DIR) && tar zxvpf lua-$(LIBLUA_VERSION).tar.gz +$(WINDOWS_DEPS_DIR)/lib/liblua.a: $(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION) + $(MAKE) -C "$(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION)/src" LUA_A=liblua.a CC=$(MINGW_PREFIX)-gcc AR="$(MINGW_PREFIX)-ar rcu" RANLIB=$(MINGW_PREFIX)-ranlib SYSCFLAGS= SYSLIBS= SYSLDFLAGS= liblua.a + mkdir -p $(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION)/src && cp lauxlib.h lua.h lua.hpp luaconf.h lualib.h ../../../$(WINDOWS_DEPS_DIR)/include + mkdir -p $(WINDOWS_DEPS_DIR)/lib + cd $(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION)/src && cp liblua.a ../../../$(WINDOWS_DEPS_DIR)/lib + +$(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION).tar.gz: + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR) && curl -OL https://www.openssl.org/source/openssl-$(OPENSSL_VERSION).tar.gz +$(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION): $(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION).tar.gz + cd $(BUILD_WINDOWS_DEPS_DIR) && tar zxvpf openssl-$(OPENSSL_VERSION).tar.gz +$(WINDOWS_DEPS_DIR)/lib/libssl.a: $(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION) + cd $(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION) && ./Configure --prefix=$(CURDIR)/$(WINDOWS_DEPS_DIR) --cross-compile-prefix=$(MINGW_PREFIX)- $(OPENSSL_PLATFORM) + $(MAKE) -C "$(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION)" + $(MAKE) -C "$(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION)" install_sw + +$(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION).tar.gz: + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR) && curl -OL https://www.zlib.net/zlib-$(ZLIB_VERSION).tar.gz +$(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION): $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION).tar.gz + cd $(BUILD_WINDOWS_DEPS_DIR) && tar zxvpf zlib-$(ZLIB_VERSION).tar.gz +$(WINDOWS_DEPS_DIR)/lib/libz.a: $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && sed -ie "s,dllwrap,$(MINGW_PREFIX)-dllwrap," win32/Makefile.gcc + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && ./configure --prefix=$(CURDIR)/$(WINDOWS_DEPS_DIR) --static + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && $(MAKE) -f win32/Makefile.gcc CC=$(MINGW_PREFIX)-gcc AR=$(MINGW_PREFIX)-ar RC=$(MINGW_PREFIX)-windres STRIP=$(MINGW_PREFIX)-strip IMPLIB=libz.dll.a + mkdir -p $(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && cp zlib.h zconf.h ../../$(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && $(MINGW_PREFIX)-strip -g libz.a + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && cp libz.a ../../$(WINDOWS_DEPS_DIR)/lib + +$(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION).tar.gz: + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR) && curl -OL http://downloads.sourceforge.net/project/bzip2/bzip2-$(BZIP2_VERSION).tar.gz +$(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION): $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION).tar.gz + cd $(BUILD_WINDOWS_DEPS_DIR) && tar zxvpf bzip2-$(BZIP2_VERSION).tar.gz +$(WINDOWS_DEPS_DIR)/lib/libbz2.a: $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION) + $(MAKE) -C "$(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION)" libbz2.a CC=$(MINGW_PREFIX)-gcc AR=$(MINGW_PREFIX)-ar RANLIB=$(MINGW_PREFIX)-ranlib + mkdir -p $(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION) && cp bzlib.h ../../$(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION) && $(MINGW_PREFIX)-strip -g libbz2.a + mkdir -p $(WINDOWS_DEPS_DIR)/lib + cd $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION) && cp libbz2.a ../../$(WINDOWS_DEPS_DIR)/lib + +windows-clean: + rm -rf $(WINDOWS_DEPS_DIR) $(BUILD_WINDOWS_BINARY_DIR) diff --git a/binary/all_in_one b/binary/all_in_one new file mode 100755 index 0000000..9b675eb --- /dev/null +++ b/binary/all_in_one @@ -0,0 +1,503 @@ +#!/usr/bin/env lua +--[[ + +All-in-one packager for LuaRocks + * by Hisham Muhammad + * licensed under the same terms as Lua (MIT license). + +Based on: + +* srlua.c - Lua interpreter for self-running programs + * by Luiz Henrique de Figueiredo + * 03 Nov 2014 15:31:43 + * srlua.c is placed in the public domain. +* bin2c.lua - converts a binary to a C string that can be embedded + * by Mark Edgar + * http://lua-users.org/wiki/BinTwoCee + * bin2c.lua is licensed under the same terms as Lua (MIT license). +* lua.c - Lua stand-alone interpreter + * by Luiz Henrique de Figueiredo, Waldemar Celes, Roberto Ierusalimschy + * lua.c is licensed under the same terms as Lua (MIT license). +* luastatic - builds a standalone executable from a Lua program + * by Eric R. Schulz + * https://github.com/ers35/luastatic + * luastatic is licensed under the CC0 1.0 Universal license + +]] + +local MAIN_PROGRAM = arg[1] or "src/bin/luarocks" +local LUA_DIR = arg[2] or "/usr" +local EXCLUDE = arg[3] or "^src/luarocks/admin/" +local SYSCONFDIR = arg[4] or "/etc/luarocks" +local TARGET_DIR = arg[5] or "build-binary" +local FORCE_CONFIG = (arg[6] == "yes") +local MY_PLATFORM = arg[7] or "unix" +local CC = arg[8] or "gcc" +local NM = arg[9] or "nm" +local CROSSCOMPILER_SYSROOT = arg[10] or "/usr/lib/mingw-w64-sysroot/i686-w64-mingw32" +local TRIPLET = arg[11] or CROSSCOMPILER_SYSROOT:gsub(".*/", "") +local PROCESSOR = arg[12] or TRIPLET:gsub("%-.*", "") +if PROCESSOR == "i686" then + PROCESSOR = "x86" +end + +local LUA_MODULES = TARGET_DIR .. "/lua_modules" +local CONFIG_DIR = TARGET_DIR .. "/.luarocks" + +package.path = "./src/?.lua;" .. package.path + +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local cmd = require("luarocks.cmd") +local deps = require("luarocks.deps") +local path = require("luarocks.path") +local manif = require("luarocks.manif") +local queries = require("luarocks.queries") +local persist = require("luarocks.persist") +local sysdetect = require("luarocks.core.sysdetect") + +-------------------------------------------------------------------------------- + +local function if_platform(plat, val) + if MY_PLATFORM == plat then + return val + end +end + +local function reindent_c(input) + local out = {} + local indent = 0 + local previous_is_blank = true + for line in input:gmatch("([^\n]*)") do + line = line:match("^[ \t]*(.-)[ \t]*$") + + local is_blank = (#line == 0) + local do_print = + (not is_blank) or + (not previous_is_blank and indent == 0) + + if line:match("^[})]") then + indent = indent - 1 + if indent < 0 then indent = 0 end + end + if do_print then + table.insert(out, string.rep(" ", indent)) + table.insert(out, line) + table.insert(out, "\n") + end + if line:match("[{(]$") then + indent = indent + 1 + end + + previous_is_blank = is_blank + end + return table.concat(out) +end + +local hexdump +do + local numtab = {} + for i = 0, 255 do + numtab[string.char(i)] = ("%-3d,"):format(i) + end + function hexdump(str) + return (str:gsub(".", numtab):gsub(("."):rep(80), "%0\n")) + end +end + +local c_preamble = [[ + +#include +#include +#include +#include +#include +#include + +/* portable alerts, from srlua */ +#ifdef _WIN32 +#include +#define alert(message) MessageBox(NULL, message, progname, MB_ICONERROR | MB_OK) +#define getprogname() char name[MAX_PATH]; argv[0]= GetModuleFileName(NULL,name,sizeof(name)) ? name : NULL; +#else +#define alert(message) fprintf(stderr,"%s: %s\n", progname, message) +#define getprogname() +#endif + +static int registry_key; + +/* fatal error, from srlua */ +static void fatal(const char* message) { + alert(message); + exit(EXIT_FAILURE); +} + +]] + +local function bin2c_file(out, filename) + local fd = io.open(filename, "rb") + local content = fd:read("*a"):gsub("^#![^\n]+\n", "") + fd:close() + table.insert(out, ("static const unsigned char code[] = {")) + table.insert(out, hexdump(content)) + table.insert(out, ("};")) +end + +local function write_hardcoded_module(dir) + + local system, processor + if if_platform("unix", true) then + system, processor = sysdetect.detect() + else + system, processor = "windows", PROCESSOR + end + + local hardcoded = { + SYSTEM = system, + PROCESSOR = processor, + FORCE_CONFIG = FORCE_CONFIG, + IS_BINARY = true, + + SYSCONFDIR = if_platform("unix", SYSCONFDIR), + } + + local name = dir .. "/luarocks/core/hardcoded.lua" + persist.save_as_module(name, hardcoded) + return name +end + +local function declare_modules(out, dirs, skip) + skip = skip or {} + table.insert(out, [[ + static void declare_modules(lua_State* L) { + lua_settop(L, 0); /* */ + lua_newtable(L); /* modules */ + lua_pushlightuserdata(L, (void*) ®istry_key); /* modules registry_key */ + lua_pushvalue(L, 1); /* modules registry_key modules */ + lua_rawset(L, LUA_REGISTRYINDEX); /* modules */ + ]]) + for _, dir in ipairs(dirs) do + for _, name in ipairs(fs.find(dir)) do + local run = true + for _, pat in ipairs(skip) do + if name:match(pat) then + run = false + break + end + end + if run then + local filename = dir .. "/" .. name + if fs.is_file(filename) then + print(name) + local modname = name:gsub("%.lua$", ""):gsub("/", ".") + table.insert(out, ("/* %s */"):format(modname)) + table.insert(out, ("{")) + bin2c_file(out, filename) + table.insert(out, ("luaL_loadbuffer(L, code, sizeof(code), %q);"):format(filename)) + table.insert(out, ("lua_setfield(L, 1, %q);"):format(modname)) + table.insert(out, ("}")) + end + end + end + end + table.insert(out, [[ + lua_settop(L, 0); /* */ + } + ]]) +end + +local function nm(filename) + local pd = io.popen(NM .. " " .. filename) + local out = pd:read("*a") + pd:close() + return out +end + +local function declare_libraries(out, dir) + local a_files = {} + local externs = {} + local fn = {} + table.insert(fn, [[ + static void declare_libraries(lua_State* L) { + lua_getglobal(L, "package"); /* package */ + lua_getfield(L, -1, "preload"); /* package package.preload */ + ]]) + for _, name in ipairs(fs.find(dir)) do + local filename = dir .. "/" .. name + if name:match("%.a$") then + table.insert(a_files, filename) + local nmout = nm(filename) + for luaopen in nmout:gmatch("[^dD] _?(luaopen_[%a%p%d]+)") do + + -- FIXME what about module names with underscores? + local modname = luaopen:gsub("^_?luaopen_", ""):gsub("_", ".") + + table.insert(externs, "extern int " .. luaopen .. "(lua_State* L);") + table.insert(fn, "lua_pushcfunction(L, " .. luaopen .. ");") + table.insert(fn, "lua_setfield(L, -2, \"" .. modname .. "\");") + end + end + end + local pd = io.popen("find " .. dir .. " -name '*.a'", "r") + for line in pd:lines() do + table.insert(a_files, line) + end + pd:close() + table.insert(fn, [[ + lua_settop(L, 0); /* */ + } + ]]) + + table.insert(out, "\n") + for _, line in ipairs(externs) do + table.insert(out, line) + end + table.insert(out, "\n") + for _, line in ipairs(fn) do + table.insert(out, line) + end + table.insert(out, "\n") + + return a_files +end + +local function load_main(out, main_program, program_name) + table.insert(out, [[static void load_main(lua_State* L) {]]) + bin2c_file(out, main_program) + table.insert(out, ("if(luaL_loadbuffer(L, code, sizeof(code), %q) != LUA_OK) {"):format(program_name)) + table.insert(out, (" fatal(lua_tostring(L, -1));")) + table.insert(out, ("}")) + table.insert(out, [[}]]) + table.insert(out, [[]]) +end + +local c_main = [[ + +/* custom package loader */ +static int pkg_loader(lua_State* L) { + lua_pushlightuserdata(L, (void*) ®istry_key); /* modname ? registry_key */ + lua_rawget(L, LUA_REGISTRYINDEX); /* modname ? modules */ + lua_pushvalue(L, -1); /* modname ? modules modules */ + lua_pushvalue(L, 1); /* modname ? modules modules modname */ + lua_gettable(L, -2); /* modname ? modules mod */ + if (lua_type(L, -1) == LUA_TNIL) { + lua_pop(L, 1); /* modname ? modules */ + lua_pushvalue(L, 1); /* modname ? modules modname */ + lua_pushliteral(L, ".init"); /* modname ? modules modname ".init" */ + lua_concat(L, 2); /* modname ? modules modname..".init" */ + lua_gettable(L, -2); /* modname ? mod */ + } + return 1; +} + +static void install_pkg_loader(lua_State* L) { + lua_settop(L, 0); /* */ + lua_getglobal(L, "table"); /* table */ + lua_getfield(L, -1, "insert"); /* table table.insert */ + lua_getglobal(L, "package"); /* table table.insert package */ + lua_getfield(L, -1, "searchers"); /* table table.insert package package.searchers */ + if (lua_type(L, -1) == LUA_TNIL) { + lua_pop(L, 1); + lua_getfield(L, -1, "loaders"); /* table table.insert package package.loaders */ + } + lua_copy(L, 4, 3); /* table table.insert package.searchers */ + lua_settop(L, 3); /* table table.insert package.searchers */ + lua_pushnumber(L, 1); /* table table.insert package.searchers 1 */ + lua_pushcfunction(L, pkg_loader); /* table table.insert package.searchers 1 pkg_loader */ + lua_call(L, 3, 0); /* table */ + lua_settop(L, 0); /* */ +} + +/* main script launcher, from srlua */ +static int pmain(lua_State *L) { + int argc = lua_tointeger(L, 1); + char** argv = lua_touserdata(L, 2); + int i; + load_main(L); + lua_createtable(L, argc, 0); + for (i = 0; i < argc; i++) { + lua_pushstring(L, argv[i]); + lua_rawseti(L, -2, i); + } + lua_setglobal(L, "arg"); + luaL_checkstack(L, argc - 1, "too many arguments to script"); + for (i = 1; i < argc; i++) { + lua_pushstring(L, argv[i]); + } + lua_call(L, argc - 1, 0); + return 0; +} + +/* error handler, from luac */ +static int msghandler (lua_State *L) { + /* is error object not a string? */ + const char *msg = lua_tostring(L, 1); + if (msg == NULL) { + /* does it have a metamethod that produces a string */ + if (luaL_callmeta(L, 1, "__tostring") && lua_type(L, -1) == LUA_TSTRING) { + /* then that is the message */ + return 1; + } else { + msg = lua_pushfstring(L, "(error object is a %s value)", luaL_typename(L, 1)); + } + } + /* append a standard traceback */ + luaL_traceback(L, L, msg, 1); + return 1; +} + +/* main function, from srlua */ +int main(int argc, char** argv) { + lua_State* L; + getprogname(); + if (argv[0] == NULL) { + fatal("cannot locate this executable"); + } + L = luaL_newstate(); + if (L == NULL) { + fatal("not enough memory for state"); + } + luaL_openlibs(L); + install_pkg_loader(L); + declare_libraries(L); + declare_modules(L); + lua_pushcfunction(L, &msghandler); + lua_pushcfunction(L, &pmain); + lua_pushinteger(L, argc); + lua_pushlightuserdata(L, argv); + if (lua_pcall(L, 2, 0, -4) != 0) { + fatal(lua_tostring(L, -1)); + } + lua_close(L); + return EXIT_SUCCESS; +} + +]] + +local function filter_in(f, xs) + for i = #xs, 1, -1 do + if not f(xs[i]) then + table.remove(xs, i) + end + end + return xs +end + +local function nonnull(x) return x ~= nil end + +local function generate(main_program, dir, skip) + local program_name = main_program:gsub(".*/", "") + + local hardcoded = write_hardcoded_module(dir) + + local out = {} + table.insert(out, ([[static const char* progname = %q;]]):format(program_name)) + table.insert(out, c_preamble) + load_main(out, main_program, program_name) + local lua_modules = LUA_MODULES .. "/share/lua/" .. cfg.lua_version + declare_modules(out, { dir, lua_modules }, skip) + local a_files = declare_libraries(out, LUA_MODULES .. "/lib/lua/" .. cfg.lua_version) + table.insert(out, c_main) + + os.remove(hardcoded) + + local c_filename = TARGET_DIR .. "/" .. program_name .. ".exe.c" + local fd = io.open(c_filename, "w") + fd:write(reindent_c(table.concat(out, "\n"))) + fd:close() + + assert(deps.check_lua_incdir(cfg.variables)) + assert(deps.check_lua_libdir(cfg.variables)) + + cmd = table.concat(filter_in(nonnull, { + CC, "-o", TARGET_DIR .. "/" .. program_name .. ".exe", + "-I", cfg.variables.LUA_INCDIR, + if_platform("unix", "-rdynamic"), + "-Os", + c_filename, + "-L", cfg.variables.LUA_LIBDIR, + table.concat(a_files, " "), + --if_platform("unix", cfg.variables.LUA_LIBDIR .. "/" .. cfg.variables.LUALIB:gsub("%.so.*$", ".a")), + --if_platform("windows", "mingw/liblua.a"), -- FIXME + cfg.variables.LUA_LIBDIR .. "/" .. cfg.variables.LUALIB:gsub("%.so.*$", ".a"), + if_platform("unix", "-ldl"), + if_platform("unix", "-lpthread"), + if_platform("windows", "-mconsole -mwindows"), + "-lm" + }), " ") + print(cmd) + os.execute(cmd) +end + +-------------------------------------------------------------------------------- + +local function main() + + os.remove("src/luarocks/core/hardcoded.lua") + cfg.init() + cfg.variables.LUA_DIR = LUA_DIR + cfg.variables.LUA_INCDIR = nil -- let it autodetect later + cfg.variables.LUA_LIBDIR = nil -- let it autodetect later + fs.init() + path.use_tree(LUA_MODULES) + + local CONFIG_FILE = CONFIG_DIR .. "/config-" .. cfg.lua_version .. ".lua" + + fs.make_dir(CONFIG_DIR) + + persist.save_from_table(CONFIG_FILE, { + lib_extension = "a", + external_lib_extension = "a", + variables = { + CC = fs.current_dir() .. "/binary/static-gcc", + LD = fs.current_dir() .. "/binary/static-gcc", + LIB_EXTENSION = "a", + LUA_DIR = LUA_DIR, + LIBFLAG = "-static", + PWD = "pwd", + MKDIR = "mkdir", + }, + platforms = if_platform("windows", { "windows", "win32", "mingw32" }), + external_deps_dirs = if_platform("windows", { CROSSCOMPILER_SYSROOT, fs.current_dir() .. "/windows-deps-" .. TRIPLET }), + }) + + local dependencies = { + md5 = "md5", + luasocket = "./binary/luasocket-3.1.0-1.rockspec", + luasec = "./binary/luasec-1.3.2-1.rockspec", + ["lua-zlib"] = "./binary/lua-zlib-1.2-0.rockspec", + ["lua-bz2"] = "./binary/lua-bz2-0.2.1-1.rockspec", + luaposix = if_platform("unix", "./binary/luaposix-35.1-1.rockspec"), + luafilesystem = "luafilesystem", + } + + local dependency_order = { + "md5", + "luasocket", "luasec", + "lua-zlib", + "lua-bz2", + "luaposix", + "luafilesystem", + } + + fs.make_dir(LUA_MODULES) + for _, name in ipairs(dependency_order) do + local use = dependencies[name] + if use then + print("----------------------------------------------------------------") + print(name) + print("----------------------------------------------------------------") + local vers = manif.get_versions(queries.from_dep_string(name), "one") + if not next(vers) then + local ok = os.execute("LUAROCKS_CONFIG='" .. CONFIG_FILE .. "' ./luarocks install --no-project '--tree=" .. LUA_MODULES .. "' " .. use) + if ok ~= 0 and ok ~= true then + error("Failed building dependency: " .. name) + end + end + end + end + + generate(MAIN_PROGRAM, "src", { EXCLUDE, "^bin/?" }) +end + +main() diff --git a/binary/lua-bz2-0.2.1-1.rockspec b/binary/lua-bz2-0.2.1-1.rockspec new file mode 100644 index 0000000..ec582b4 --- /dev/null +++ b/binary/lua-bz2-0.2.1-1.rockspec @@ -0,0 +1,44 @@ +package = "lua-bz2" +version = "0.2.1-1" +source = { + url = "git+https://github.com/hishamhm/lua-bz2.git", + tag = "0.2.1", +} +description = { + summary = "A Lua binding to Julian Seward's libbzip2", + detailed = [[ + Support for reading and writing .bz2 files + and handling streams compressed in bzip2 format. + ]], + homepage = "https://github.com/harningt/lua-bz2", + license = "ISC" +} +external_dependencies = { + BZ2 = { + library = "bz2" + } +} +build = { + type = "builtin", + modules = { + bz2 = { + incdirs = { + "$(BZ2_INCDIR)" + }, + libdirs = { + "$(BZ2_LIBDIR)" + }, + libraries = { + "bz2" + }, + sources = { + "lbz.c", + "lbz2_common.c", + "lbz2_file_reader.c", + "lbz2_file_writer.c", + "lbz2_stream.c", + } + }, + ["bz2.ltn12"] = "bz2/ltn12.lua", + } +} diff --git a/binary/lua-zlib-1.2-0.rockspec b/binary/lua-zlib-1.2-0.rockspec new file mode 100644 index 0000000..9d3adc8 --- /dev/null +++ b/binary/lua-zlib-1.2-0.rockspec @@ -0,0 +1,39 @@ +package = "lua-zlib" +version = "1.2-0" +source = { + url = "git+https://github.com/brimworks/lua-zlib.git", + tag = "v1.2", +} +description = { + summary = "Simple streaming interface to zlib for Lua.", + detailed = [[ + Simple streaming interface to zlib for Lua. + Consists of two functions: inflate and deflate. + Both functions return "stream functions" (takes a buffer of input and returns a buffer of output). + This project is hosted on github. + ]], + homepage = "https://github.com/brimworks/lua-zlib", + license = "MIT" +} +dependencies = { + "lua >= 5.1, <= 5.4" +} +external_dependencies = { + ZLIB = { + header = "zlib.h", + library = "z", + } +} + +build = { + type = "builtin", + modules = { + zlib = { + sources = { "lua_zlib.c" }, + libraries = { "z" }, + defines = { "LZLIB_COMPAT" }, + incdirs = { "$(ZLIB_INCDIR)" }, + libdirs = { "$(ZLIB_LIBDIR)" }, + } + }, +} diff --git a/binary/luaposix-35.1-1.rockspec b/binary/luaposix-35.1-1.rockspec new file mode 100644 index 0000000..1940c75 --- /dev/null +++ b/binary/luaposix-35.1-1.rockspec @@ -0,0 +1,61 @@ +local _MODREV, _SPECREV = '35.1', '-1' + +package = 'luaposix' +version = _MODREV .. _SPECREV + +description = { + summary = 'Lua bindings for POSIX', + detailed = [[ + A library binding various POSIX APIs. POSIX is the IEEE Portable + Operating System Interface standard. luaposix is based on lposix. + ]], + homepage = 'http://github.com/luaposix/luaposix/', + license = 'MIT/X11', +} + +dependencies = { + 'lua >= 5.1, < 5.5', +} + +do + -- We only want to install a bit32 module for Lua 5.1. + local _ENV={package=nil, dependencies=dependencies} + if package then + dependencies[#dependencies + 1] = 'bit32' + end +end + +source = { + url = 'http://github.com/luaposix/luaposix/archive/v' .. _MODREV .. '.zip', + dir = 'luaposix-' .. _MODREV, +} + +build = { + type = 'command', + build_command = '$(LUA) build-aux/luke' + .. ' package="' .. package .. '"' + .. ' version="' .. _MODREV .. '"' + .. ' PREFIX="$(PREFIX)"' + .. ' LUA="$(LUA)"' + .. ' LUA_INCDIR="$(LUA_INCDIR)"' + .. ' CFLAGS="$(CFLAGS)"' + .. ' LIBFLAG="$(LIBFLAG)"' + .. ' LIB_EXTENSION="$(LIB_EXTENSION)"' + .. ' OBJ_EXTENSION="$(OBJ_EXTENSION)"' + .. ' INST_LIBDIR="$(LIBDIR)"' + .. ' INST_LUADIR="$(LUADIR)"' + , + install_command = '$(LUA) build-aux/luke install --quiet' + .. ' INST_LIBDIR="$(LIBDIR)"' + .. ' LIB_EXTENSION="$(LIB_EXTENSION)"' + .. ' INST_LUADIR="$(LUADIR)"' + , +} + +if _MODREV == 'git' then + dependencies[#dependencies + 1] = 'ldoc' + + source = { + url = 'git://github.com/luaposix/luaposix.git', + } +end diff --git a/binary/luasec-1.3.2-1.rockspec b/binary/luasec-1.3.2-1.rockspec new file mode 100644 index 0000000..81cb31f --- /dev/null +++ b/binary/luasec-1.3.2-1.rockspec @@ -0,0 +1,98 @@ +package = "LuaSec" +version = "1.3.2-1" +source = { + url = "git+https://github.com/brunoos/luasec", + tag = "v1.3.2", +} +description = { + summary = "A binding for OpenSSL library to provide TLS/SSL communication over LuaSocket.", + detailed = "This version delegates to LuaSocket the TCP connection establishment between the client and server. Then LuaSec uses this connection to start a secure TLS/SSL session.", + homepage = "https://github.com/brunoos/luasec/wiki", + license = "MIT" +} +dependencies = { + "lua >= 5.1", "luasocket" +} +external_dependencies = { + platforms = { + unix = { + OPENSSL = { + header = "openssl/ssl.h", + library = "ssl" + } + }, + windows = { + OPENSSL = { + header = "openssl/ssl.h", + } + }, + mingw32 = { + OPENSSL = { + library = "ssl", + } + }, + } +} +build = { + type = "builtin", + copy_directories = { + "samples" + }, + platforms = { + unix = { + modules = { + ['ssl.https'] = "src/https.lua", + ['ssl.init'] = "src/ssl.lua", + ssl = { + defines = { + "WITH_LUASOCKET", "LUASOCKET_DEBUG", + }, + incdirs = { + "$(OPENSSL_INCDIR)", "src/", "src/luasocket", + }, + libdirs = { + "$(OPENSSL_LIBDIR)" + }, + libraries = { + "ssl", "crypto" + }, + sources = { + "src/options.c", "src/config.c", "src/ec.c", + "src/x509.c", "src/context.c", "src/ssl.c", + "src/luasocket/buffer.c", "src/luasocket/io.c", + "src/luasocket/usocket.c" -- , "src/luasocket/timeout.c" + } + } + } + }, + windows = { + modules = { + ['ssl.https'] = "src/https.lua", + ['ssl.init'] = "src/ssl.lua", + ssl = { + defines = { + "WIN32", "NDEBUG", "_WINDOWS", "_USRDLL", "LSEC_EXPORTS", "BUFFER_DEBUG", "LSEC_API=__declspec(dllexport)", + "WITH_LUASOCKET", "LUASOCKET_DEBUG", + "LUASEC_INET_NTOP", "WINVER=0x0501", "_WIN32_WINNT=0x0501", "NTDDI_VERSION=0x05010300" + }, + libdirs = { + "$(OPENSSL_LIBDIR)", + "$(OPENSSL_BINDIR)", + }, + libraries = { + "ssl", "crypto", "ws2_32" + }, + incdirs = { + "$(OPENSSL_INCDIR)", "src/", "src/luasocket" + }, + sources = { + "src/options.c", "src/config.c", "src/ec.c", + "src/x509.c", "src/context.c", "src/ssl.c", + "src/luasocket/buffer.c", "src/luasocket/io.c", + "src/luasocket/timeout.c", "src/luasocket/wsocket.c" + }, + }, + }, + }, + } +} diff --git a/binary/luasocket-3.1.0-1.rockspec b/binary/luasocket-3.1.0-1.rockspec new file mode 100644 index 0000000..dd9e636 --- /dev/null +++ b/binary/luasocket-3.1.0-1.rockspec @@ -0,0 +1,135 @@ +package = "LuaSocket" +version = "3.1.0-1" +source = { + url = "git+https://github.com/lunarmodules/luasocket.git", + tag = "v3.1.0" +} +description = { + summary = "Network support for the Lua language", + detailed = [[ + LuaSocket is a Lua extension library composed of two parts: a set of C + modules that provide support for the TCP and UDP transport layers, and a + set of Lua modules that provide functions commonly needed by applications + that deal with the Internet. + ]], + homepage = "https://github.com/lunarmodules/luasocket", + license = "MIT" +} +dependencies = { + "lua >= 5.1" +} + +local function make_plat(plat) + local defines = { + unix = { + "LUASOCKET_DEBUG" + }, + macosx = { + "LUASOCKET_DEBUG", + "UNIX_HAS_SUN_LEN" + }, + win32 = { + "LUASOCKET_DEBUG", + "NDEBUG" + }, + mingw32 = { + "LUASOCKET_DEBUG", + "LUASOCKET_INET_PTON", + "WINVER=0x0501" + } + } + local modules = { + ["socket.core"] = { + sources = { + "src/luasocket.c" + , "src/timeout.c" + , "src/buffer.c" + , "src/io.c" + , "src/auxiliar.c" + , "src/options.c" + , "src/inet.c" + , "src/except.c" + , "src/select.c" + , "src/tcp.c" + , "src/udp.c" + , "src/compat.c" }, + defines = defines[plat], + incdir = "/src" + }, + ["mime.core"] = { + sources = { "src/mime.c", "src/compat.c" }, + defines = defines[plat], + incdir = "/src" + }, + ["socket.http"] = "src/http.lua", + ["socket.url"] = "src/url.lua", + ["socket.tp"] = "src/tp.lua", + ["socket.ftp"] = "src/ftp.lua", + ["socket.headers"] = "src/headers.lua", + ["socket.smtp"] = "src/smtp.lua", + ltn12 = "src/ltn12.lua", + socket = "src/socket.lua", + mime = "src/mime.lua" + } + if plat == "unix" + or plat == "macosx" + or plat == "haiku" + then + modules["socket.core"].sources[#modules["socket.core"].sources+1] = "src/usocket.c" + if plat == "haiku" then + modules["socket.core"].libraries = {"network"} + end + modules["socket.unix"] = { + sources = { + "src/buffer.c" + , "src/compat.c" + , "src/auxiliar.c" + , "src/options.c" + , "src/timeout.c" + , "src/io.c" + , "src/usocket.c" + , "src/unix.c" + , "src/unixdgram.c" + , "src/unixstream.c" }, + defines = defines[plat], + incdir = "/src" + } + modules["socket.serial"] = { + sources = { + "src/buffer.c" + , "src/compat.c" + , "src/auxiliar.c" + , "src/options.c" + , "src/timeout.c" + , "src/io.c" + , "src/usocket.c" + , "src/serial.c" }, + defines = defines[plat], + incdir = "/src" + } + end + if plat == "win32" + or plat == "mingw32" + then + modules["socket.core"].sources[#modules["socket.core"].sources+1] = "src/wsocket.c" + modules["socket.core"].libraries = { "ws2_32" } + modules["socket.core"].libdirs = {} + end + return { modules = modules } +end + +build = { + type = "builtin", + platforms = { + unix = make_plat("unix"), + macosx = make_plat("macosx"), + haiku = make_plat("haiku"), + win32 = make_plat("win32"), + mingw32 = make_plat("mingw32") + }, + copy_directories = { + "docs" + , "samples" + , "etc" + , "test" } +} diff --git a/binary/static-gcc b/binary/static-gcc new file mode 100755 index 0000000..1e4ad0e --- /dev/null +++ b/binary/static-gcc @@ -0,0 +1,170 @@ +#!/usr/bin/env bash + +STATIC_GCC_AR=${STATIC_GCC_AR:-ar} +STATIC_GCC_RANLIB=${STATIC_GCC_RANLIB:-ranlib} +STATIC_GCC_CC=${STATIC_GCC_CC:-gcc} + +DIR="$( cd "$( dirname "$0" )" && pwd )" + +function log() { echo -- "$@" >> $DIR/log.txt; } + +function runlog() { log "$@"; "$@"; } + +log "---------------------------" +log INP "$@" + +allargs=() +sources=() +objects=() +etc=() +libdirs=($("$STATIC_GCC_CC" -print-search-dirs | grep libraries | cut -d= -f2 | tr ':' '\n')) +incdirs=() + +linking=0 + +while [ "$1" ] +do + allargs+=("$1") + if [ "$next_libdir" = "1" ] + then + libdirs+=("$1") + next_libdir=0 + elif [ "$next_incdir" = "1" ] + then + incdirs+=("-I$1") + next_incdir=0 + elif [ "$next_lib" = "1" ] + then + libs+=("$1") + next_lib=0 + elif [ "$next_output" = "1" ] + then + output="$1" + next_output=0 + else + case "$1" in + -*) + case "$1" in + -shared) + linking=1 + ;; + -static) + linking=1 + ;; + -o) + next_output=1 + ;; + -c) + object=1 + etc+=("$1") + ;; + -L) + next_libdir=1 + ;; + -L*) + libdirs+=("${1:2}") + ;; + -I) + next_incdir=1 + ;; + -I*) + incdirs+=("$1") + ;; + -l) + next_lib=1 + ;; + -l*) + libs+=("${1:2}") + ;; + *) + etc+=("$1") + ;; + esac + ;; + *.c) + sources+=("$1") + ;; + *.o) + objects+=("$1") + ;; + *) + etc+=("$1") + ;; + esac + fi + shift +done + +staticlibs=() +for lib in "${libs[@]}" +do + found=0 + for libdir in "${libdirs[@]}" + do + staticlib="$libdir/lib$lib.a" + if [ -e "$staticlib" ] + then + staticlibs+=("$staticlib") + found=1 + break + fi + done + if [ "$found" = 0 ] + then + log "STATICLIB not found for $lib" + runlog exit 1 + fi +done + +oflag=() +if [ "$output" != "" ] +then + oflag=("-o" "$output") +fi + +if [ "$linking" = "1" ] +then + log LINK + if [ "${#sources[@]}" -gt 0 ] + then + for source in "${sources[@]}" + do + object="${source%.c}.o" + runlog $STATIC_GCC_CC "${incdirs[@]}" "${etc[@]}" -c -o "$object" "$source" + [ "$?" = 0 ] || runlog exit $? + objects+=("$object") + done + fi + + # runlog ar rcu "${oflag[@]}" "${objects[@]}" "${staticlibs[@]}" + echo "CREATE $output" > ar.script + for o in "${objects[@]}" + do + echo "ADDMOD $o" >> ar.script + done + for o in "${staticlibs[@]}" + do + echo "ADDLIB $o" >> ar.script + done + echo "SAVE" >> ar.script + echo "END" >> ar.script + cat ar.script >> "$DIR/log.txt" + cat ar.script | $STATIC_GCC_AR -M + [ "$?" = 0 ] || runlog exit $? + + [ -e "$output" ] || { + exit 1 + } + + runlog $STATIC_GCC_RANLIB "$output" + runlog exit $? +elif [ "$object" = 1 ] +then + log OBJECT + runlog $STATIC_GCC_CC "${oflag[@]}" "${incdirs[@]}" "${etc[@]}" "${sources[@]}" + runlog exit $? +else + log EXECUTABLE + runlog $STATIC_GCC_CC "${allargs[@]}" + runlog exit $? +fi diff --git a/config.ld b/config.ld new file mode 100644 index 0000000..231a89f --- /dev/null +++ b/config.ld @@ -0,0 +1,6 @@ +file = "src/luarocks" +project = "LuaRocks" +dir = "docs/ldoc" +description = [[ +LuaRocks - a deployment and management system for Lua modules +]] diff --git a/configure b/configure new file mode 100755 index 0000000..6c831d4 --- /dev/null +++ b/configure @@ -0,0 +1,513 @@ +#!/bin/sh + +# Defaults + +prefix="/usr/local" +sysconfdir="$prefix/etc" +rocks_tree="$prefix" + +# ---------------------------------------------------------------------------- +# FUNCTION DEFINITIONS +# ---------------------------------------------------------------------------- + +# Utility functions +# ----------------- + +# Resolves a full path +# - alternative to "readlink -f", which is not available on solaris +# based on https://stackoverflow.com/a/6554854/1793220 +canonicalpath() { + oldpwd="$PWD" + if [ -d "$1" ] + then + if cd "$1" >/dev/null 2>&1 + then + echo "$PWD" + else + echo "$1" + fi + else + if cd "$(dirname "$1")" >/dev/null 2>&1 + then + if [ "$PWD" = "/" ] + then + echo "/$(basename "$1")" + else + echo "$PWD/$(basename "$1")" + fi + else + echo "$1" + fi + fi + cd "$oldpwd" >/dev/null 2>&1 || return +} + +find_program() { + prog=$(command -v "$1" 2>/dev/null) + if [ -n "$prog" ] + then + dirname "$prog" + fi +} + +die() { + echo "$*" + echo + RED "configure failed." + echo + echo + exit 1 +} + +echo_n() { + printf "%s" "$*" +} + +bold='\033[1m' +red='\033[1;31m' +green='\033[1;32m' +blue='\033[1;36m' +reset='\033[0m' + +BOLD() { + printf "$bold%s$reset" "$*" +} + +RED() { + printf "$red%s$reset" "$*" +} + +GREEN() { + printf "$green%s$reset" "$*" +} + +BLUE() { + printf "$blue%s$reset" "$*" +} + +# Help +# ---- + +show_help() { +cat < /dev/null) + if [ "$detected_lua" != "nil" ] + then + if [ "$LUA_VERSION_SET" != "yes" ] + then + echo "Lua version detected: $(GREEN "$detected_lua")" + LUA_VERSION=$detected_lua + return 0 + elif [ "$LUA_VERSION" = "$detected_lua" ] + then + return 0 + fi + fi + return 1 +} + +search_interpreter() { + name="$1" + lua_at="" + if [ "$LUA_BINDIR_SET" = "yes" ] + then + lua_at="$LUA_BINDIR" + elif [ "$LUA_DIR_SET" = "yes" ] + then + LUA_BINDIR="$LUA_DIR/bin" + if [ -f "$LUA_BINDIR/$name" ] + then + lua_at="$LUA_BINDIR" + fi + else + lua_at=$(find_program "$name") + fi + if [ -n "$lua_at" ] && [ -x "$lua_at/$name" ] + then + if detect_lua_version "$lua_at/$name" + then + LUA="$lua_at/$name" + echo "Lua interpreter found: $(GREEN "$LUA")" + if [ "$LUA_BINDIR_SET" != "yes" ] + then + LUA_BINDIR="$lua_at" + fi + if [ "$LUA_DIR_SET" != "yes" ] + then + LUA_DIR=$(dirname "$lua_at") + fi + + return 0 + fi + fi + return 1 +} + +# ---------------------------------------------------------------------------- +# MAIN PROGRAM +# ---------------------------------------------------------------------------- + +# Parse options + +while [ -n "$1" ] +do + value="$(echo "$1" | sed 's/[^=]*.\(.*\)/\1/')" + key="$(echo "$1" | sed 's/=.*//')" + if echo "$value" | grep "~" >/dev/null 2>/dev/null + then + echo + echo "$(RED WARNING:) the '~' sign is not expanded in flags." + echo "If you mean the home directory, use \$HOME instead." + echo + fi + case "$key" in + + # Help + # ---- + -h|--help) + show_help + exit 0 + ;; + + # Where to install LuaRocks: + # -------------------------- + --prefix) + [ -n "$value" ] || die "Missing value in flag $key." + prefix="$(canonicalpath "$value")" + prefix_SET=yes + ;; + --sysconfdir) + [ -n "$value" ] || die "Missing value in flag $key." + sysconfdir="$(canonicalpath "$value")" + sysconfdir_SET=yes + ;; + + + # Where to install files provided by rocks: + # ----------------------------------------- + --rocks-tree) + [ -n "$value" ] || die "Missing value in flag $key." + rocks_tree="$(canonicalpath "$value")" + rocks_tree_SET=yes + ;; + + # Where is your Lua interpreter: + # ------------------------------ + --lua-version|--with-lua-version) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_VERSION="$value" + case "$LUA_VERSION" in + 5.1|5.2|5.3|5.4) ;; + *) die "Invalid Lua version in flag $key." + esac + LUA_VERSION_SET=yes + ;; + --with-lua) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_DIR="$(canonicalpath "$value")" + [ -d "$LUA_DIR" ] || die "Bad value for --with-lua: $LUA_DIR is not a valid directory." + LUA_DIR_SET=yes + ;; + --with-lua-bin) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_BINDIR="$(canonicalpath "$value")" + [ -d "$LUA_BINDIR" ] || die "Bad value for --with-lua-bin: $LUA_BINDIR is not a valid directory." + LUA_BINDIR_SET=yes + ;; + --with-lua-include) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_INCDIR="$(canonicalpath "$value")" + [ -d "$LUA_INCDIR" ] || die "Bad value for --with-lua-include: $LUA_INCDIR is not a valid directory." + LUA_INCDIR_SET=yes + ;; + --with-lua-lib) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_LIBDIR="$(canonicalpath "$value")" + [ -d "$LUA_LIBDIR" ] || die "Bad value for --with-lua-lib: $LUA_LIBDIR is not a valid directory." + LUA_LIBDIR_SET=yes + ;; + --with-lua-interpreter) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_EXE="$value" + LUA_EXE_SET=yes + ;; + + # For specialized uses of LuaRocks: + # --------------------------------- + --force-config) + FORCE_CONFIG=yes + ;; + --disable-incdir-check) + DISABLE_INCDIR_CHECK=yes + ;; + + # Old options that no longer apply + # -------------------------------- + --versioned-rocks-dir) + echo "--versioned-rocks-dir is no longer necessary." + echo "The rocks tree in LuaRocks 3.0 is always versioned." + ;; + --lua-suffix) + echo "--lua-suffix is no longer necessary." + echo "The suffix is automatically detected." + ;; + + *) + die "Error: Unknown flag: $1" + ;; + esac + shift +done + +echo +BLUE "Configuring LuaRocks version 3.11.1..." +echo +echo + +# ---------------------------------------- +# Derive options from the ones given +# ---------------------------------------- + +if [ "$prefix_SET" = "yes" ] && [ ! "$sysconfdir_SET" = "yes" ] +then + if [ "$prefix" = "/usr" ] + then sysconfdir=/etc + else sysconfdir=$prefix/etc + fi + sysconfdir_SET=yes +fi + +if [ "$prefix_SET" = "yes" ] && [ ! "$rocks_tree_SET" = "yes" ] +then + rocks_tree=$prefix +fi + +# ---------------------------------------- +# Search for Lua +# ---------------------------------------- + +lua_interp_found=no + +case "$LUA_VERSION" in +5.1) + names="lua5.1 lua51 lua-5.1 lua-51 luajit lua" + ;; +5.2) + names="lua5.2 lua52 lua-5.2 lua-52 lua" + ;; +5.3) + names="lua5.3 lua53 lua-5.3 lua-53 lua" + ;; +5.4) + names="lua5.4 lua54 lua-5.4 lua-54 lua" + ;; +*) + names="lua5.4 lua54 lua-5.4 lua-54 lua5.3 lua53 lua-5.3 lua-53 lua5.2 lua52 lua-5.2 lua-52 lua5.1 lua51 lua-5.1 lua-51 luajit lua" + ;; +esac + +if [ "$LUA_EXE_SET" = "yes" ] +then + names="$LUA_EXE" +fi + +for name in $names +do + search_interpreter "$name" && { + lua_interp_found=yes + break + } +done + +if [ "$lua_interp_found" != "yes" ] +then + if [ "$LUA_VERSION_SET" ] + then + interp="Lua $LUA_VERSION" + else + interp="Lua" + fi + if [ "$LUA_DIR_SET" ] || [ "$LUA_BINDIR_SET" ] + then + where="$LUA_BINDIR" + else + where="\$PATH" + fi + echo "$(RED $interp interpreter not found) in $where" + echo "You may want to use the flags $(BOLD --with-lua), $(BOLD --with-lua-bin) and/or $(BOLD --lua-version)" + die "Run $(BOLD ./configure --help) for details." +fi + +if [ "$LUA_VERSION_SET" = "yes" ] +then + echo_n "Checking if $LUA is Lua version $LUA_VERSION... " + if detect_lua_version "$LUA" + then + echo "yes" + else + echo "no" + die "You may want to use the flags --with-lua, --with-lua-bin and/or --lua-version. See --help." + fi +fi + +# ---------------------------------------- +# Additional checks +# ---------------------------------------- + +check_incdir() { + if [ "$LUA_INCDIR_SET" = "yes" ] + then + incdir="$LUA_INCDIR" + else + incdir="$LUA_DIR/include" + fi + + tried="" + # Verification of the Lua include path happens at LuaRocks runtime, + # but we also perform it here just so that the user gets an early failure + # if they try to install LuaRocks with the Lua interpreter package + # but not the "development files" that many Linux distros ship separately. + for lua_h in \ + "$incdir/lua/$LUA_VERSION/lua.h" \ + "$incdir/lua$LUA_VERSION/lua.h" \ + "$incdir/lua-$LUA_VERSION/lua.h" \ + "$incdir/lua$(echo "$LUA_VERSION" | tr -d .)/lua.h" \ + "$incdir/lua.h" \ + $("$LUA" -e 'print(jit and [['"$incdir"'/luajit-]]..jit.version:match("(%d+%.%d+)")..[[/lua.h]] or "")') + do + if [ -f "$lua_h" ] + then + grep "LUA_VERSION_NUM.*$LUA_VERSION" "$lua_h" > /dev/null 2> /dev/null && return + fi + tried="$tried $lua_h" + done + + echo "$(RED "lua.h for Lua $LUA_VERSION not found") (tried$tried)" + echo + echo "If the development files for Lua (headers and libraries)" + echo "are installed in your system, you may need to use the" + echo "$(BOLD --with-lua) or $(BOLD --with-lua-include) flags to specify their location." + echo + echo "If those files are not yet installed, you need to install" + echo "them using the appropriate method for your operating system." + echo + die "Run $(BOLD ./configure --help) for details on flags." +} + +if [ "$DISABLE_INCDIR_CHECK" != "yes" ] +then + check_incdir + echo "lua.h found: $(GREEN "$lua_h")" +fi + +unzip_found=$(find_program "unzip") +if [ -n "$unzip_found" ] +then + echo "unzip found in PATH: $(GREEN "$unzip_found")" +else + RED "Could not find 'unzip'." + echo + die "Make sure it is installed and available in your PATH." +fi + +# ---------------------------------------- +# Write config +# ---------------------------------------- + +make clean > /dev/null 2> /dev/null + +rm -f built +cat < config.unix +# This file was automatically generated by the configure script. +# Run "./configure --help" for details. + +prefix=$prefix +sysconfdir=$sysconfdir +rocks_tree=$rocks_tree +LUA_VERSION=$LUA_VERSION +LUA=$LUA +LUA_DIR=$LUA_DIR +LUA_BINDIR=$LUA_BINDIR +LUA_INCDIR=$LUA_INCDIR +LUA_LIBDIR=$LUA_LIBDIR +FORCE_CONFIG=$FORCE_CONFIG +EOF + +echo +BLUE "Done configuring." +echo +echo +echo "LuaRocks will be installed at......: $(GREEN "$prefix")" +echo "LuaRocks will install rocks at.....: $(GREEN "$rocks_tree")" +echo "LuaRocks configuration directory...: $(GREEN "$sysconfdir/luarocks")" +echo "Using Lua from.....................: $(GREEN "$LUA_DIR")" +if [ "$LUA_BINDIR_SET" = "yes" ]; then echo "Lua bin directory..................: $(GREEN "$LUA_BINDIR")" ; fi +if [ "$LUA_INCDIR_SET" = "yes" ]; then echo "Lua include directory..............: $(GREEN "$LUA_INCDIR")" ; fi +if [ "$LUA_LIBDIR_SET" = "yes" ]; then echo "Lua lib directory..................: $(GREEN "$LUA_LIBDIR")" ; fi +echo +echo "* Type $(BOLD make) and $(BOLD make install):" +echo " to install to $prefix as usual." +echo "* Type $(BOLD make bootstrap):" +echo " to install LuaRocks into $rocks_tree as a rock." +echo diff --git a/luarocks-3.11.1-1.rockspec b/luarocks-3.11.1-1.rockspec new file mode 100644 index 0000000..97040c5 --- /dev/null +++ b/luarocks-3.11.1-1.rockspec @@ -0,0 +1,38 @@ +rockspec_format = "3.0" +package = "luarocks" +version = "3.11.1-1" +source = { + url = "git+https://github.com/luarocks/luarocks", + tag = "v3.11.1" +} +description = { + summary = "A package manager for Lua modules.", + detailed = [[ + LuaRocks allows you to install Lua modules as self-contained + packages called "rocks", which also contain version dependency + information. This information is used both during installation, + so that when one rock is requested all rocks it depends on are + installed as well, and at run time, so that when a module is + required, the correct version is loaded. LuaRocks supports both + local and remote repositories, and multiple local rocks trees. + ]], + homepage = "http://www.luarocks.org", + issues_url = "https://github.com/luarocks/luarocks/issues", + maintainer = "Hisham Muhammad", + license = "MIT", +} +test_dependencies = { + "luacov", + "busted-htest", +} +test = { + type = "busted", + platforms = { + windows = { + flags = { "--exclude-tags=ssh,git,unix", "-Xhelper", "lua_dir=$(LUA_DIR)", "-Xhelper", "lua=$(LUA)" } + }, + unix = { + flags = { "--exclude-tags=ssh,git", "-Xhelper", "lua_dir=$(LUA_DIR)", "-Xhelper", "lua=$(LUA)" } + } + } +} diff --git a/mergerelease b/mergerelease new file mode 100755 index 0000000..e7c9c76 --- /dev/null +++ b/mergerelease @@ -0,0 +1,33 @@ +#!/bin/sh + +[ "$1" ] || { + echo "usage.....: $0 " + echo "example...: $0 3.1.3" + echo + exit 1 +} + +v="$1" + +git show $v &> /dev/null || { + echo "There is no release branch $v" + exit 1 +} + +git show origin v$v &> /dev/null || { + echo "There is no pushed tag v$v in origin." + echo + echo "Before running this, make sure branch is tagged:" + echo " git tag -s v$v $v -m 'Release $v'" + echo " git push origin v$v" + echo + exit 1 +} + +git fetch --all +git checkout master +git diff master $v > version.diff +git merge --no-ff $v +patch -R -p1 < version.diff +git add luarocks-dev-1.rockspec +git commit -av --amend diff --git a/publishrelease b/publishrelease new file mode 100755 index 0000000..f11218e --- /dev/null +++ b/publishrelease @@ -0,0 +1,213 @@ +#!/usr/bin/env bash + +[ "$1" ] || { + echo "usage.....: $0 " + echo "example...: $0 3.1.1" + echo + echo "Before running this, make sure the packages were built:" + echo " makedist 3.1.1 /opt/lua54/ binary sign" + echo "And the tag was merged:" + echo " mergerelease 3.1.1" + echo + exit 1 +} + +####################################### +# preliminary checks +####################################### + +v="$1" + +git checkout v$v || { + echo "Could not checkout release tag." +} + +packages=( + luarocks-$v-windows-32.zip + luarocks-$v-windows-32.zip.asc + luarocks-$v-windows-64.zip + luarocks-$v-windows-64.zip.asc + luarocks-$v-linux-x86_64.zip + luarocks-$v-linux-x86_64.zip.asc + luarocks-$v-win32.zip + luarocks-$v-win32.zip.asc + luarocks-$v.tar.gz + luarocks-$v.tar.gz.asc +) + +for f in "${packages[@]}" luarocks-$v-1.rockspec +do + [ -e "$f" ] || { + echo "Missing file $f" + exit 1 + } +done + +####################################### +# utility +####################################### + +function confirm() { + branch="$1" + + echo "****************************************" + git diff $branch + echo "****************************************" + git status + echo "****************************************" + + echo "Everything looks all right? (y/n)" + echo "(Answering y will commit and push)" + read + if ! [ "$REPLY" == "y" ] + then + git reset + git checkout . + git checkout master + exit 1 + fi +} + +####################################### +# luarocks.org +####################################### + +luarocks upload luarocks-$v-1.rockspec + +####################################### +# gh-pages +####################################### + +git checkout gh-pages +git fetch origin gh-pages +git reset --hard origin/gh-pages + +cp "${packages[@]}" releases +cd releases +git add "${packages[@]}" +gawk ' +/add new release here/ { + print "" + print "" + print "luarocks-'$v'.tar.gzPGP signature" + print "luarocks-'$v'-windows-32.zip (luarocks.exe stand-alone Windows 32-bit binary)PGP signature" + print "luarocks-'$v'-windows-64.zip (luarocks.exe stand-alone Windows 64-bit binary)PGP signature" + print "luarocks-'$v'-linux-x86_64.zip (luarocks stand-alone Linux x86_64 binary)PGP signature" + print "luarocks-'$v'-win32.zip (legacy Windows package, includes Lua 5.1)PGP signature" + done = 1 +} +// { + if (done == 1) { + done = 0 + } else { + print + } +} +' index.html > index.html.1 +mv index.html.1 index.html +git add index.html + +gawk ' +/^\[$/ { + go = 1 +} +// { + print + if (go == 1) { + go = 0 + + print "{" + print "\"'$v'\": {" + print "\"date\": \"'$(date +'%Y-%m-%d')'\"," + print "\"files\": [\"luarocks-'$v'.tar.gz\", \"luarocks-'$v'.tar.gz.asc\", \"luarocks-'$v'-win32.zip\", \"luarocks-'$v'-win32.zip.asc\", \"luarocks-'$v'-windows-32.zip\", \"luarocks-'$v'-windows-32.zip.asc\", \"luarocks-'$v'-windows-64.zip\", \"luarocks-'$v'-windows-64.zip.asc\", \"luarocks-'$v'-linux-x86_64.zip\", \"luarocks-'$v'-linux-x86_64.zip.asc\"]," + print "\"about\": []" + print "}}," + } +} +' releases.json > releases.json.1 +mv releases.json.1 releases.json +git add releases.json + +confirm gh-pages + +git commit -av -m "Release $v" +git push + +####################################### +# luarocks.org +####################################### + +git checkout v$v + +luarocks upload luarocks-$v-1.rockspec + +git checkout master + +####################################### +# luarocks-site +####################################### + +if [ -e ../luarocks-site ] +then + cd ../luarocks-site + git pull +else + cd .. + git clone ssh://git@github.com/luarocks/luarocks-site + cd luarocks-site +fi + +sed -i 's,luarocks-[0-9]*\.[0-9]*\.[0-9]*,luarocks-'$v',' static/md/home.md +git add static/md/home.md + +confirm master + +git commit static/md/home.md -m "update front page for LuaRocks $v" +git push + +####################################### +# luarocks.wiki +####################################### + +[ -e ../luarocks.wiki ] || { + cd .. + git clone ssh://git@github.com/luarocks/luarocks.wiki.git +} + +if [ -e ../luarocks.wiki ] +then + cd ../luarocks.wiki + git pull +else + cd .. + git clone ssh://git@github.com/luarocks/luarocks.wiki.git + cd luarocks.wiki +fi + +sed -i "s,Latest release: .*,Latest release: '''LuaRocks $v''' - '$(date +'%d/%b/%Y')'," Download.mediawiki + +sed -i "s,/luarocks-[0-9.]*[0-9],/luarocks-$v,g" Download.mediawiki + +gawk ' +BEGIN { + print "'\'\'\''Version '$v\'\'\'' - '$(date +'%d/%b/%Y')' - [http://luarocks.org/releases/luarocks-'$v'.tar.gz Source tarball for Unix] -" + print "[http://luarocks.org/releases/luarocks-'$v'-windows-32.zip Windows binary (32-bit)] -" + print "[http://luarocks.org/releases/luarocks-'$v'-windows-64.zip Windows binary (64-bit)] -" + print "[http://luarocks.org/releases/luarocks-'$v'-linux-x86_64.zip Linux binary (x86_64)] -" + print "[http://luarocks.github.io/luarocks/releases other files]" + print "" +} +// { + print +} +' "Release-history.mediawiki" > "Release-history.mediawiki.1" +mv "Release-history.mediawiki.1" "Release-history.mediawiki" + +git add "Download.mediawiki" +git add "Release-history.mediawiki" +git add "Installation-instructions-for-Unix.md" + +confirm master + +git commit -av -m "Release $v" +git push diff --git a/smoke_test.sh b/smoke_test.sh new file mode 100755 index 0000000..86b85c5 --- /dev/null +++ b/smoke_test.sh @@ -0,0 +1,111 @@ +#!/bin/sh -e + +tarball="$1" + +rm -rf smoketestdir +mkdir smoketestdir +cp "$tarball" smoketestdir +cd smoketestdir + +tar zxvpf "$(basename "$tarball")" +cd "$(basename "$tarball" .tar.gz)" + +if [ "$2" = "binary" ] +then + ./configure --prefix=foobar + make binary + make install-binary + cd foobar + bin/luarocks + bin/luarocks install inspect + bin/luarocks show inspect + ( + eval $(bin/luarocks path) + lua -e 'print(assert(require("inspect")(_G)))' + ) + cd .. + rm -rf foobar + exit 0 +fi + +################################################################################ +# test installation with make install +################################################################################ + +./configure --prefix=foobar +make +make install +cd foobar +bin/luarocks --verbose +bin/luarocks --verbose install inspect +bin/luarocks --verbose show inspect +( + eval $(bin/luarocks path) + lua -e 'print(assert(require("inspect")(_G)))' +) +bin/luarocks --verbose remove inspect +cd .. +rm -rf foobar + +################################################################################ +# test installation with make bootstrap +################################################################################ + +./configure --prefix=fooboot +make bootstrap +./luarocks --verbose +./luarocks --verbose install inspect +./luarocks --verbose show inspect +./lua -e 'print(assert(require("inspect")(_G)))' +./luarocks --verbose remove inspect +cd fooboot +bin/luarocks --verbose +bin/luarocks --verbose install inspect +bin/luarocks --verbose show inspect +( + eval $(bin/luarocks path) + lua -e 'print(assert(require("inspect")(_G)))' +) +bin/luarocks --verbose remove inspect +cd .. +rm -rf fooboot + +################################################################################ +# test installation with luarocks install +################################################################################ + +./configure --prefix=foorock +make bootstrap +./luarocks make --pack-binary-rock +cd foorock +bin/luarocks install ../luarocks-*-1.all.rock +bin/luarocks --verbose +bin/luarocks --verbose install inspect +bin/luarocks --verbose show inspect +bin/luarocks install ../luarocks-*-1.all.rock --tree=../foorock2 +bin/luarocks --verbose remove inspect +cd ../foorock2 +bin/luarocks --verbose +bin/luarocks --verbose install inspect +bin/luarocks --verbose show inspect +( + eval $(bin/luarocks path) + lua -e 'print(assert(require("inspect")(_G)))' +) +bin/luarocks --verbose remove inspect +cd .. +rm -rf foorock +rm -rf foorock2 + +################################################################################ + +if [ "$3" = "windows" ] +then + make windows-binary +fi + +cd .. +rm -rf smoketestdir +echo +echo "Full test ran and nothing caught fire!" +echo diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 0000000..89e13ea --- /dev/null +++ b/spec/README.md @@ -0,0 +1,58 @@ + +# LuaRocks testsuite + +## Overview + +Test suite for LuaRocks project with Busted unit testing framework(http://olivinelabs.com/busted/). + +* Contains unit and integration tests +* Easy setup for your purpose on command line or from configuration file + +## Dependencies + +* Lua >= 5.1 +* Busted with dependencies + +## Usage + +Running of tests is based on basic Busted usage. *-Xhelper* flag is used +for inserting arguments into testing. Flag *--tags=* or *-t* is used +for specifying which tests will run. Start tests inside +LuaRocks folder or specify with *-C* flag. + +**Arguments for Busted helper script** + +``` +env=, (default:"minimal") type what kind of environment to use ["minimal", "full"] +noreset, Don't reset environment after each test +clean, remove existing testing environment +appveyor, add just if running on Appveyor +ci, add just if running on Unix CI +os=, type your OS ["linux", "os x", "windows"] +``` +--------------------------------------------------------------------------------------------- +## _**Tags** of tests are required and are in this format:_ + +**unit** - run all unit tests + +**integration** - run all integration tests + +**ssh** - run all tests which require ssh + +**mock** - run all tests which require mock LuaRocks server (upload tests) + +**unix** - run all tests which are UNIX based, won't work on Windows systems + +## Examples + +To run all tests: + +`busted` + +To run unit tests in LuaRocks directory type : + +`busted -t "unit"` + +To run integration tests without tests which use ssh: + +`busted -t "integration" --exclude-tags=ssh` diff --git a/spec/add_spec.lua b/spec/add_spec.lua new file mode 100644 index 0000000..60eea86 --- /dev/null +++ b/spec/add_spec.lua @@ -0,0 +1,41 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run +local testing_paths = test_env.testing_paths + +local extra_rocks = { + "/luasocket-${LUASOCKET}.src.rock", +} + +describe("LuaRocks add tests #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + describe("LuaRocks-admin add tests", function() + it("invalid rock #ssh", function() + assert.is_false(run.luarocks_admin_bool("--server=testing add invalid")) + end) + + it("missing argument", function() + assert.is_false(run.luarocks_admin_bool("--server=testing add")) + end) + + it("invalid server", function() + assert.is_false(run.luarocks_admin_bool("--server=invalid add " .. testing_paths.testing_server .. "/luasocket-${LUASOCKET}.src.rock")) + end) + + it("invalid server #ssh", function() + assert.is_true(run.luarocks_admin_bool("--server=testing add " .. testing_paths.testing_server .. "/luasocket-${LUASOCKET}.src.rock")) + end) + + --TODO This test fails, sftp support not yet implemented + it("invalid server", function() + assert.is_false(run.luarocks_admin_bool("--server=testing add luasocket-${LUASOCKET}.src.rock", { LUAROCKS_CONFIG = testing_paths.testrun_dir .. "/testing_config_sftp.lua" } )) + end) + + it("split server url", function() + assert.is_false(run.luarocks_admin_bool("--server=\"localhost@/tmp/luarocks_testing\" add " .. testing_paths.testing_server .. "/luasocket-${LUASOCKET}.src.rock")) + end) + end) +end) diff --git a/spec/build_spec.lua b/spec/build_spec.lua new file mode 100644 index 0000000..034c70d --- /dev/null +++ b/spec/build_spec.lua @@ -0,0 +1,456 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local get_tmp_path = test_env.get_tmp_path +local run = test_env.run +local testing_paths = test_env.testing_paths +local write_file = test_env.write_file +local git_repo = require("spec.util.git_repo") + +local cfg, fs + +local extra_rocks = { + "/lmathx-20120430.51-1.src.rock", + "/lmathx-20120430.51-1.rockspec", + "/lmathx-20120430.52-1.src.rock", + "/lmathx-20120430.52-1.rockspec", + "/lmathx-20150505-1.src.rock", + "/lmathx-20150505-1.rockspec", + "/lpeg-1.0.0-1.src.rock", + "/luafilesystem-${LUAFILESYSTEM}.src.rock", + "/luasocket-${LUASOCKET}.src.rock", + "spec/fixtures/a_rock-1.0-1.src.rock", +} + +local c_module_source = [[ + #include + #include + + int luaopen_c_module(lua_State* L) { + lua_newtable(L); + lua_pushinteger(L, 1); + lua_setfield(L, -2, "c_module"); + return 1; + } +]] + +describe("LuaRocks build #integration", function() + before_each(function() + test_env.setup_specs(extra_rocks) + cfg = require("luarocks.core.cfg") + fs = require("luarocks.fs") + cfg.init() + fs.init() + end) + + describe("building with flags", function() + it("verbose", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test.lua" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]]) + write_file("test.lua", "return {}") + + assert.is_true(run.luarocks_bool("build --verbose test-1.0-1.rockspec")) + assert.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/test/1.0-1/test-1.0-1.rockspec")) + end, finally) + end) + + it("fails if the deps-mode argument is invalid", function() + assert.is_false(run.luarocks_bool("build --deps-mode=123 " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec")) + assert.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + end) + + it("with --only-sources", function() + assert.is_true(run.luarocks_bool("download --server=" .. testing_paths.fixtures_dir .. "/a_repo --rockspec a_rock 1.0")) + assert.is_false(run.luarocks_bool("build --only-sources=\"http://example.com\" a_rock-1.0-1.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + + assert.is_true(run.luarocks_bool("download --server=" .. testing_paths.fixtures_dir .. "/a_repo --source a_rock 1.0")) + assert.is_true(run.luarocks_bool("build --only-sources=\"http://example.com\" a_rock-1.0-1.src.rock")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + + assert.is_true(os.remove("a_rock-1.0-1.rockspec")) + assert.is_true(os.remove("a_rock-1.0-1.src.rock")) + end) + + it("fails if an empty tree is given", function() + assert.is_false(run.luarocks_bool("build --tree=\"\" " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec")) + assert.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + end) + end) + + describe("basic builds", function() + it("luacov diff version", function() + assert.is_true(run.luarocks_bool("build luacov ${LUACOV}")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/luacov/${LUACOV}/luacov-${LUACOV}.rockspec")) + end) + + it("fails if the current platform is not supported", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test.lua" + } + supported_platforms = { + "unix", "macosx" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]]) + write_file("test.lua", "return {}") + + if test_env.TEST_TARGET_OS == "windows" then + assert.is_false(run.luarocks_bool("build test-1.0-1.rockspec")) -- Error: This rockspec does not support windows platforms + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/test/1.0-1/test-1.0-1.rockspec")) + else + assert.is_true(run.luarocks_bool("build test-1.0-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/test/1.0-1/test-1.0-1.rockspec")) + end + end, finally) + end) + + it("with skipping dependency checks", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test.lua" + } + dependencies = { + "a_rock 1.0" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]]) + write_file("test.lua", "return {}") + + assert.is_true(run.luarocks_bool("build test-1.0-1.rockspec --deps-mode=none")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/test/1.0-1/test-1.0-1.rockspec")) + end, finally) + end) + + it("lmathx deps partial match", function() + if test_env.LUA_V == "5.1" or test_env.LUAJIT_V then + assert.is_true(run.luarocks_bool("build lmathx")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lmathx/20120430.51-1/lmathx-20120430.51-1.rockspec")) + elseif test_env.LUA_V == "5.2" then + assert.is_true(run.luarocks_bool("build lmathx")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lmathx/20120430.52-1/lmathx-20120430.52-1.rockspec")) + elseif test_env.LUA_V == "5.3" then + assert.is_true(run.luarocks_bool("build lmathx")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lmathx/20150505-1/lmathx-20150505-1.rockspec")) + end + end) + end) + + describe("#namespaces", function() + it("builds a namespaced package from the command-line", function() + assert(run.luarocks_bool("build a_user/a_rock --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert.is_false(run.luarocks_bool("show a_rock 1.0")) + assert(run.luarocks_bool("show a_rock 2.0")) + assert(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/2.0-1/rock_namespace")) + end) + + it("builds a package with a namespaced dependency", function() + assert(run.luarocks_bool("build has_namespaced_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(run.luarocks_bool("show has_namespaced_dep")) + assert.is_false(run.luarocks_bool("show a_rock 1.0")) + assert(run.luarocks_bool("show a_rock 2.0")) + end) + + it("builds a package reusing a namespaced dependency", function() + assert(run.luarocks_bool("build a_user/a_rock --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(run.luarocks_bool("show a_rock 2.0")) + assert(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/2.0-1/rock_namespace")) + local output = run.luarocks("build has_namespaced_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" ) + assert.has.no.match("Missing dependencies", output) + end) + + it("builds a package considering namespace of locally installed package", function() + assert(run.luarocks_bool("build a_user/a_rock --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(run.luarocks_bool("show a_rock 2.0")) + assert(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/2.0-1/rock_namespace")) + local output = run.luarocks("build has_another_namespaced_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" ) + assert.has.match("Missing dependencies", output) + print(output) + assert(run.luarocks_bool("show a_rock 3.0")) + end) + end) + + describe("more complex tests", function() + if test_env.TYPE_TEST_ENV == "full" then + it("luacheck show downloads test_config", function() + local output = run.luarocks("build luacheck", { LUAROCKS_CONFIG = testing_paths.testrun_dir .. "/testing_config_show_downloads.lua"} ) + assert.is.truthy(output:match("%.%.%.")) + end) + end + + it("only deps", function() + local rockspec = testing_paths.fixtures_dir .. "/build_only_deps-0.1-1.rockspec" + + assert.is_true(run.luarocks_bool("build " .. rockspec .. " --only-deps")) + assert.is_false(run.luarocks_bool("show build_only_deps")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/build_only_deps/0.1-1/build_only_deps-0.1-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + end) + + it("only deps of a given rockspec", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test.lua" + } + dependencies = { + "a_rock 1.0" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]]) + write_file("test.lua", "return {}") + + assert.is.truthy(run.luarocks_bool("build --server=" .. testing_paths.fixtures_dir .. "/a_repo test-1.0-1.rockspec --only-deps")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/test/1.0-1/test-1.0-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + end, finally) + end) + + it("only deps of a given rock", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test.lua" + } + dependencies = { + "a_rock 1.0" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]]) + write_file("test.lua", "return {}") + + assert.is.truthy(run.luarocks_bool("pack test-1.0-1.rockspec")) + assert.is.truthy(lfs.attributes("test-1.0-1.src.rock")) + + assert.is.truthy(run.luarocks_bool("build --server=" .. testing_paths.fixtures_dir .. "/a_repo test-1.0-1.src.rock --only-deps")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/test/1.0-1/test-1.0-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + end, finally) + end) + + it("fails if given an argument with an invalid patch", function() + assert.is_false(run.luarocks_bool("build " .. testing_paths.fixtures_dir .. "/invalid_patch-0.1-1.rockspec")) + end) + end) + + describe("rockspec format 3.0 #rs3", function() + local tmpdir + local olddir + + before_each(function() + tmpdir = get_tmp_path() + olddir = lfs.currentdir() + lfs.mkdir(tmpdir) + lfs.chdir(tmpdir) + + lfs.mkdir("autodetect") + write_file("autodetect/bla.lua", "return {}", finally) + write_file("c_module.c", c_module_source, finally) + end) + + after_each(function() + if olddir then + lfs.chdir(olddir) + if tmpdir then + lfs.rmdir("autodetect") + lfs.rmdir(tmpdir) + end + end + end) + + it("defaults to build.type == 'builtin'", function() + local rockspec = "a_rock-1.0-1.rockspec" + test_env.write_file(rockspec, [[ + rockspec_format = "3.0" + package = "a_rock" + version = "1.0-1" + source = { + url = "file://]] .. testing_paths.fixtures_dir:gsub("\\", "/") .. [[/a_rock.lua" + } + description = { + summary = "An example rockspec", + } + dependencies = { + "lua >= 5.1" + } + build = { + modules = { + build = "a_rock.lua" + }, + } + ]], finally) + assert.truthy(run.luarocks_bool("build " .. rockspec)) + assert.is.truthy(run.luarocks("show a_rock")) + end) + + it("'builtin' detects lua files if build is not given", function() + local rockspec = "autodetect-1.0-1.rockspec" + test_env.write_file(rockspec, [[ + rockspec_format = "3.0" + package = "autodetect" + version = "1.0-1" + source = { + url = "file://autodetect/bla.lua" + } + description = { + summary = "An example rockspec", + } + dependencies = { + "lua >= 5.1" + } + ]], finally) + assert.truthy(run.luarocks_bool("build " .. rockspec)) + assert.match("bla.lua", run.luarocks("show autodetect")) + end) + + it("'builtin' synthesizes external_dependencies if not given but a library is given in build", function() + local rockspec = "autodetect-1.0-1.rockspec" + test_env.write_file(rockspec, [[ + rockspec_format = "3.0" + package = "autodetect" + version = "1.0-1" + source = { + url = "file://c_module.c" + } + description = { + summary = "An example rockspec", + } + dependencies = { + "lua >= 5.1" + } + build = { + modules = { + c_module = { + sources = "c_module.c", + libraries = "inexistent_library", + } + } + } + ]], finally) + assert.match("INEXISTENT_LIBRARY_DIR", run.luarocks("build " .. rockspec)) + end) + end) + + describe("#mock external dependencies", function() + lazy_setup(function() + test_env.setup_specs(nil, "mock") + test_env.mock_server_init() + end) + + lazy_teardown(function() + test_env.mock_server_done() + end) + + it("fails when missing external dependency", function() + test_env.run_in_tmp(function(tmpdir) + write_file("missing_external-0.1-1.rockspec", [[ + package = "missing_external" + version = "0.1-1" + source = { + url = "https://example.com/build.lua" + } + external_dependencies = { + INEXISTENT = { + library = "inexistentlib*", + header = "inexistentheader*.h", + } + } + dependencies = { + "lua >= 5.1" + } + build = { + type = "builtin", + modules = { + build = "build.lua" + } + } + ]]) + assert.is_false(run.luarocks_bool("build missing_external-0.1-1.rockspec INEXISTENT_INCDIR=\"/invalid/dir\"")) + end, finally) + end) + + it("builds with external dependency", function() + local rockspec = testing_paths.fixtures_dir .. "/with_external_dep-0.1-1.rockspec" + local foo_incdir = testing_paths.fixtures_dir .. "/with_external_dep" + assert.is_truthy(run.luarocks_bool("build " .. rockspec .. " FOO_INCDIR=\"" .. foo_incdir .. "\"")) + assert.is.truthy(run.luarocks("show with_external_dep")) + end) + end) + + describe("#build_dependencies", function() + it("builds with a build dependency", function() + assert(run.luarocks_bool("build has_build_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(run.luarocks_bool("show has_build_dep 1.0")) + assert(run.luarocks_bool("show a_build_dep 1.0")) + end) + end) + + describe("#unix build from #git", function() + local git + + lazy_setup(function() + git = git_repo.start() + end) + + lazy_teardown(function() + if git then + git:stop() + end + end) + + it("using --branch", function() + write_file("my_branch-1.0-1.rockspec", [[ + rockspec_format = "3.0" + package = "my_branch" + version = "1.0-1" + source = { + url = "git://localhost/testrock" + } + ]], finally) + assert.is_false(run.luarocks_bool("build --branch unknown-branch ./my_branch-1.0-1.rockspec")) + assert.is_true(run.luarocks_bool("build --branch test-branch ./my_branch-1.0-1.rockspec")) + end) + end) +end) diff --git a/spec/cmd_spec.lua b/spec/cmd_spec.lua new file mode 100644 index 0000000..20248af --- /dev/null +++ b/spec/cmd_spec.lua @@ -0,0 +1,105 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run + +describe("LuaRocks command line #integration", function() + + lazy_setup(function() + test_env.setup_specs() + end) + + describe("--version", function() + it("returns the LuaRocks version", function() + local output = run.luarocks("--version") + assert.match("LuaRocks main command-line interface", output, 1, true) + end) + + it("runs if Lua detection fails", function() + test_env.run_in_tmp(function(tmpdir) + test_env.write_file("bad_config.lua", [[ + variables = { + LUA_DIR = "/bad/lua/dir", + } + ]], finally) + local env = { + LUAROCKS_CONFIG = "bad_config.lua" + } + local output = run.luarocks("--version", env) + assert.match("LuaRocks main command-line interface", output, 1, true) + end, finally) + end) + end) + + describe("--lua-dir", function() + it("fails if given an invalid path", function() + local output = run.luarocks("--lua-dir=/bad/lua/path") + assert.match("Lua interpreter not found at /bad/lua/path", output, 1, true) + end) + + it("fails if given a valid path without Lua", function() + local output = run.luarocks("--lua-dir=.") + assert.match("Lua interpreter not found at .", output, 1, true) + end) + + it("passes if given a valid path with Lua", function() + assert.truthy(run.luarocks("--lua-dir=" .. test_env.testing_paths.luadir)) + end) + + it("passes if given a quoted path with Lua", function() + assert.truthy(run.luarocks("--lua-dir '" .. test_env.testing_paths.luadir .. "'")) + end) + end) + + describe("--lua-version", function() + it("fails if given something that is not a number", function() + local output = run.luarocks("--lua-version=bozo") + assert.match("malformed", output, 1, true) + end) + + it("sets the version independently of project tree", function() + test_env.run_in_tmp(function(tmpdir) + assert.truthy(run.luarocks_bool("init --lua-version=" .. test_env.lua_version .. " --lua-versions=" .. test_env.lua_version)) + + local output = run.luarocks("--lua-version=1.0") + assert.match("Version%s*:%s*1.0", output) + + output = run.luarocks("--lua-version=1.0 --project-tree=.") + assert.match("Version%s*:%s*1.0", output) + end, finally) + end) + end) + + it("detects version based on project tree", function() + test_env.run_in_tmp(function(tmpdir) + assert.truthy(run.luarocks_bool("init --lua-version=" .. test_env.lua_version)) + assert.truthy(run.luarocks_bool("config lua_version 1.0 --project-tree=" .. tmpdir .. "/lua_modules")) + + lfs.mkdir("aaa") + lfs.chdir("aaa") + lfs.mkdir("bbb") + lfs.chdir("bbb") + + local output = run.luarocks("") + assert.match("Version%s*:%s*1.0", output) + end, finally) + end) + + -- for backward compatibility + it("detects version of a project based on config", function() + test_env.run_in_tmp(function(tmpdir) + assert.truthy(run.luarocks_bool("init --lua-version=" .. test_env.lua_version)) + os.remove(".luarocks/config-" .. test_env.lua_version .. ".lua") + os.remove(".luarocks/default-lua-version.lua") + test_env.write_file(".luarocks/config-5.2.lua", [[ ]], finally) + + lfs.mkdir("aaa") + lfs.chdir("aaa") + lfs.mkdir("bbb") + lfs.chdir("bbb") + + local output = run.luarocks("") + assert.match("Version%s*:%s*5.2", output) + end, finally) + end) + +end) diff --git a/spec/config_spec.lua b/spec/config_spec.lua new file mode 100644 index 0000000..9dd8fbc --- /dev/null +++ b/spec/config_spec.lua @@ -0,0 +1,253 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run +local testing_paths = test_env.testing_paths +local env_variables = test_env.env_variables +local write_file = test_env.write_file +local get_tmp_path = test_env.get_tmp_path +local hardcoded + +describe("LuaRocks config tests #integration", function() + + lazy_setup(function() + test_env.setup_specs() + -- needs to be required here, because hardcoded is created after first loading of specs + hardcoded = require("luarocks.core.hardcoded") + end) + + describe("full configuration query", function() + it("no flags/arguments", function() + assert.match("rocks_servers = {", run.luarocks("config")) + end) + + it("--json", function() + assert.match('"rocks_servers":[', run.luarocks("config --json"), 1, true) + end) + + it("with --tree respects custom config", function() + write_file("my_config.lua", [[ + rocks_trees = { + { + name = "system", + root = "/example/tree", + lua_dir = "/example/luadir", + }, + } + ]], finally) + local output = run.luarocks("config", {LUAROCKS_CONFIG = "my_config.lua"}) + assert.match([[deploy_lua_dir = "/example/luadir"]], output) + output = run.luarocks("config --tree=system", {LUAROCKS_CONFIG = "my_config.lua"}) + assert.match([[deploy_lua_dir = "/example/luadir"]], output) + end) + + it("#unix can find config via $XDG_CONFIG_HOME", function() + local tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + lfs.mkdir(tmpdir .. "/luarocks") + local tmp_config_file = tmpdir .. "/luarocks/config-" .. test_env.lua_version .. ".lua" + write_file(tmp_config_file, [[ + rocks_trees = { + { + name = "system", + root = "/example/tree", + lua_dir = "/example/luadir", + }, + } + ]]) + finally(function() + os.remove(tmp_config_file) + lfs.rmdir(tmpdir .. "/luarocks") + lfs.rmdir(tmpdir) + end) + + local output = run.luarocks("config --verbose", {XDG_CONFIG_HOME = tmpdir, LUAROCKS_CONFIG="invalid"}) + assert.match([[deploy_lua_dir = "/example/luadir"]], output) + end) + end) + + describe("query flags", function() + it("--lua-incdir returns a subdir of LUA_DIR", function() + local output = run.luarocks("config --lua-incdir") + assert.match(hardcoded.LUA_DIR, output, 1, true) + end) + + it("--lua-libdir returns a subdir of LUA_DIR", function() + local output = run.luarocks("config --lua-libdir") + assert.match(hardcoded.LUA_DIR, output, 1, true) + end) + + it("--lua-ver returns the Lua version", function() + local output = run.luarocks("config --lua-ver") + local lua_version = _VERSION:gsub("Lua ", "") + if test_env.LUAJIT_V then + lua_version = "5.1" + end + assert.are.same(lua_version, output) + end) + + it("--rock-trees lists rock trees", function() + assert.is_true(run.luarocks_bool("config --rock-trees")) + end) + + describe("--user-config", function() + it("returns user config dir", function() + local user_config_path = run.luarocks("config --user-config") + assert.is.truthy(lfs.attributes(user_config_path)) + end) + + it("handles a missing user config", function() + local output = run.luarocks("config --user-config", {LUAROCKS_CONFIG = "missing_file.lua"}) + assert.match("Warning", output) + end) + end) + + describe("--system-config", function() + local scdir = testing_paths.testing_lrprefix .. "/etc/luarocks" + local configfile = scdir .. "/config-" .. env_variables.LUA_VERSION .. ".lua" + + it("fails if system config doesn't exist", function() + os.rename(configfile, configfile .. ".bak") + finally(function() + os.rename(configfile .. ".bak", configfile) + end) + assert.is_false(run.luarocks_bool("config --system-config")) + end) + + it("fails if system config is invalid", function() + lfs.mkdir(testing_paths.testing_lrprefix) + lfs.mkdir(testing_paths.testing_lrprefix .. "/etc/") + lfs.mkdir(scdir) + + local sysconfig = io.open(configfile, "w+") + sysconfig:write("if if if") + sysconfig:close() + finally(function() + os.remove(configfile) + end) + assert.is_false(run.luarocks_bool("config --system-config")) + end) + end) + end) + + describe("read config keys", function() + it("reads a simple config key", function() + local output = run.luarocks("config user_agent") + assert.match("LuaRocks/", output) + end) + + it("reads an array config key", function() + local output = run.luarocks("config rocks_trees[2]") + assert.match("{%s*name", output) + end) + + it("can read as JSON", function() + local output = run.luarocks("config rocks_trees --json") + assert.match('^%[{', output) + end) + + it("reads an array -> hash config key", function() + local output = run.luarocks("config rocks_trees[2].name") + assert.match("[a-z]+", output) + end) + + it("reads a hash config key", function() + local output = run.luarocks("config variables.ICACLS") + assert.same("icacls", output) + end) + + it("fails on invalid config key", function() + local output = run.luarocks("config xyz") + assert.match("Error: Unknown entry xyz", output) + end) + end) + + describe("unset config keys", function() + it("unsets a simple config key", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init")) + assert.truthy(run.luarocks_bool("config my_var my_value")) + + local output = run.luarocks("config my_var") + assert.match("my_value", output) + + assert.truthy(run.luarocks_bool("config my_var --unset")) + + output = run.luarocks("config my_var") + assert.not_match("my_value", output) + end, finally) + end) + end) + + describe("write config keys", function() + it("rejects invalid --scope", function() + assert.is_false(run.luarocks_bool("config web_browser foo --scope=foo")) + end) + + it("reads an array config key", function() + local output = run.luarocks("config rocks_trees[2]") + assert.match("{%s*name", output) + end) + + it("writes a simple config key", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init")) + assert.truthy(run.luarocks_bool("config web_browser foo --scope=project")) + + local output = run.luarocks("config web_browser") + assert.match("foo", output) + end, finally) + end) + + it("writes a hash config key", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init")) + assert.truthy(run.luarocks_bool("config variables.FOO_DIR /foo/bar --scope=project")) + + local output = run.luarocks("config variables.FOO_DIR") + assert.match("/foo/bar", output) + end, finally) + end) + + it("writes a boolean config key", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init")) + assert.truthy(run.luarocks_bool("config hooks_enabled true")) + + local output = run.luarocks("config hooks_enabled") + assert.match("true", output) + end, finally) + end) + + it("writes an array config key", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init")) + assert.truthy(run.luarocks_bool("config external_deps_patterns.lib[1] testtest --scope=project")) + + local output = run.luarocks("config external_deps_patterns.lib[1]") + assert.match("testtest", output) + end, finally) + end) + + end) + +end) diff --git a/spec/deps_spec.lua b/spec/deps_spec.lua new file mode 100644 index 0000000..766e0f0 --- /dev/null +++ b/spec/deps_spec.lua @@ -0,0 +1,114 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run +local testing_paths = test_env.testing_paths + +local extra_rocks = { + "/lxsh-${LXSH}.src.rock", + "/luasocket-${LUASOCKET}.src.rock", + "/lpeg-${LPEG}.src.rock", +} + +describe("LuaRocks deps-mode #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + it("one", function() + assert.is_true(run.luarocks_bool("build --tree=system lpeg")) + assert.is_true(run.luarocks_bool("build --deps-mode=one --tree=" .. testing_paths.testing_tree .. " lxsh")) + + assert.is.truthy(lfs.attributes(testing_paths.testing_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("order", function() + assert.is_true(run.luarocks_bool("build --tree=system lpeg")) + assert.is_true(run.luarocks_bool("build --deps-mode=order --tree=" .. testing_paths.testing_tree .. " lxsh")) + + assert.is.falsy(lfs.attributes(testing_paths.testing_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("order sys", function() + assert.is_true(run.luarocks_bool("build --tree=" .. testing_paths.testing_tree .. " lpeg")) + assert.is_true(run.luarocks_bool("build --deps-mode=order --tree=" .. testing_paths.testing_sys_tree .. " lxsh")) + + assert.is.truthy(lfs.attributes(testing_paths.testing_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("all sys", function() + assert.is_true(run.luarocks_bool("build --tree=" .. testing_paths.testing_tree .. " lpeg")) + assert.is_true(run.luarocks_bool("build --deps-mode=all --tree=" .. testing_paths.testing_sys_tree .. " lxsh")) + + assert.is.truthy(lfs.attributes(testing_paths.testing_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("none", function() + assert.is_true(run.luarocks_bool("build --tree=" .. testing_paths.testing_tree .. " lpeg")) + assert.is_true(run.luarocks_bool("build --deps-mode=none lxsh")) + + assert.is.truthy(lfs.attributes(testing_paths.testing_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("LuaRocks nodeps alias", function() + assert.is_true(run.luarocks_bool("build --tree=" .. testing_paths.testing_tree .. " --nodeps lxsh")) + + assert.is.falsy(lfs.attributes(testing_paths.testing_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("make order", function() + assert.is_true(run.luarocks_bool("build --tree=" .. testing_paths.testing_sys_tree .. " lpeg")) + assert.is_true(run.luarocks_bool("download --source lxsh ${LXSH_V}")) + assert.is_true(run.luarocks_bool("unpack lxsh-${LXSH}.src.rock")) + lfs.chdir("lxsh-${LXSH}/lxsh-${LXSH_V}-1/") + assert.is_true(run.luarocks_bool("make --tree=" .. testing_paths.testing_tree .. " --deps-mode=order")) + + finally(function() + lfs.chdir(testing_paths.testrun_dir) + test_env.remove_dir("lxsh-${LXSH}") + assert.is_true(os.remove("lxsh-${LXSH}.src.rock")) + end) + + assert.is.falsy(lfs.attributes(testing_paths.testing_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("make order sys", function() + assert.is_true(run.luarocks_bool("build --tree=" .. testing_paths.testing_tree .. " lpeg")) + assert.is_true(run.luarocks_bool("download --source lxsh ${LXSH_V}")) + assert.is_true(run.luarocks_bool("unpack lxsh-${LXSH}.src.rock")) + lfs.chdir("lxsh-${LXSH}/lxsh-${LXSH_V}-1/") + assert.is_true(run.luarocks_bool("make --tree=" .. testing_paths.testing_sys_tree .. " --deps-mode=order")) + + finally(function() + lfs.chdir(testing_paths.testrun_dir) + test_env.remove_dir("lxsh-${LXSH}") + assert.is_true(os.remove("lxsh-${LXSH}.src.rock")) + end) + + assert.is.truthy(lfs.attributes(testing_paths.testing_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lpeg/${LPEG}/lpeg-${LPEG}.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) +end) diff --git a/spec/doc_spec.lua b/spec/doc_spec.lua new file mode 100644 index 0000000..f48f951 --- /dev/null +++ b/spec/doc_spec.lua @@ -0,0 +1,154 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run +local testing_paths = test_env.testing_paths + +describe("luarocks doc #integration", function() + before_each(function() + test_env.setup_specs() + end) + + describe("basic tests", function() + it("with no flags/arguments", function() + assert.is_false(run.luarocks_bool("doc")) + end) + + it("with invalid argument", function() + assert.is_false(run.luarocks_bool("doc invalid")) + end) + + it("with no homepage and no doc folder", function() + test_env.run_in_tmp(function(tmpdir) + test_env.write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://test.lua" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]], finally) + test_env.write_file("test.lua", "return {}", finally) + + assert.is_true(run.luarocks_bool("install test-1.0-1.rockspec")) + assert.is_false(run.luarocks_bool("doc test --home")) + end, finally) + end) + + it("with no doc folder but with homepage", function() + test_env.run_in_tmp(function(tmpdir) + test_env.write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://test.lua" + } + description = { + homepage = "http://www.example.com" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]], finally) + test_env.write_file("test.lua", "return {}", finally) + + assert.is_true(run.luarocks_bool("install test-1.0-1.rockspec")) + local output = assert.is.truthy(run.luarocks("doc test")) + assert.is.truthy(output:find("documentation directory not found")) + end, finally) + end) + end) + + describe("#namespaces", function() + it("retrieves docs for a namespaced package from the command-line", function() + assert(run.luarocks_bool("build a_user/a_rock --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(run.luarocks_bool("build a_rock 1.0 --keep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert.match("a_rock 2.0", run.luarocks("doc a_user/a_rock")) + end) + end) + + describe("tests with flags", function() + it("of installed package", function() + test_env.run_in_tmp(function(tmpdir) + test_env.write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://test.lua" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]], finally) + test_env.write_file("test.lua", "return {}", finally) + + assert.is_true(run.luarocks_bool("install test-1.0-1.rockspec")) + lfs.mkdir(testing_paths.testing_sys_rocks .. "/test/1.0-1/doc") + test_env.write_file(testing_paths.testing_sys_rocks .. "/test/1.0-1/doc/doc.md", "", finally) + test_env.write_file(testing_paths.testing_sys_rocks .. "/test/1.0-1/doc/readme.md", "", finally) + assert.is_true(run.luarocks_bool("doc test")) + end, finally) + end) + + it("with --list", function() + test_env.run_in_tmp(function(tmpdir) + test_env.write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://test.lua" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]], finally) + test_env.write_file("test.lua", "return {}", finally) + + assert.is_true(run.luarocks_bool("install test-1.0-1.rockspec")) + lfs.mkdir(testing_paths.testing_sys_rocks .. "/test/1.0-1/doc") + test_env.write_file(testing_paths.testing_sys_rocks .. "/test/1.0-1/doc/doc1.md", "", finally) + test_env.write_file(testing_paths.testing_sys_rocks .. "/test/1.0-1/doc/doc2.md", "", finally) + local output = assert.is.truthy(run.luarocks("doc test --list")) + assert.is.truthy(output:find("doc1%.md")) + assert.is.truthy(output:find("doc2%.md")) + end, finally) + end) + + it("with --porcelain", function() + test_env.run_in_tmp(function(tmpdir) + test_env.write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://test.lua" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]], finally) + test_env.write_file("test.lua", "return {}", finally) + + assert.is_true(run.luarocks_bool("install test-1.0-1.rockspec")) + lfs.mkdir(testing_paths.testing_sys_rocks .. "/test/1.0-1/doc") + test_env.write_file(testing_paths.testing_sys_rocks .. "/test/1.0-1/doc/doc1.md", "", finally) + test_env.write_file(testing_paths.testing_sys_rocks .. "/test/1.0-1/doc/doc2.md", "", finally) + assert.is_true(run.luarocks_bool("doc test --porcelain")) + end, finally) + end) + end) +end) diff --git a/spec/download_spec.lua b/spec/download_spec.lua new file mode 100644 index 0000000..f7c10e8 --- /dev/null +++ b/spec/download_spec.lua @@ -0,0 +1,55 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run +local testing_paths = test_env.testing_paths + +local extra_rocks = { + "/say-1.3-1.rockspec", +} + +describe("luarocks download #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + it("with no flags/arguments", function() + assert.is_false(run.luarocks_bool("download")) + end) + + it("invalid", function() + assert.is_false(run.luarocks_bool("download invalid")) + end) + + it("all with delete downloaded files", function() --TODO maybe download --all more rocks + assert.is_true(run.luarocks_bool("download --all say")) + assert.is.truthy(lfs.attributes("say-1.3-1.rockspec")) + test_env.remove_files(lfs.currentdir(), "say--") + end) + + it("rockspec version", function() + assert.is_true(run.luarocks_bool("download --rockspec say 1.3-1")) + assert.is.truthy(lfs.attributes("say-1.3-1.rockspec")) + test_env.remove_files(lfs.currentdir(), "say--") + end) + + describe("#namespaces", function() + it("retrieves namespaced rockspec", function() + finally(function() + os.remove("a_rock-2.0-1.rockspec") + end) + assert(run.luarocks_bool("download a_user/a_rock --rockspec --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(lfs.attributes("a_rock-2.0-1.rockspec")) + end) + + it("retrieves namespaced rock", function() + finally(function() + os.remove("a_rock-2.0-1.src.rock") + end) + assert(run.luarocks_bool("download a_user/a_rock --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(lfs.attributes("a_rock-2.0-1.src.rock")) + end) + end) + + +end) diff --git a/spec/dummy_spec.lua b/spec/dummy_spec.lua new file mode 100644 index 0000000..9ddd669 --- /dev/null +++ b/spec/dummy_spec.lua @@ -0,0 +1,24 @@ +-- Dummy test file for including files with 0% coverage in the luacov report + +local test_env = require("spec.util.test_env") +local testing_paths = test_env.testing_paths + +test_env.setup_specs() +local runner = require("luacov.runner") +runner.init(testing_paths.testrun_dir .. "/luacov.config") + +require("luarocks.build.cmake") +require("luarocks.build.command") +require("luarocks.tools.tar") +require("luarocks.fetch.cvs") +require("luarocks.fetch.git_file") +require("luarocks.fetch.git_https") +require("luarocks.fetch.git_ssh") +require("luarocks.fetch.hg_http") +require("luarocks.fetch.hg_https") +require("luarocks.fetch.hg_ssh") +require("luarocks.fetch.hg") +require("luarocks.fetch.sscm") +require("luarocks.fetch.svn") + +runner.save_stats() diff --git a/spec/external_spec.lua b/spec/external_spec.lua new file mode 100644 index 0000000..5a69a67 --- /dev/null +++ b/spec/external_spec.lua @@ -0,0 +1,32 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run +local testing_paths = test_env.testing_paths + +describe("luarocks external commands #integration", function() + lazy_setup(function() + test_env.setup_specs() + test_env.mock_server_init() + end) + + lazy_teardown(function() + test_env.mock_server_done() + end) + + it("installs a legacy external command", function() + local rockspec = testing_paths.fixtures_dir .. "/legacyexternalcommand-0.1-1.rockspec" + assert.is_truthy(run.luarocks_bool("build " .. rockspec)) + assert.is.truthy(run.luarocks("show legacyexternalcommand")) + local output = run.luarocks("legacyexternalcommand") + assert.match("Argument missing", output) + output = run.luarocks("legacyexternalcommand foo") + assert.match("ARG1\tfoo", output) + assert.match("ARG2\tnil", output) + output = run.luarocks("legacyexternalcommand foo bar") + assert.match("ARG1\tfoo", output) + assert.match("ARG2\tbar", output) + output = run.luarocks("legacyexternalcommand foo bar bla") + assert.match("ARG1\tfoo", output) + assert.match("ARG2\tbar", output) + end) +end) + diff --git a/spec/fixtures/a_repo/a_build_dep-1.0-1.rockspec b/spec/fixtures/a_repo/a_build_dep-1.0-1.rockspec new file mode 100644 index 0000000..2398d3d --- /dev/null +++ b/spec/fixtures/a_repo/a_build_dep-1.0-1.rockspec @@ -0,0 +1,18 @@ +rockspec_format = "3.0" +package = "a_build_dep" +version = "1.0-1" +source = { + url = "http://localhost:8080/file/a_rock.lua" +} +description = { + summary = "An example rockspec that is a build dependency for has_build_dep.", +} +dependencies = { + "lua >= 5.1", +} +build = { + type = "builtin", + modules = { + build_dep = "a_rock.lua" + }, +} diff --git a/spec/fixtures/a_repo/a_build_dep-1.0-1.src.rock b/spec/fixtures/a_repo/a_build_dep-1.0-1.src.rock new file mode 100644 index 0000000..c56ee34 Binary files /dev/null and b/spec/fixtures/a_repo/a_build_dep-1.0-1.src.rock differ diff --git a/spec/fixtures/a_repo/a_rock-1.0-1.rockspec b/spec/fixtures/a_repo/a_rock-1.0-1.rockspec new file mode 100644 index 0000000..69d7820 --- /dev/null +++ b/spec/fixtures/a_repo/a_rock-1.0-1.rockspec @@ -0,0 +1,18 @@ +package = "a_rock" +version = "1.0-1" +source = { + url = "http://localhost:8080/file/a_rock.lua" +} +description = { + summary = "An example rockspec", + homepage = "http://www.example.com" +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + build = "a_rock.lua" + }, +} diff --git a/spec/fixtures/a_repo/a_rock-1.0-1.src.rock b/spec/fixtures/a_repo/a_rock-1.0-1.src.rock new file mode 100644 index 0000000..9d0bb45 Binary files /dev/null and b/spec/fixtures/a_repo/a_rock-1.0-1.src.rock differ diff --git a/spec/fixtures/a_repo/a_rock-2.0-1.src.rock b/spec/fixtures/a_repo/a_rock-2.0-1.src.rock new file mode 100644 index 0000000..5824c76 Binary files /dev/null and b/spec/fixtures/a_repo/a_rock-2.0-1.src.rock differ diff --git a/spec/fixtures/a_repo/busted_project-0.1-1.rockspec b/spec/fixtures/a_repo/busted_project-0.1-1.rockspec new file mode 100644 index 0000000..54ed28a --- /dev/null +++ b/spec/fixtures/a_repo/busted_project-0.1-1.rockspec @@ -0,0 +1,19 @@ +rockspec_format = "3.0" +package = "busted_project" +version = "0.1-1" +source = { + url = "http://localhost:8080/file/busted_project-0.1.tar.gz", + dir = "busted_project", +} +description = { + summary = "A project that uses Busted tests", +} +build = { + type = "builtin", + modules = { + sum = "sum.lua", + } +} +test = { + type = "busted", +} diff --git a/spec/fixtures/a_repo/busted_project-0.1-1.src.rock b/spec/fixtures/a_repo/busted_project-0.1-1.src.rock new file mode 100644 index 0000000..db92411 Binary files /dev/null and b/spec/fixtures/a_repo/busted_project-0.1-1.src.rock differ diff --git a/spec/fixtures/a_repo/has_another_namespaced_dep-1.0-1.rockspec b/spec/fixtures/a_repo/has_another_namespaced_dep-1.0-1.rockspec new file mode 100644 index 0000000..04cb06c --- /dev/null +++ b/spec/fixtures/a_repo/has_another_namespaced_dep-1.0-1.rockspec @@ -0,0 +1,19 @@ +rockspec_format = "3.0" +package = "has_another_namespaced_dep" +version = "1.0-1" +source = { + url = "http://localhost:8080/file/a_rock.lua" +} +description = { + summary = "An example rockspec", +} +dependencies = { + "another_user/a_rock", + "lua >= 5.1", +} +build = { + type = "builtin", + modules = { + bla = "a_rock.lua" + }, +} diff --git a/spec/fixtures/a_repo/has_another_namespaced_dep-1.0-1.src.rock b/spec/fixtures/a_repo/has_another_namespaced_dep-1.0-1.src.rock new file mode 100644 index 0000000..4bbbf1a Binary files /dev/null and b/spec/fixtures/a_repo/has_another_namespaced_dep-1.0-1.src.rock differ diff --git a/spec/fixtures/a_repo/has_build_dep-1.0-1.all.rock b/spec/fixtures/a_repo/has_build_dep-1.0-1.all.rock new file mode 100644 index 0000000..a2d09ab Binary files /dev/null and b/spec/fixtures/a_repo/has_build_dep-1.0-1.all.rock differ diff --git a/spec/fixtures/a_repo/has_build_dep-1.0-1.rockspec b/spec/fixtures/a_repo/has_build_dep-1.0-1.rockspec new file mode 100644 index 0000000..417473a --- /dev/null +++ b/spec/fixtures/a_repo/has_build_dep-1.0-1.rockspec @@ -0,0 +1,22 @@ +rockspec_format = "3.0" +package = "has_build_dep" +version = "1.0-1" +source = { + url = "http://localhost:8080/file/a_rock.lua" +} +description = { + summary = "An example rockspec that has build dependencies.", +} +dependencies = { + "a_rock", + "lua >= 5.1", +} +build_dependencies = { + "a_build_dep", +} +build = { + type = "builtin", + modules = { + bla = "a_rock.lua" + }, +} diff --git a/spec/fixtures/a_repo/has_build_dep-1.0-1.src.rock b/spec/fixtures/a_repo/has_build_dep-1.0-1.src.rock new file mode 100644 index 0000000..fc4e11b Binary files /dev/null and b/spec/fixtures/a_repo/has_build_dep-1.0-1.src.rock differ diff --git a/spec/fixtures/a_repo/has_namespaced_dep-1.0-1.rockspec b/spec/fixtures/a_repo/has_namespaced_dep-1.0-1.rockspec new file mode 100644 index 0000000..6152c78 --- /dev/null +++ b/spec/fixtures/a_repo/has_namespaced_dep-1.0-1.rockspec @@ -0,0 +1,19 @@ +rockspec_format = "3.0" +package = "has_namespaced_dep" +version = "1.0-1" +source = { + url = "http://localhost:8080/file/a_rock.lua" +} +description = { + summary = "An example rockspec", +} +dependencies = { + "a_user/a_rock", + "lua >= 5.1", +} +build = { + type = "builtin", + modules = { + bla = "a_rock.lua" + }, +} diff --git a/spec/fixtures/a_repo/has_namespaced_dep-1.0-1.src.rock b/spec/fixtures/a_repo/has_namespaced_dep-1.0-1.src.rock new file mode 100644 index 0000000..c4d5f94 Binary files /dev/null and b/spec/fixtures/a_repo/has_namespaced_dep-1.0-1.src.rock differ diff --git a/spec/fixtures/a_repo/manifest b/spec/fixtures/a_repo/manifest new file mode 100644 index 0000000..a5f770a --- /dev/null +++ b/spec/fixtures/a_repo/manifest @@ -0,0 +1,90 @@ +commands = {} +modules = {} +repository = { + a_build_dep = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + a_rock = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + }, + ["2.0-1"] = { + { + arch = "src" + } + } + }, + busted_project = { + ["0.1-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + has_another_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + has_build_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + }, + { + arch = "all" + } + } + }, + has_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + non_lua_file = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + }, + ["1.0-2"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifest-5.1 b/spec/fixtures/a_repo/manifest-5.1 new file mode 100644 index 0000000..a5f770a --- /dev/null +++ b/spec/fixtures/a_repo/manifest-5.1 @@ -0,0 +1,90 @@ +commands = {} +modules = {} +repository = { + a_build_dep = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + a_rock = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + }, + ["2.0-1"] = { + { + arch = "src" + } + } + }, + busted_project = { + ["0.1-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + has_another_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + has_build_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + }, + { + arch = "all" + } + } + }, + has_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + non_lua_file = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + }, + ["1.0-2"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifest-5.1.zip b/spec/fixtures/a_repo/manifest-5.1.zip new file mode 100644 index 0000000..e63d6f1 Binary files /dev/null and b/spec/fixtures/a_repo/manifest-5.1.zip differ diff --git a/spec/fixtures/a_repo/manifest-5.2 b/spec/fixtures/a_repo/manifest-5.2 new file mode 100644 index 0000000..a5f770a --- /dev/null +++ b/spec/fixtures/a_repo/manifest-5.2 @@ -0,0 +1,90 @@ +commands = {} +modules = {} +repository = { + a_build_dep = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + a_rock = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + }, + ["2.0-1"] = { + { + arch = "src" + } + } + }, + busted_project = { + ["0.1-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + has_another_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + has_build_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + }, + { + arch = "all" + } + } + }, + has_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + non_lua_file = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + }, + ["1.0-2"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifest-5.2.zip b/spec/fixtures/a_repo/manifest-5.2.zip new file mode 100644 index 0000000..cec28c0 Binary files /dev/null and b/spec/fixtures/a_repo/manifest-5.2.zip differ diff --git a/spec/fixtures/a_repo/manifest-5.3 b/spec/fixtures/a_repo/manifest-5.3 new file mode 100644 index 0000000..a5f770a --- /dev/null +++ b/spec/fixtures/a_repo/manifest-5.3 @@ -0,0 +1,90 @@ +commands = {} +modules = {} +repository = { + a_build_dep = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + a_rock = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + }, + ["2.0-1"] = { + { + arch = "src" + } + } + }, + busted_project = { + ["0.1-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + has_another_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + has_build_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + }, + { + arch = "all" + } + } + }, + has_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + non_lua_file = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + }, + ["1.0-2"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifest-5.3.zip b/spec/fixtures/a_repo/manifest-5.3.zip new file mode 100644 index 0000000..23df5c3 Binary files /dev/null and b/spec/fixtures/a_repo/manifest-5.3.zip differ diff --git a/spec/fixtures/a_repo/manifest-5.4 b/spec/fixtures/a_repo/manifest-5.4 new file mode 100644 index 0000000..a5f770a --- /dev/null +++ b/spec/fixtures/a_repo/manifest-5.4 @@ -0,0 +1,90 @@ +commands = {} +modules = {} +repository = { + a_build_dep = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + a_rock = { + ["1.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + }, + ["2.0-1"] = { + { + arch = "src" + } + } + }, + busted_project = { + ["0.1-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + }, + has_another_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + has_build_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + }, + { + arch = "all" + } + } + }, + has_namespaced_dep = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + }, + non_lua_file = { + ["1.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + }, + ["1.0-2"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifest-5.4.zip b/spec/fixtures/a_repo/manifest-5.4.zip new file mode 100644 index 0000000..14b5621 Binary files /dev/null and b/spec/fixtures/a_repo/manifest-5.4.zip differ diff --git a/spec/fixtures/a_repo/manifests/a_user/a_rock-2.0-1.rockspec b/spec/fixtures/a_repo/manifests/a_user/a_rock-2.0-1.rockspec new file mode 100644 index 0000000..da5a9a1 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/a_user/a_rock-2.0-1.rockspec @@ -0,0 +1,17 @@ +package = "a_rock" +version = "2.0-1" +source = { + url = "http://localhost:8080/file/a_rock.lua" +} +description = { + summary = "An example rockspec", +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + build = "a_rock.lua" + }, +} diff --git a/spec/fixtures/a_repo/manifests/a_user/a_rock-2.0-1.src.rock b/spec/fixtures/a_repo/manifests/a_user/a_rock-2.0-1.src.rock new file mode 100644 index 0000000..8d10fac Binary files /dev/null and b/spec/fixtures/a_repo/manifests/a_user/a_rock-2.0-1.src.rock differ diff --git a/spec/fixtures/a_repo/manifests/a_user/manifest b/spec/fixtures/a_repo/manifests/a_user/manifest new file mode 100644 index 0000000..74b0c61 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/a_user/manifest @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["2.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifests/a_user/manifest-5.1 b/spec/fixtures/a_repo/manifests/a_user/manifest-5.1 new file mode 100644 index 0000000..74b0c61 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/a_user/manifest-5.1 @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["2.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifests/a_user/manifest-5.2 b/spec/fixtures/a_repo/manifests/a_user/manifest-5.2 new file mode 100644 index 0000000..74b0c61 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/a_user/manifest-5.2 @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["2.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifests/a_user/manifest-5.3 b/spec/fixtures/a_repo/manifests/a_user/manifest-5.3 new file mode 100644 index 0000000..74b0c61 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/a_user/manifest-5.3 @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["2.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifests/a_user/manifest-5.4 b/spec/fixtures/a_repo/manifests/a_user/manifest-5.4 new file mode 100644 index 0000000..74b0c61 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/a_user/manifest-5.4 @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["2.0-1"] = { + { + arch = "rockspec" + }, + { + arch = "src" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifests/another_user/a_rock-3.0-1.rockspec b/spec/fixtures/a_repo/manifests/another_user/a_rock-3.0-1.rockspec new file mode 100644 index 0000000..628c5a4 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/another_user/a_rock-3.0-1.rockspec @@ -0,0 +1,17 @@ +package = "a_rock" +version = "3.0-1" +source = { + url = "http://localhost:8080/file/a_rock.lua" +} +description = { + summary = "An example rockspec", +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + a_rock = "a_rock.lua" + }, +} diff --git a/spec/fixtures/a_repo/manifests/another_user/a_rock-3.0-1.src.rock b/spec/fixtures/a_repo/manifests/another_user/a_rock-3.0-1.src.rock new file mode 100644 index 0000000..4c20afc Binary files /dev/null and b/spec/fixtures/a_repo/manifests/another_user/a_rock-3.0-1.src.rock differ diff --git a/spec/fixtures/a_repo/manifests/another_user/manifest b/spec/fixtures/a_repo/manifests/another_user/manifest new file mode 100644 index 0000000..185aed0 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/another_user/manifest @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["3.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifests/another_user/manifest-5.1 b/spec/fixtures/a_repo/manifests/another_user/manifest-5.1 new file mode 100644 index 0000000..185aed0 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/another_user/manifest-5.1 @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["3.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifests/another_user/manifest-5.2 b/spec/fixtures/a_repo/manifests/another_user/manifest-5.2 new file mode 100644 index 0000000..185aed0 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/another_user/manifest-5.2 @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["3.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifests/another_user/manifest-5.3 b/spec/fixtures/a_repo/manifests/another_user/manifest-5.3 new file mode 100644 index 0000000..185aed0 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/another_user/manifest-5.3 @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["3.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + } +} diff --git a/spec/fixtures/a_repo/manifests/another_user/manifest-5.4 b/spec/fixtures/a_repo/manifests/another_user/manifest-5.4 new file mode 100644 index 0000000..185aed0 --- /dev/null +++ b/spec/fixtures/a_repo/manifests/another_user/manifest-5.4 @@ -0,0 +1,14 @@ +commands = {} +modules = {} +repository = { + a_rock = { + ["3.0-1"] = { + { + arch = "src" + }, + { + arch = "rockspec" + } + } + } +} diff --git a/spec/fixtures/a_repo/non_lua_file-1.0-1.rockspec b/spec/fixtures/a_repo/non_lua_file-1.0-1.rockspec new file mode 100644 index 0000000..51ef42e --- /dev/null +++ b/spec/fixtures/a_repo/non_lua_file-1.0-1.rockspec @@ -0,0 +1,22 @@ +-- regression test for sailorproject/sailor#138 +rockspec_format = "3.0" +package = "non_lua_file" +version = "1.0-1" +source = { + url = "file://../upstream/non_lua_file-1.0.tar.gz" +} +description = { + summary = "An example rockspec that has a script.", +} +dependencies = { + "lua >= 5.1", +} +build = { + type = "builtin", + modules = {}, + install = { + lua = { + ["sailor.blank-app.htaccess"] = "src/sailor/blank-app/.htaccess", + } + } +} diff --git a/spec/fixtures/a_repo/non_lua_file-1.0-1.src.rock b/spec/fixtures/a_repo/non_lua_file-1.0-1.src.rock new file mode 100644 index 0000000..148f703 Binary files /dev/null and b/spec/fixtures/a_repo/non_lua_file-1.0-1.src.rock differ diff --git a/spec/fixtures/a_repo/non_lua_file-1.0-2.rockspec b/spec/fixtures/a_repo/non_lua_file-1.0-2.rockspec new file mode 100644 index 0000000..f9d2e2c --- /dev/null +++ b/spec/fixtures/a_repo/non_lua_file-1.0-2.rockspec @@ -0,0 +1,22 @@ +-- regression test for sailorproject/sailor#138 +rockspec_format = "3.0" +package = "non_lua_file" +version = "1.0-2" +source = { + url = "file://../upstream/non_lua_file-1.0.tar.gz" +} +description = { + summary = "An example rockspec that has a script.", +} +dependencies = { + "lua >= 5.1", +} +build = { + type = "builtin", + modules = {}, + install = { + lua = { + ["sailor.blank-app.htaccess"] = "src/sailor/blank-app/.htaccess", + } + } +} diff --git a/spec/fixtures/a_repo/non_lua_file-1.0-2.src.rock b/spec/fixtures/a_repo/non_lua_file-1.0-2.src.rock new file mode 100644 index 0000000..06eb9ac Binary files /dev/null and b/spec/fixtures/a_repo/non_lua_file-1.0-2.src.rock differ diff --git a/spec/fixtures/a_rock-1.0-1.rockspec b/spec/fixtures/a_rock-1.0-1.rockspec new file mode 100644 index 0000000..9f15e87 --- /dev/null +++ b/spec/fixtures/a_rock-1.0-1.rockspec @@ -0,0 +1,17 @@ +package = "a_rock" +version = "1.0-1" +source = { + url = "http://localhost:8080/file/a_rock.lua" +} +description = { + summary = "An example rockspec", +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + build = "a_rock.lua" + }, +} diff --git a/spec/fixtures/a_rock-1.0-1.src.rock b/spec/fixtures/a_rock-1.0-1.src.rock new file mode 100644 index 0000000..9d0bb45 Binary files /dev/null and b/spec/fixtures/a_rock-1.0-1.src.rock differ diff --git a/spec/fixtures/a_rock.lua b/spec/fixtures/a_rock.lua new file mode 100644 index 0000000..a564707 --- /dev/null +++ b/spec/fixtures/a_rock.lua @@ -0,0 +1 @@ +return {} diff --git a/spec/fixtures/abc.bz2 b/spec/fixtures/abc.bz2 new file mode 100644 index 0000000..ee78671 Binary files /dev/null and b/spec/fixtures/abc.bz2 differ diff --git a/spec/fixtures/an_upstream_tarball-0.1.tar.gz b/spec/fixtures/an_upstream_tarball-0.1.tar.gz new file mode 100644 index 0000000..518c8cb Binary files /dev/null and b/spec/fixtures/an_upstream_tarball-0.1.tar.gz differ diff --git a/spec/fixtures/bad_pack-0.1-1.rockspec b/spec/fixtures/bad_pack-0.1-1.rockspec new file mode 100644 index 0000000..2393424 --- /dev/null +++ b/spec/fixtures/bad_pack-0.1-1.rockspec @@ -0,0 +1,19 @@ +rockspec_format = "3.0" +package = "bad_pack" +version = "0.1-1" +source = { + url = "http://localhost:8080/file/busted_project-0.1.tar.gz", + dir = "invalid_dir", +} +description = { + summary = "A project that uses Busted tests", +} +build = { + type = "builtin", + modules = { + sum = "sum.lua", + } +} +test = { + type = "busted", +} diff --git a/spec/fixtures/build_only_deps-0.1-1.rockspec b/spec/fixtures/build_only_deps-0.1-1.rockspec new file mode 100644 index 0000000..02d2b47 --- /dev/null +++ b/spec/fixtures/build_only_deps-0.1-1.rockspec @@ -0,0 +1,18 @@ +package = "build_only_deps" +version = "0.1-1" +source = { + url = "file://./a_rock.lua" +} +description = { + summary = "Fixture to test --only-deps", +} +dependencies = { + "lua >= 5.1", + "a_rock 1.0", +} +build = { + type = "builtin", + modules = { + dummy = "a_rock.lua", + } +} diff --git a/spec/fixtures/build_only_deps-0.1-1.src.rock b/spec/fixtures/build_only_deps-0.1-1.src.rock new file mode 100644 index 0000000..74b2d1e Binary files /dev/null and b/spec/fixtures/build_only_deps-0.1-1.src.rock differ diff --git a/spec/fixtures/busted_project-0.1-1.rockspec b/spec/fixtures/busted_project-0.1-1.rockspec new file mode 100644 index 0000000..54ed28a --- /dev/null +++ b/spec/fixtures/busted_project-0.1-1.rockspec @@ -0,0 +1,19 @@ +rockspec_format = "3.0" +package = "busted_project" +version = "0.1-1" +source = { + url = "http://localhost:8080/file/busted_project-0.1.tar.gz", + dir = "busted_project", +} +description = { + summary = "A project that uses Busted tests", +} +build = { + type = "builtin", + modules = { + sum = "sum.lua", + } +} +test = { + type = "busted", +} diff --git a/spec/fixtures/busted_project-0.1.tar.gz b/spec/fixtures/busted_project-0.1.tar.gz new file mode 100644 index 0000000..bd4e055 Binary files /dev/null and b/spec/fixtures/busted_project-0.1.tar.gz differ diff --git a/spec/fixtures/busted_project/spec/sum_spec.lua b/spec/fixtures/busted_project/spec/sum_spec.lua new file mode 100644 index 0000000..2fa0537 --- /dev/null +++ b/spec/fixtures/busted_project/spec/sum_spec.lua @@ -0,0 +1,10 @@ + +local sum = require("sum") + +describe("sum", function() + + it("sums", function() + assert.equal(2, sum.sum(1, 1)) + end) + +end) diff --git a/spec/fixtures/busted_project/sum.lua b/spec/fixtures/busted_project/sum.lua new file mode 100644 index 0000000..ba12805 --- /dev/null +++ b/spec/fixtures/busted_project/sum.lua @@ -0,0 +1,7 @@ +local sum = {} + +function sum.sum(a, b) + return a + b +end + +return sum diff --git a/spec/fixtures/double_deploy_type/ddt.c b/spec/fixtures/double_deploy_type/ddt.c new file mode 100644 index 0000000..f9050a4 --- /dev/null +++ b/spec/fixtures/double_deploy_type/ddt.c @@ -0,0 +1,6 @@ +#include "lua.h" + +int luaopen_ddt(lua_State *L) { + lua_pushstring(L, "ddt.c"); + return 1; +} diff --git a/spec/fixtures/double_deploy_type/ddt1.lua b/spec/fixtures/double_deploy_type/ddt1.lua new file mode 100644 index 0000000..ea1dafe --- /dev/null +++ b/spec/fixtures/double_deploy_type/ddt1.lua @@ -0,0 +1 @@ +return "ddt1" diff --git a/spec/fixtures/double_deploy_type/ddt2.lua b/spec/fixtures/double_deploy_type/ddt2.lua new file mode 100644 index 0000000..b1fa5d7 --- /dev/null +++ b/spec/fixtures/double_deploy_type/ddt2.lua @@ -0,0 +1 @@ +return "ddt2" diff --git a/spec/fixtures/double_deploy_type/ddt_file b/spec/fixtures/double_deploy_type/ddt_file new file mode 100644 index 0000000..988bbfb --- /dev/null +++ b/spec/fixtures/double_deploy_type/ddt_file @@ -0,0 +1 @@ +return "ddt_file" diff --git a/spec/fixtures/double_deploy_type/double_deploy_type-0.1.0-1.rockspec b/spec/fixtures/double_deploy_type/double_deploy_type-0.1.0-1.rockspec new file mode 100644 index 0000000..ff13df0 --- /dev/null +++ b/spec/fixtures/double_deploy_type/double_deploy_type-0.1.0-1.rockspec @@ -0,0 +1,22 @@ +package = "double_deploy_type" +version = "0.1.0-1" +source = { + url = "http://example.com" +} +description = { + homepage = "http://example.com", + license = "*** please specify a license ***" +} +dependencies = {} +build = { + type = "builtin", + modules = { + ddt = "ddt/ddt.c" + }, + install = { + lua = { + ddt = "ddt/ddt1.lua", + ddt_file = "ddt/ddt_file", + } + } +} diff --git a/spec/fixtures/double_deploy_type/double_deploy_type-0.2.0-1.rockspec b/spec/fixtures/double_deploy_type/double_deploy_type-0.2.0-1.rockspec new file mode 100644 index 0000000..15f0c15 --- /dev/null +++ b/spec/fixtures/double_deploy_type/double_deploy_type-0.2.0-1.rockspec @@ -0,0 +1,22 @@ +package = "double_deploy_type" +version = "0.2.0-1" +source = { + url = "http://example.com" +} +description = { + homepage = "http://example.com", + license = "*** please specify a license ***" +} +dependencies = {} +build = { + type = "builtin", + modules = { + ddt = "ddt/ddt.c" + }, + install = { + lua = { + ddt = "ddt/ddt2.lua", + ddt_file = "ddt/ddt_file", + } + } +} diff --git a/spec/fixtures/fixturedep.c b/spec/fixtures/fixturedep.c new file mode 100644 index 0000000..e3fcdd5 --- /dev/null +++ b/spec/fixtures/fixturedep.c @@ -0,0 +1,4 @@ + +int fixturedep_fn() { + return 0; +} diff --git a/spec/fixtures/git_repo/LICENSE b/spec/fixtures/git_repo/LICENSE new file mode 100644 index 0000000..e8c415d --- /dev/null +++ b/spec/fixtures/git_repo/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 Test + +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/spec/fixtures/git_repo/README.md b/spec/fixtures/git_repo/README.md new file mode 100644 index 0000000..220327e --- /dev/null +++ b/spec/fixtures/git_repo/README.md @@ -0,0 +1,3 @@ + + +Test repo diff --git a/spec/fixtures/gpg/private-keys-v1.d/5D2D3F97B88B18604D819EA9DF5B730C75D71B60.key b/spec/fixtures/gpg/private-keys-v1.d/5D2D3F97B88B18604D819EA9DF5B730C75D71B60.key new file mode 100644 index 0000000..26240f5 Binary files /dev/null and b/spec/fixtures/gpg/private-keys-v1.d/5D2D3F97B88B18604D819EA9DF5B730C75D71B60.key differ diff --git a/spec/fixtures/gpg/private-keys-v1.d/B71C36B4EDEB72A047FED1C01BCFF4D08837E3B1.key b/spec/fixtures/gpg/private-keys-v1.d/B71C36B4EDEB72A047FED1C01BCFF4D08837E3B1.key new file mode 100644 index 0000000..e75a2eb Binary files /dev/null and b/spec/fixtures/gpg/private-keys-v1.d/B71C36B4EDEB72A047FED1C01BCFF4D08837E3B1.key differ diff --git a/spec/fixtures/gpg/pubring.kbx b/spec/fixtures/gpg/pubring.kbx new file mode 100644 index 0000000..fc63cbc Binary files /dev/null and b/spec/fixtures/gpg/pubring.kbx differ diff --git a/spec/fixtures/gpg/trustdb.gpg b/spec/fixtures/gpg/trustdb.gpg new file mode 100644 index 0000000..cabca66 Binary files /dev/null and b/spec/fixtures/gpg/trustdb.gpg differ diff --git a/spec/fixtures/invalid_patch-0.1-1.rockspec b/spec/fixtures/invalid_patch-0.1-1.rockspec new file mode 100644 index 0000000..c2ecd16 --- /dev/null +++ b/spec/fixtures/invalid_patch-0.1-1.rockspec @@ -0,0 +1,29 @@ +package = "invalid_patch" +version = "0.1-1" +source = { + -- any valid URL + url = "https://raw.github.com/keplerproject/luarocks/master/src/luarocks/build.lua" +} +description = { + summary = "A rockspec with an invalid patch", +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + build = "build.lua" + }, + patches = { + ["I_am_an_invalid_patch.patch"] = +[[ +diff -Naur luadoc-3.0.1/src/luadoc/doclet/html.lua luadoc-3.0.1-new/src/luadoc/doclet/html.lua +--- luadoc-3.0.1/src/luadoc/doclet/html.lua2007-12-21 15:50:48.000000000 -0200 ++++ luadoc-3.0.1-new/src/luadoc/doclet/html.lua2008-02-28 01:59:53.000000000 -0300 +@@ -18,6 +18,7 @@ +- gabba gabba gabba ++ gobo gobo gobo +]] + } +} diff --git a/spec/fixtures/invalid_say-1.3-1.rockspec b/spec/fixtures/invalid_say-1.3-1.rockspec new file mode 100644 index 0000000..890b4db --- /dev/null +++ b/spec/fixtures/invalid_say-1.3-1.rockspec @@ -0,0 +1,23 @@ +package = "say" +version = "1.3-1" +source = {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{++{ + url = "https://github.com/Olivine-Labs/say/archive/v1.3-1.tar.gz", + dir = "say-1.3-1" +} +description = { + summary = "Lua String Hashing/Indexing Library", + detailed = [[ + Useful for internationalization. + ]], + homepage = "http://olivinelabs.com/busted/", + license = "MIT " +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + ["say.init"] = "src/init.lua" + } +} diff --git a/spec/fixtures/legacyexternalcommand-0.1-1.rockspec b/spec/fixtures/legacyexternalcommand-0.1-1.rockspec new file mode 100644 index 0000000..47bcbb0 --- /dev/null +++ b/spec/fixtures/legacyexternalcommand-0.1-1.rockspec @@ -0,0 +1,17 @@ +package = "legacyexternalcommand" +version = "0.1-1" +source = { + url = "http://localhost:8080/file/legacyexternalcommand.lua" +} +description = { + summary = "an external command with legacy arg parsing", +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + ["luarocks.cmd.external.legacyexternalcommand"] = "legacyexternalcommand.lua", + } +} diff --git a/spec/fixtures/legacyexternalcommand.lua b/spec/fixtures/legacyexternalcommand.lua new file mode 100644 index 0000000..af57537 --- /dev/null +++ b/spec/fixtures/legacyexternalcommand.lua @@ -0,0 +1,34 @@ + +--- Module implementing an external command with legacy arg parsing. +local legacyexternalcommand = {} + +local util = require("luarocks.util") + +legacyexternalcommand.help_summary = "generate legacyexternalcommand package files of a rock." +legacyexternalcommand.help_arguments = "arg1 [arg2]" +legacyexternalcommand.help = [[ +This addon generates legacyexternalcommand package files of a rock. +First argument is the name of a rock, the second argument is optional +and needed when legacyexternalcommand uses another name (usually prefixed by lua-). +Files are generated with the source content of the rock and more +especially the rockspec. So, the rock is downloaded and unpacked. +]] + +--- Driver function for the "legacyexternalcommand" command. +-- @param arg1 string: arg1. +-- @param arg2 string: arg2 (optional) +-- @return boolean: true if successful +function legacyexternalcommand.command(flags, arg1, arg2) + if type(arg1) ~= 'string' then + return nil, "Argument missing. "..util.see_help('legacyexternalcommand') + end + + for k,v in pairs(flags) do + print("FLAGS", k,v) + end + print("ARG1", tostring(arg1)) + print("ARG2", tostring(arg2)) + return true +end + +return legacyexternalcommand diff --git a/spec/fixtures/luajit-fail-1.0-1.rockspec b/spec/fixtures/luajit-fail-1.0-1.rockspec new file mode 100644 index 0000000..f820460 --- /dev/null +++ b/spec/fixtures/luajit-fail-1.0-1.rockspec @@ -0,0 +1,22 @@ +package = "luajit-fail" +version = "1.0-1" +source = { + url = "https://raw.githubusercontent.com/keplerproject/luarocks/master/test/testing.lua", +} +description = { + summary = "Test luajit dependency fail", + detailed = [[ +Fail luajit dependency when running with rockspec_format < 3.0. +]], + homepage = "http://luarocks.org/", + license = "MIT/X license" +} +dependencies = { + "luajit >= 2.0" +} +build = { + type = "builtin", + modules = { + testing = "testing.lua" + } +} diff --git a/spec/fixtures/luajit-success-1.0-1.rockspec b/spec/fixtures/luajit-success-1.0-1.rockspec new file mode 100644 index 0000000..31c930c --- /dev/null +++ b/spec/fixtures/luajit-success-1.0-1.rockspec @@ -0,0 +1,23 @@ +rockspec_format = "3.0" +package = "luajit-success" +version = "1.0-1" +source = { + url = "https://raw.githubusercontent.com/keplerproject/luarocks/master/test/testing.lua", +} +description = { + summary = "Test luajit dependency fail", + detailed = [[ +Use luajit dependency when running with rockspec_format >= 3.0. +]], + homepage = "http://luarocks.org/", + license = "MIT/X license" +} +dependencies = { + "luajit >= 2.0" +} +build = { + type = "builtin", + modules = { + testing = "testing.lua" + } +} diff --git a/spec/fixtures/mixed_deploy_type/mdt.c b/spec/fixtures/mixed_deploy_type/mdt.c new file mode 100644 index 0000000..a162ce2 --- /dev/null +++ b/spec/fixtures/mixed_deploy_type/mdt.c @@ -0,0 +1,6 @@ +#include "lua.h" + +int luaopen_mdt(lua_State *L) { + lua_pushstring(L, "mdt.c"); + return 1; +} diff --git a/spec/fixtures/mixed_deploy_type/mdt.lua b/spec/fixtures/mixed_deploy_type/mdt.lua new file mode 100644 index 0000000..c9ca9c6 --- /dev/null +++ b/spec/fixtures/mixed_deploy_type/mdt.lua @@ -0,0 +1 @@ +return "mdt.lua" diff --git a/spec/fixtures/mixed_deploy_type/mdt_file b/spec/fixtures/mixed_deploy_type/mdt_file new file mode 100644 index 0000000..1a15f7d --- /dev/null +++ b/spec/fixtures/mixed_deploy_type/mdt_file @@ -0,0 +1 @@ +return "mdt_file" diff --git a/spec/fixtures/mixed_deploy_type/mixed_deploy_type-0.1.0-1.rockspec b/spec/fixtures/mixed_deploy_type/mixed_deploy_type-0.1.0-1.rockspec new file mode 100644 index 0000000..91b725d --- /dev/null +++ b/spec/fixtures/mixed_deploy_type/mixed_deploy_type-0.1.0-1.rockspec @@ -0,0 +1,21 @@ +package = "mixed_deploy_type" +version = "0.1.0-1" +source = { + url = "http://example.com" +} +description = { + homepage = "http://example.com", + license = "*** please specify a license ***" +} +dependencies = {} +build = { + type = "builtin", + modules = { + mdt = "mdt/mdt.lua" + }, + install = { + lua = { + mdt_file = "mdt/mdt_file" + } + } +} diff --git a/spec/fixtures/mixed_deploy_type/mixed_deploy_type-0.2.0-1.rockspec b/spec/fixtures/mixed_deploy_type/mixed_deploy_type-0.2.0-1.rockspec new file mode 100644 index 0000000..9ca0318 --- /dev/null +++ b/spec/fixtures/mixed_deploy_type/mixed_deploy_type-0.2.0-1.rockspec @@ -0,0 +1,21 @@ +package = "mixed_deploy_type" +version = "0.2.0-1" +source = { + url = "http://example.com" +} +description = { + homepage = "http://example.com", + license = "*** please specify a license ***" +} +dependencies = {} +build = { + type = "builtin", + modules = { + mdt = "mdt/mdt.c" + }, + install = { + lib = { + mdt_file = "mdt/mdt_file" + } + } +} diff --git a/spec/fixtures/patch_create_delete-0.1-1.rockspec b/spec/fixtures/patch_create_delete-0.1-1.rockspec new file mode 100644 index 0000000..3d55da5 --- /dev/null +++ b/spec/fixtures/patch_create_delete-0.1-1.rockspec @@ -0,0 +1,34 @@ +rockspec_format = "3.0" +package = "patch_create_delete" +version = "0.1-1" +source = { + -- any valid URL + url = "git://github.com/luarocks/luarocks" +} +description = { + summary = "A rockspec with a patch that creates and deletes files", +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + ["luarocks.loader"] = "src/luarocks/loader.lua" + }, + patches = { + ["create_delete.patch"] = +[[ +diff -Naur luarocks/spec/fixtures/patch_create_delete/a_file.txt luarocks-patch/spec/fixtures/patch_create_delete/a_file.txt +--- luarocks/spec/fixtures/patch_create_delete/a_file.txt 2017-10-04 15:39:44.179306674 -0300 ++++ luarocks-patch/spec/fixtures/patch_create_delete/a_file.txt 1969-12-31 21:00:00.000000000 -0300 +@@ -1 +0,0 @@ +-I am a file. +diff -Naur luarocks/spec/fixtures/patch_create_delete/another_file.txt luarocks-patch/spec/fixtures/patch_create_delete/another_file.txt +--- luarocks/spec/fixtures/patch_create_delete/another_file.txt 1969-12-31 21:00:00.000000000 -0300 ++++ luarocks-patch/spec/fixtures/patch_create_delete/another_file.txt 2017-10-04 15:40:12.836306564 -0300 +@@ -0,0 +1 @@ ++I am another file. +]] + } +} diff --git a/spec/fixtures/patch_create_delete/a_file.txt b/spec/fixtures/patch_create_delete/a_file.txt new file mode 100644 index 0000000..6539c24 --- /dev/null +++ b/spec/fixtures/patch_create_delete/a_file.txt @@ -0,0 +1 @@ +I am a file. diff --git a/spec/fixtures/renamed_upstream_tarball-0.1.tar.gz b/spec/fixtures/renamed_upstream_tarball-0.1.tar.gz new file mode 100644 index 0000000..518c8cb Binary files /dev/null and b/spec/fixtures/renamed_upstream_tarball-0.1.tar.gz differ diff --git a/spec/fixtures/with_external_dep-0.1-1.rockspec b/spec/fixtures/with_external_dep-0.1-1.rockspec new file mode 100644 index 0000000..45fea4b --- /dev/null +++ b/spec/fixtures/with_external_dep-0.1-1.rockspec @@ -0,0 +1,25 @@ +package = "with_external_dep" +version = "0.1-1" +source = { + url = "http://localhost:8080/file/with_external_dep.c" +} +description = { + summary = "An example rockspec", +} +external_dependencies = { + FOO = { + header = "foo/foo.h" + } +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + with_external_dep = { + sources = "with_external_dep.c", + incdirs = "$(FOO_INCDIR)", + } + } +} diff --git a/spec/fixtures/with_external_dep.c b/spec/fixtures/with_external_dep.c new file mode 100644 index 0000000..16435d8 --- /dev/null +++ b/spec/fixtures/with_external_dep.c @@ -0,0 +1,10 @@ +#include +#include +#include + +int luaopen_with_external_dep(lua_State* L) { + lua_newtable(L); + lua_pushinteger(L, FOO); + lua_setfield(L, -2, "foo"); + return 1; +} diff --git a/spec/fixtures/with_external_dep/foo/foo.h b/spec/fixtures/with_external_dep/foo/foo.h new file mode 100644 index 0000000..eedd558 --- /dev/null +++ b/spec/fixtures/with_external_dep/foo/foo.h @@ -0,0 +1 @@ +#define FOO 42 diff --git a/spec/fs_spec.lua b/spec/fs_spec.lua new file mode 100644 index 0000000..d5e93c4 --- /dev/null +++ b/spec/fs_spec.lua @@ -0,0 +1,76 @@ +local test_env = require("spec.util.test_env") + +local lfs = require("lfs") +local testing_paths = test_env.testing_paths +local get_tmp_path = test_env.get_tmp_path + +describe("luarocks.fs #integration", function() + + local fs + + describe("fs.download #mock", function() + local tmpfile + local tmpdir + + lazy_setup(function() + test_env.setup_specs(nil, "mock") + local cfg = require("luarocks.core.cfg") + fs = require("luarocks.fs") + cfg.init() + fs.init() + test_env.mock_server_init() + end) + + lazy_teardown(function() + test_env.mock_server_done() + end) + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true and fetches the url argument into the specified filename", function() + tmpfile = get_tmp_path() + assert.truthy(fs.download("http://localhost:8080/file/a_rock.lua", tmpfile)) + local fd = assert(io.open(tmpfile, "r")) + local downloadcontent = assert(fd:read("*a")) + fd:close() + fd = assert(io.open(testing_paths.fixtures_dir .. "/a_rock.lua", "r")) + local originalcontent = assert(fd:read("*a")) + fd:close() + assert.same(downloadcontent, originalcontent) + end) + + it("returns true and fetches the url argument into a file whose name matches the basename of the url if the filename argument is not given", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + fs.change_dir(tmpdir) + assert.truthy(fs.download("http://localhost:8080/file/a_rock.lua")) + tmpfile = tmpdir .. "/a_rock.lua" + local fd = assert(io.open(tmpfile, "r")) + local downloadcontent = assert(fd:read("*a")) + fd:close() + fd = assert(io.open(testing_paths.fixtures_dir .. "/a_rock.lua", "r")) + local originalcontent = assert(fd:read("*a")) + fd:close() + assert.same(downloadcontent, originalcontent) + fs.pop_dir() + end) + + it("returns false and does nothing if the url argument contains a nonexistent file", function() + tmpfile = get_tmp_path() + assert.falsy(fs.download("http://localhost:8080/file/nonexistent", tmpfile)) + end) + + it("returns false and does nothing if the url argument is invalid", function() + assert.falsy(fs.download("invalidurl")) + end) + end) +end) diff --git a/spec/help_spec.lua b/spec/help_spec.lua new file mode 100644 index 0000000..e13f1c3 --- /dev/null +++ b/spec/help_spec.lua @@ -0,0 +1,25 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run + +describe("luarocks help #integration", function() + + before_each(function() + test_env.setup_specs() + end) + + it("with no flags/arguments", function() + assert.is_true(run.luarocks_bool("help")) + end) + + it("invalid argument", function() + assert.is_false(run.luarocks_bool("help invalid")) + end) + + it("config", function() + assert.is_true(run.luarocks_bool("help config")) + end) + + it("luarocks-admin help with no flags/arguments", function() + assert.is_true(run.luarocks_admin_bool(test_env.quiet("help"))) + end) +end) diff --git a/spec/init_spec.lua b/spec/init_spec.lua new file mode 100644 index 0000000..88bd23a --- /dev/null +++ b/spec/init_spec.lua @@ -0,0 +1,235 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run +local testing_paths = test_env.testing_paths +local copy_dir = test_env.copy_dir +local is_win = test_env.TEST_TARGET_OS == "windows" +local write_file = test_env.write_file +local lfs = require("lfs") + +describe("luarocks init #integration", function() + + lazy_setup(function() + test_env.setup_specs() + end) + + it("with no arguments", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init")) + if is_win then + assert.truthy(lfs.attributes(myproject .. "/lua.bat")) + assert.truthy(lfs.attributes(myproject .. "/luarocks.bat")) + else + assert.truthy(lfs.attributes(myproject .. "/lua")) + assert.truthy(lfs.attributes(myproject .. "/luarocks")) + end + assert.truthy(lfs.attributes(myproject .. "/lua_modules")) + assert.truthy(lfs.attributes(myproject .. "/.luarocks")) + assert.truthy(lfs.attributes(myproject .. "/.luarocks/config-" .. test_env.lua_version .. ".lua")) + assert.truthy(lfs.attributes(myproject .. "/.gitignore")) + assert.truthy(lfs.attributes(myproject .. "/myproject-dev-1.rockspec")) + end, finally) + end) + + it("with --no-gitignore", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init --no-gitignore")) + if is_win then + assert.truthy(lfs.attributes(myproject .. "/lua.bat")) + assert.truthy(lfs.attributes(myproject .. "/luarocks.bat")) + else + assert.truthy(lfs.attributes(myproject .. "/lua")) + assert.truthy(lfs.attributes(myproject .. "/luarocks")) + end + assert.truthy(lfs.attributes(myproject .. "/lua_modules")) + assert.truthy(lfs.attributes(myproject .. "/.luarocks")) + assert.truthy(lfs.attributes(myproject .. "/.luarocks/config-" .. test_env.lua_version .. ".lua")) + assert.falsy(lfs.attributes(myproject .. "/.gitignore")) + assert.truthy(lfs.attributes(myproject .. "/myproject-dev-1.rockspec")) + end, finally) + end) + + it("with --no-wrapper-scripts", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init --no-wrapper-scripts")) + assert.falsy(lfs.attributes(myproject .. "/lua.bat")) + assert.falsy(lfs.attributes(myproject .. "/luarocks.bat")) + assert.falsy(lfs.attributes(myproject .. "/lua")) + assert.falsy(lfs.attributes(myproject .. "/luarocks")) + assert.truthy(lfs.attributes(myproject .. "/lua_modules")) + assert.truthy(lfs.attributes(myproject .. "/.luarocks")) + assert.truthy(lfs.attributes(myproject .. "/.luarocks/config-" .. test_env.lua_version .. ".lua")) + assert.truthy(lfs.attributes(myproject .. "/.gitignore")) + assert.truthy(lfs.attributes(myproject .. "/myproject-dev-1.rockspec")) + end, finally) + end) + + it("with --wrapper-dir", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init --wrapper-dir=./bin")) + if is_win then + assert.truthy(lfs.attributes(myproject .. "/bin/lua.bat")) + assert.truthy(lfs.attributes(myproject .. "/bin/luarocks.bat")) + else + assert.truthy(lfs.attributes(myproject .. "/bin/lua")) + assert.truthy(lfs.attributes(myproject .. "/bin/luarocks")) + end + assert.truthy(lfs.attributes(myproject .. "/lua_modules")) + assert.truthy(lfs.attributes(myproject .. "/.luarocks")) + assert.truthy(lfs.attributes(myproject .. "/.luarocks/config-" .. test_env.lua_version .. ".lua")) + assert.truthy(lfs.attributes(myproject .. "/.gitignore")) + assert.truthy(lfs.attributes(myproject .. "/myproject-dev-1.rockspec")) + end, finally) + end) + + it("lua wrapper works", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init")) + if is_win then + assert.truthy(lfs.attributes(myproject .. "/lua.bat")) + assert.truthy(lfs.attributes(myproject .. "/luarocks.bat")) + local pd = assert(io.popen([[echo print(_VERSION) | lua.bat]], "r")) + local output = pd:read("*a") + pd:close() + assert.match("5", output, 1, true) + local fd = io.open("hello.lua", "w") + fd:write("print('hello' .. _VERSION)") + fd:close() + pd = assert(io.popen([[lua.bat hello.lua]], "r")) + output = pd:read("*a") + pd:close() + assert.match("hello", output, 1, true) + else + assert.truthy(lfs.attributes(myproject .. "/lua")) + assert.truthy(lfs.attributes(myproject .. "/luarocks")) + local pd = assert(io.popen([[echo "print('hello ' .. _VERSION)" | ./lua]], "r")) + local output = pd:read("*a") + pd:close() + assert.match("hello", output, 1, true) + local fd = io.open("hello.lua", "w") + fd:write("print('hello' .. _VERSION)") + fd:close() + pd = assert(io.popen([[./lua ./hello.lua]], "r")) + output = pd:read("*a") + pd:close() + assert.match("hello", output, 1, true) + end + end, finally) + end) + + it("with given arguments", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init customname 1.0")) + assert.truthy(lfs.attributes(myproject .. "/customname-1.0-1.rockspec")) + end, finally) + end) + + it("with --lua-versions", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init --lua-versions=5.1,5.2,5.3,5.4")) + local rockspec_name = myproject .. "/myproject-dev-1.rockspec" + assert.truthy(lfs.attributes(rockspec_name)) + local fd = assert(io.open(rockspec_name, "rb")) + local data = fd:read("*a") + fd:close() + assert.truthy(data:find("lua >= 5.1, < 5.5", 1, true)) + end, finally) + end) + + it("in a git repo", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + copy_dir(testing_paths.fixtures_dir .. "/git_repo", myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init")) + local fd = assert(io.open(myproject .. "/myproject-dev-1.rockspec", "r")) + local content = assert(fd:read("*a")) + assert.truthy(content:find("summary = \"Test repo\"")) + assert.truthy(content:find("detailed = .+Test repo.+")) + assert.truthy(content:find("license = \"MIT\"")) + + fd = assert(io.open(myproject .. "/.gitignore", "r")) + content = assert(fd:read("*a")) + assert.truthy(content:find("/foo")) + assert.truthy(content:find("/lua")) + assert.truthy(content:find("/lua_modules")) + end, finally) + end) + + it("does not autodetect config or dependencies as modules of the package", function() + test_env.run_in_tmp(function(tmpdir) + local myproject = tmpdir .. "/myproject" + lfs.mkdir(myproject) + lfs.chdir(myproject) + + assert(run.luarocks("init")) + assert.truthy(lfs.attributes(myproject .. "/.luarocks/config-" .. test_env.lua_version .. ".lua")) + local rockspec_filename = myproject .. "/myproject-dev-1.rockspec" + assert.truthy(lfs.attributes(rockspec_filename)) + + -- install a package locally + write_file("my_dependency-1.0-1.rockspec", [[ + package = "my_dependency" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/my_dependency.lua" + } + build = { + type = "builtin", + modules = { + my_dependency = "my_dependency.lua" + } + } + ]], finally) + write_file(tmpdir .. "/my_dependency.lua", "return {}", finally) + + assert.truthy(run.luarocks("build my_dependency-1.0-1.rockspec")) + assert.truthy(lfs.attributes(myproject .. "/lua_modules/share/lua/" .. test_env.lua_version .."/my_dependency.lua")) + + os.remove(rockspec_filename) + os.remove("my_dependency-1.0-1.rockspec") + + -- re-run init + assert(run.luarocks("init")) + + -- file is recreated + assert.truthy(lfs.attributes(rockspec_filename)) + + local fd = assert(io.open(rockspec_filename, "rb")) + local rockspec = assert(fd:read("*a")) + fd:close() + + assert.no.match("my_dependency", rockspec, 1, true) + assert.no.match("config", rockspec, 1, true) + + end, finally) + end) +end) diff --git a/spec/install_spec.lua b/spec/install_spec.lua new file mode 100644 index 0000000..e6b1ad0 --- /dev/null +++ b/spec/install_spec.lua @@ -0,0 +1,235 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run +local testing_paths = test_env.testing_paths +local env_variables = test_env.env_variables +local write_file = test_env.write_file +local git_repo = require("spec.util.git_repo") +local V = test_env.V + +local extra_rocks = { + "/cprint-${CPRINT}.src.rock", + "/lpeg-${LPEG}.src.rock", + "/luassert-1.7.0-1.src.rock", + "/luasocket-${LUASOCKET}.src.rock", + "/lxsh-${LXSH}.src.rock", + "/luafilesystem-${LUAFILESYSTEM}.src.rock", + "/luafilesystem-${LUAFILESYSTEM_OLD}.src.rock", + "spec/fixtures/a_repo/has_build_dep-1.0-1.all.rock", + "spec/fixtures/a_repo/a_build_dep-1.0-1.all.rock", + "spec/fixtures/a_repo/a_rock-1.0-1.src.rock", +} + +describe("luarocks install #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + describe("basic tests", function() + pending("fails with local flag as root #unix", function() + if test_env.TYPE_TEST_ENV ~= "full" then + assert.is_false(run.luarocks_bool("install --local luasocket ", { USER = "root" } )) + end + end) + + pending("fails with no downloader", function() + if test_env.TYPE_TEST_ENV ~= "full" then + local output = assert(run.luarocks("install https://example.com/rock-1.0.src.rock", { LUAROCKS_CONFIG = testing_paths.testrun_dir .. "/testing_config_no_downloader.lua" } )) + assert.match("no downloader tool", output) + + -- can do http but not https + assert(run.luarocks("install luasocket")) + output = assert(run.luarocks("install https://example.com/rock-1.0.src.rock", { LUAROCKS_CONFIG = testing_paths.testrun_dir .. "/testing_config_no_downloader.lua" } )) + assert.match("no downloader tool", output) + end + end) + + it("only-deps of lxsh show there is no lxsh", function() + assert.is_true(run.luarocks_bool("install lxsh ${LXSH} --only-deps")) + assert.is_false(run.luarocks_bool("show lxsh")) + end) + + it("installs a package with a dependency", function() + assert.is_true(run.luarocks_bool("install has_build_dep")) + assert.is_true(run.luarocks_bool("show a_rock")) + end) + end) + + describe("#namespaces", function() + it("installs a namespaced package from the command-line", function() + assert(run.luarocks_bool("install a_user/a_rock --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert.is_false(run.luarocks_bool("show a_rock 1.0")) + assert(run.luarocks_bool("show a_rock 2.0")) + assert(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/2.0-1/rock_namespace")) + end) + + it("installs a namespaced package given an URL and any string in --namespace", function() + -- This is not a "valid" namespace (as per luarocks.org rules) + -- but we're not doing any format checking in the luarocks codebase + -- so this keeps our options open. + assert(run.luarocks_bool("install --namespace=x.y@z file://" .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.src.rock" )) + assert.truthy(run.luarocks_bool("show a_rock 1.0")) + local fd = assert(io.open(testing_paths.testing_sys_rocks .. "/a_rock/1.0-1/rock_namespace", "r")) + finally(function() fd:close() end) + assert.same("x.y@z", fd:read("*l")) + end) + + it("installs a package with a namespaced dependency", function() + assert(run.luarocks_bool("install has_namespaced_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(run.luarocks_bool("show has_namespaced_dep")) + assert.is_false(run.luarocks_bool("show a_rock 1.0")) + assert(run.luarocks_bool("show a_rock 2.0")) + assert(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/2.0-1/rock_namespace")) + end) + + it("installs a package reusing a namespaced dependency", function() + assert(run.luarocks_bool("install a_user/a_rock --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(run.luarocks_bool("show a_rock 2.0")) + assert(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/2.0-1/rock_namespace")) + local output = run.luarocks("install has_namespaced_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" ) + assert.has.no.match("Missing dependencies", output) + end) + + it("installs a package considering namespace of locally installed package", function() + assert(run.luarocks_bool("install a_user/a_rock --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(run.luarocks_bool("show a_rock 2.0")) + assert(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/2.0-1/rock_namespace")) + local output = run.luarocks("install has_another_namespaced_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" ) + assert.has.match("Missing dependencies", output) + print(output) + assert(run.luarocks_bool("show a_rock 3.0")) + end) + end) + + describe("more complex tests", function() + it('skipping dependency checks', function() + assert.is_true(run.luarocks_bool("install has_build_dep --nodeps")) + assert.is_true(run.luarocks_bool("show has_build_dep")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/has_build_dep")) + end) + + it('handle relative path in --tree #632', function() + local relative_path = "./temp_dir_"..math.random(100000) + if test_env.TEST_TARGET_OS == "windows" then + relative_path = relative_path:gsub("/", "\\") + end + test_env.remove_dir(relative_path) + assert.is.falsy(lfs.attributes(relative_path)) + assert.is_true(run.luarocks_bool("install luafilesystem --tree="..relative_path)) + assert.is.truthy(lfs.attributes(relative_path)) + test_env.remove_dir(relative_path) + assert.is.falsy(lfs.attributes(relative_path)) + end) + + it("only-deps of luasocket packed rock", function() + assert.is_true(run.luarocks_bool("build --pack-binary-rock luasocket ${LUASOCKET}")) + local output = run.luarocks("install --only-deps " .. "luasocket-${LUASOCKET}." .. test_env.platform .. ".rock") + assert.match(V"Successfully installed dependencies for luasocket ${LUASOCKET}", output, 1, true) + assert.is_true(os.remove("luasocket-${LUASOCKET}." .. test_env.platform .. ".rock")) + end) + + it("reinstall", function() + assert.is_true(run.luarocks_bool("build --pack-binary-rock luasocket ${LUASOCKET}")) + assert.is_true(run.luarocks_bool("install " .. "luasocket-${LUASOCKET}." .. test_env.platform .. ".rock")) + assert.is_true(run.luarocks_bool("install --deps-mode=none " .. "luasocket-${LUASOCKET}." .. test_env.platform .. ".rock")) + assert.is_true(os.remove("luasocket-${LUASOCKET}." .. test_env.platform .. ".rock")) + end) + + it("binary rock of cprint", function() + assert.is_true(run.luarocks_bool("build --pack-binary-rock cprint")) + assert.is_true(run.luarocks_bool("install cprint-${CPRINT}." .. test_env.platform .. ".rock")) + assert.is_true(os.remove("cprint-${CPRINT}." .. test_env.platform .. ".rock")) + end) + + it("accepts --no-manifest flag", function() + assert.is_true(run.luarocks_bool("install lxsh ${LXSH}")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/manifest")) + assert.is.truthy(os.remove(testing_paths.testing_sys_rocks .. "/manifest")) + + assert.is_true(run.luarocks_bool("install --no-manifest lxsh ${LXSH}")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/manifest")) + end) + end) + + describe("#build_dependencies", function() + it("install does not install a build dependency", function() + assert(run.luarocks_bool("install has_build_dep")) + assert(run.luarocks_bool("show has_build_dep 1.0")) + assert.falsy(run.luarocks_bool("show a_build_dep 1.0")) + end) + end) + + it("respects luarocks.lock in package #pinning", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test.lua" + } + dependencies = { + "a_rock >= 0.8" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]]) + write_file("test.lua", "return {}") + write_file("luarocks.lock", [[ + return { + dependencies = { + ["a_rock"] = "1.0-1", + } + } + ]]) + + assert.is_true(run.luarocks_bool("make --pack-binary-rock --server=" .. testing_paths.fixtures_dir .. "/a_repo test-1.0-1.rockspec")) + assert.is_true(os.remove("luarocks.lock")) + + assert.is.truthy(lfs.attributes("./test-1.0-1.all.rock")) + + assert.is.falsy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/test/1.0-1/test-1.0-1.rockspec")) + assert.is.falsy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + + print(run.luarocks("install ./test-1.0-1.all.rock --tree=lua_modules --server=" .. testing_paths.fixtures_dir .. "/a_repo")) + + assert.is.truthy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/test/1.0-1/test-1.0-1.rockspec")) + assert.is.truthy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/test/1.0-1/luarocks.lock")) + assert.is.truthy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + assert.is.falsy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/a_rock/2.0-1")) + end, finally) + end) + + describe("#unix install runs build from #git", function() + local git + + lazy_setup(function() + git = git_repo.start() + end) + + lazy_teardown(function() + if git then + git:stop() + end + end) + + it("using --branch", function() + write_file("my_branch-1.0-1.rockspec", [[ + rockspec_format = "3.0" + package = "my_branch" + version = "1.0-1" + source = { + url = "git://localhost/testrock" + } + ]], finally) + assert.is_false(run.luarocks_bool("install --branch unknown-branch ./my_branch-1.0-1.rockspec")) + assert.is_true(run.luarocks_bool("install --branch test-branch ./my_branch-1.0-1.rockspec")) + end) + end) + +end) diff --git a/spec/lint_spec.lua b/spec/lint_spec.lua new file mode 100644 index 0000000..919e4e7 --- /dev/null +++ b/spec/lint_spec.lua @@ -0,0 +1,109 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run +local get_tmp_path = test_env.get_tmp_path +local write_file = test_env.write_file +local lfs = require("lfs") + +local extra_rocks = { + "/say-1.3-1.rockspec" +} + +describe("luarocks lint #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + it("with no flags/arguments", function() + assert.is_false(run.luarocks_bool("lint")) + end) + + it("invalid argument", function() + assert.is_false(run.luarocks_bool("lint invalid")) + end) + + it("OK", function() + assert.is_true(run.luarocks_bool("download --rockspec say 1.3-1")) + local output = run.luarocks("lint say-1.3-1.rockspec") + assert.are.same(output, "") + assert.is_true(os.remove("say-1.3-1.rockspec")) + end) + + describe("mismatch set", function() + local tmpdir + local olddir + + before_each(function() + tmpdir = get_tmp_path() + olddir = lfs.currentdir() + lfs.mkdir(tmpdir) + lfs.chdir(tmpdir) + end) + + after_each(function() + if olddir then + lfs.chdir(olddir) + if tmpdir then + lfs.rmdir(tmpdir) + end + end + end) + + it("mismatch string", function() + write_file("type_mismatch_string-1.0-1.rockspec", [[ + package="type_mismatch_version" + version=1.0 + ]], finally) + assert.is_false(run.luarocks_bool("lint type_mismatch_string-1.0-1.rockspec")) + end) + + it("mismatch version", function() + write_file("type_mismatch_version-1.0-1.rockspec", [[ + package="type_mismatch_version" + version="1.0" + ]], finally) + assert.is_false(run.luarocks_bool("lint type_mismatch_version-1.0-1.rockspec")) + end) + + it("mismatch table", function() + write_file("type_mismatch_table-1.0-1.rockspec", [[ + package="type_mismatch_table" + version="1.0-1" + + source = "not a table" + ]], finally) + assert.is_false(run.luarocks_bool("lint type_mismatch_table-1.0-1.rockspec")) + end) + + it("mismatch no build table", function() + write_file("no_build_table-1.0-1.rockspec", [[ + package = "no_build_table" + version = "0.1-1" + source = { + url = "http://example.com/foo/tar.gz" + } + description = { + summary = "A rockspec with no build field", + } + dependencies = { + "lua >= 5.1" + } + ]], finally) + assert.is_false(run.luarocks_bool("lint no_build_table-1.0-1.rockspec")) + end) + + it("no description field", function() + write_file("nodesc-1.0-1.rockspec", [[ + package = "nodesc" + version = "0.1-1" + source = { + url = "http://example.com/foo/tar.gz" + } + dependencies = { + "lua >= 5.1" + } + ]], finally) + assert.is_false(run.luarocks_bool("lint nodesc-1.0-1.rockspec")) + end) + end) +end) diff --git a/spec/list_spec.lua b/spec/list_spec.lua new file mode 100644 index 0000000..08d8587 --- /dev/null +++ b/spec/list_spec.lua @@ -0,0 +1,34 @@ +local test_env = require("spec.util.test_env") +local V = test_env.V +local run = test_env.run +local testing_paths = test_env.testing_paths + +local extra_rocks = { + "/say-1.0-1.src.rock", + "/say-1.2-1.src.rock" +} + +describe("luarocks list #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + it("with no flags/arguments", function() + local output = run.luarocks("list") + assert.match("luacov", output) + end) + + it("shows version number", function() + local output = run.luarocks("list") + assert.is.truthy(output:find("luacov")) + assert.matches(V"${LUACOV}", output, 1, true) + end) + + it("LuaRocks install outdated and list it", function() + assert.is_true(run.luarocks_bool("install say 1.0-1")) + local output = run.luarocks("list --outdated") + assert.is.truthy(output:find("say")) + assert.matches("1.0-1 < ", output, 1, true) + end) +end) diff --git a/spec/loader_spec.lua b/spec/loader_spec.lua new file mode 100644 index 0000000..bd95a29 --- /dev/null +++ b/spec/loader_spec.lua @@ -0,0 +1,73 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run +local testing_paths = test_env.testing_paths +local write_file = test_env.write_file + +describe("luarocks.loader", function() + + before_each(function() + test_env.setup_specs() + end) + + describe("#integration", function() + it("respects version constraints", function() + test_env.run_in_tmp(function(tmpdir) + write_file("rock_b_01.lua", "print('ROCK B 0.1'); return {}") + write_file("rock_b-0.1-1.rockspec", [[ + package = "rock_b" + version = "0.1-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/rock_b_01.lua" + } + build = { + type = "builtin", + modules = { + rock_b = "rock_b_01.lua" + } + } + ]]) + + write_file("rock_b_10.lua", "print('ROCK B 1.0'); return {}") + write_file("rock_b-1.0-1.rockspec", [[ + package = "rock_b" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/rock_b_10.lua" + } + build = { + type = "builtin", + modules = { + rock_b = "rock_b_10.lua" + } + } + ]]) + + write_file("rock_a.lua", "require('rock_b'); return {}") + write_file("rock_a-2.0-1.rockspec", [[ + package = "rock_a" + version = "2.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/rock_a.lua" + } + dependencies = { + "rock_b < 1.0", + } + build = { + type = "builtin", + modules = { + rock_a = "rock_a.lua" + } + } + ]]) + + print(run.luarocks("make --server=" .. testing_paths.fixtures_dir .. "/a_repo --tree=" .. testing_paths.testing_tree .. " ./rock_b-0.1-1.rockspec")) + print(run.luarocks("make --server=" .. testing_paths.fixtures_dir .. "/a_repo --tree=" .. testing_paths.testing_tree .. " ./rock_b-1.0-1.rockspec --keep")) + print(run.luarocks("make --server=" .. testing_paths.fixtures_dir .. "/a_repo --tree=" .. testing_paths.testing_tree .. " ./rock_a-2.0-1.rockspec")) + + local output = run.lua([[-e "require 'luarocks.loader'; require('rock_a')"]]) + + assert.matches("ROCK B 0.1", output, 1, true) + end, finally) + end) + end) +end) diff --git a/spec/make_spec.lua b/spec/make_spec.lua new file mode 100644 index 0000000..b057263 --- /dev/null +++ b/spec/make_spec.lua @@ -0,0 +1,387 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run +local testing_paths = test_env.testing_paths +local env_variables = test_env.env_variables +local write_file = test_env.write_file + +local extra_rocks = { + "/luasocket-${LUASOCKET}.src.rock", + "/luasocket-${LUASOCKET}.rockspec", + "/lpeg-${LPEG}.src.rock", + "/lxsh-${LXSH}.src.rock", + "/lxsh-${LXSH}.rockspec" +} + +describe("luarocks make #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + it("with no flags/arguments", function() + finally(function() + lfs.chdir(testing_paths.testrun_dir) + test_env.remove_dir("empty") + end) + assert(lfs.mkdir("empty")) + assert(lfs.chdir("empty")) + assert.is_false(run.luarocks_bool("make")) + end) + + it("with rockspec", function() + finally(function() + -- delete downloaded and unpacked files + lfs.chdir(testing_paths.testrun_dir) + test_env.remove_dir("luasocket-${LUASOCKET}") + os.remove("luasocket-${LUASOCKET}.src.rock") + end) + + -- make luasocket + assert.is_true(run.luarocks_bool("download --source luasocket ${LUASOCKET}")) + assert.is_true(run.luarocks_bool("unpack luasocket-${LUASOCKET}.src.rock")) + lfs.chdir("luasocket-${LUASOCKET}/luasocket/") + assert.is_true(run.luarocks_bool("make luasocket-${LUASOCKET}.rockspec")) + + -- test it + assert.is_true(run.luarocks_bool("show luasocket")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/luasocket/${LUASOCKET}/luasocket-${LUASOCKET}.rockspec")) + end) + + it("--no-doc", function() + finally(function() + lfs.chdir(testing_paths.testrun_dir) + test_env.remove_dir("luasocket-${LUASOCKET}") + os.remove("luasocket-${LUASOCKET}.src.rock") + end) + + assert.is_true(run.luarocks_bool("download --source luasocket ${LUASOCKET}")) + assert.is_true(run.luarocks_bool("unpack luasocket-${LUASOCKET}.src.rock")) + lfs.chdir("luasocket-${LUASOCKET}/luasocket") + assert.is_true(run.luarocks_bool("make --no-doc luasocket-${LUASOCKET}.rockspec")) + + assert.is_true(run.luarocks_bool("show luasocket")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/luasocket/${LUASOCKET}/doc")) + end) + + it("--only-deps", function() + local rockspec = "build_only_deps-0.1-1.rockspec" + local src_rock = testing_paths.fixtures_dir .. "/build_only_deps-0.1-1.src.rock" + + test_env.remove_dir("build_only_deps-0.1-1/") + assert.is_true(run.luarocks_bool("unpack " .. src_rock)) + lfs.chdir("build_only_deps-0.1-1/") + assert.is_true(run.luarocks_bool("make " .. rockspec .. " --only-deps")) + assert.is_false(run.luarocks_bool("show build_only_deps")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/build_only_deps/0.1-1/build_only_deps-0.1-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + end) + + describe("LuaRocks making rockspecs (using lxsh)", function() + --download lxsh and unpack it + before_each(function() + assert.is_true(run.luarocks_bool("download --source lxsh ${LXSH}")) + assert.is_true(run.luarocks_bool("unpack lxsh-${LXSH}.src.rock")) + assert.is_true(lfs.chdir("lxsh-${LXSH}/lxsh-${LXSH_V}-1/")) + end) + + -- delete downloaded and unpacked files + after_each(function() + assert(lfs.chdir(testing_paths.testrun_dir)) + test_env.remove_dir("lxsh-${LXSH}") + assert.is_true(os.remove("lxsh-${LXSH}.src.rock")) + end) + + it("default rockspec", function() + assert.is_true(run.luarocks_bool("new_version lxsh-${LXSH}.rockspec")) + assert.is_true(run.luarocks_bool("make")) + + assert.is_true(run.luarocks_bool("show lxsh")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH_V}-3/lxsh-${LXSH_V}-3.rockspec")) + end) + + it("unnamed rockspec", function() + finally(function() + os.remove("rockspec") + end) + + test_env.copy("lxsh-${LXSH}.rockspec", "rockspec") + assert.is_true(run.luarocks_bool("make")) + + assert.is_true(run.luarocks_bool("show lxsh")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("ambiguous rockspec", function() + assert.is.truthy(os.rename("lxsh-${LXSH}.rockspec", "lxsh2-${LXSH}.rockspec")) + local output = run.luarocks("make") + assert.is.truthy(output:match("Error: Inconsistency between rockspec filename")) + + assert.is_false(run.luarocks_bool("show lxsh")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("ambiguous unnamed rockspec", function() + assert.is.truthy(os.rename("lxsh-${LXSH}.rockspec", "1_rockspec")) + test_env.copy("1_rockspec", "2_rockspec") + local output = run.luarocks("make") + assert.is.truthy(output:match("Error: Please specify which rockspec file to use")) + + assert.is_false(run.luarocks_bool("show lxsh")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/lxsh/${LXSH}/lxsh-${LXSH}.rockspec")) + end) + + it("pack binary rock", function() + assert.is_true(run.luarocks_bool("make --deps-mode=none --pack-binary-rock")) + assert.is.truthy(lfs.attributes("lxsh-${LXSH}.all.rock")) + end) + end) + + it("supports --pin #pinning", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test.lua" + } + dependencies = { + "a_rock 1.0" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]]) + write_file("test.lua", "return {}") + + assert.is_true(run.luarocks_bool("make --server=" .. testing_paths.fixtures_dir .. "/a_repo --pin --tree=lua_modules")) + assert.is.truthy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/test/1.0-1/test-1.0-1.rockspec")) + assert.is.truthy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + local lockfilename = "./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/test/1.0-1/luarocks.lock" + assert.is.truthy(lfs.attributes(lockfilename)) + local lockdata = loadfile(lockfilename)() + assert.same({ + dependencies = { + ["a_rock"] = "1.0-1", + ["lua"] = test_env.lua_version .. "-1", + } + }, lockdata) + end, finally) + end) + + it("respects luarocks.lock when present #pinning", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-2.0-1.rockspec", [[ + package = "test" + version = "2.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test.lua" + } + dependencies = { + "a_rock >= 0.8" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]]) + write_file("test.lua", "return {}") + write_file("luarocks.lock", [[ + return { + dependencies = { + ["a_rock"] = "1.0-1", + } + } + ]]) + + print(run.luarocks("make --server=" .. testing_paths.fixtures_dir .. "/a_repo --tree=lua_modules")) + assert.is.truthy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/test/2.0-1/test-2.0-1.rockspec")) + assert.is.truthy(lfs.attributes("./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/a_rock/1.0-1/a_rock-1.0-1.rockspec")) + local lockfilename = "./lua_modules/lib/luarocks/rocks-" .. test_env.lua_version .. "/test/2.0-1/luarocks.lock" + assert.is.truthy(lfs.attributes(lockfilename)) + local lockdata = loadfile(lockfilename)() + assert.same({ + dependencies = { + ["a_rock"] = "1.0-1", + } + }, lockdata) + end, finally) + end) + + describe("#ddt upgrading rockspecs with double deploy types", function() + local deploy_lib_dir = testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION + local deploy_lua_dir = testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION + local so = test_env.lib_extension + + before_each(function() + test_env.copy_dir(testing_paths.fixtures_dir .. "/double_deploy_type", "ddt") + end) + + after_each(function() + test_env.remove_dir("ddt") + os.remove("ddt."..test_env.lib_extension) + end) + + it("when upgrading", function() + assert.is_true(run.luarocks_bool("make ddt/double_deploy_type-0.1.0-1.rockspec")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt.lua")) + assert.same("ddt1", loadfile(deploy_lua_dir.."/ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt_file")) + assert.is.falsy(lfs.attributes(deploy_lib_dir.."/ddt."..so.."~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt.lua~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt_file~")) + + assert.is_true(run.luarocks_bool("make ddt/double_deploy_type-0.2.0-1.rockspec")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt.lua")) + assert.same("ddt2", loadfile(deploy_lua_dir.."/ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt_file")) + assert.is.falsy(lfs.attributes(deploy_lib_dir.."/ddt."..so.."~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt.lua~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt_file~")) + end) + + it("modules with same name from lua/ and lib/ when upgrading with --keep", function() + assert.is_true(run.luarocks_bool("make ddt/double_deploy_type-0.1.0-1.rockspec")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt.lua")) + assert.same("ddt1", loadfile(deploy_lua_dir.."/ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt_file")) + assert.is.falsy(lfs.attributes(deploy_lib_dir.."/ddt."..so.."~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt.lua~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt_file~")) + + assert.is_true(run.luarocks_bool("make ddt/double_deploy_type-0.2.0-1.rockspec --keep")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt.lua")) + assert.same("ddt2", loadfile(deploy_lua_dir.."/ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt_file")) + assert.is.falsy(lfs.attributes(deploy_lib_dir.."/ddt."..so.."~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt.lua~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt_file~")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/double_deploy_type_0_1_0_1-ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/double_deploy_type_0_1_0_1-ddt.lua")) + assert.same("ddt1", loadfile(deploy_lua_dir.."/double_deploy_type_0_1_0_1-ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/double_deploy_type_0_1_0_1-ddt_file")) + end) + + it("modules with same name from lua/ and lib/ when downgrading", function() + assert.is_true(run.luarocks_bool("make ddt/double_deploy_type-0.2.0-1.rockspec")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt.lua")) + assert.same("ddt2", loadfile(deploy_lua_dir.."/ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt_file")) + assert.is.falsy(lfs.attributes(deploy_lib_dir.."/ddt."..so.."~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt.lua~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt_file~")) + + assert.is_true(run.luarocks_bool("make ddt/double_deploy_type-0.1.0-1.rockspec")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt.lua")) + assert.same("ddt1", loadfile(deploy_lua_dir.."/ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt_file")) + assert.is.falsy(lfs.attributes(deploy_lib_dir.."/ddt."..so.."~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt.lua~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt_file~")) + end) + + it("modules with same name from lua/ and lib/ when downgrading with --keep", function() + assert.is_true(run.luarocks_bool("make ddt/double_deploy_type-0.2.0-1.rockspec")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt.lua")) + assert.same("ddt2", loadfile(deploy_lua_dir.."/ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt_file")) + assert.is.falsy(lfs.attributes(deploy_lib_dir.."/ddt."..so.."~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt.lua~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt_file~")) + + assert.is_true(run.luarocks_bool("make ddt/double_deploy_type-0.1.0-1.rockspec --keep")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt.lua")) + assert.same("ddt2", loadfile(deploy_lua_dir.."/ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/ddt_file")) + assert.is.falsy(lfs.attributes(deploy_lib_dir.."/ddt."..so.."~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt.lua~")) + assert.is.falsy(lfs.attributes(deploy_lua_dir.."/ddt_file~")) + assert.is.truthy(lfs.attributes(deploy_lib_dir.."/double_deploy_type_0_1_0_1-ddt."..so)) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/double_deploy_type_0_1_0_1-ddt.lua")) + assert.same("ddt1", loadfile(deploy_lua_dir.."/double_deploy_type_0_1_0_1-ddt.lua")()) + assert.is.truthy(lfs.attributes(deploy_lua_dir.."/double_deploy_type_0_1_0_1-ddt_file")) + end) + end) + + describe("upgrading rockspecs with mixed deploy types", function() + before_each(function() + test_env.copy_dir(testing_paths.fixtures_dir .. "/mixed_deploy_type", "mdt") + end) + + after_each(function() + test_env.remove_dir("mdt") + os.remove("mdt."..test_env.lib_extension) + end) + + it("modules with same name from lua/ and lib/ when upgrading", function() + assert.is_true(run.luarocks_bool("make mdt/mixed_deploy_type-0.1.0-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt.lua")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + + assert.is_true(run.luarocks_bool("make mdt/mixed_deploy_type-0.2.0-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt."..test_env.lib_extension)) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt.lua")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt.lua")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt_file")) + end) + + it("modules with same name from lua/ and lib/ when upgrading with --keep", function() + assert.is_true(run.luarocks_bool("make mdt/mixed_deploy_type-0.1.0-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt.lua")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + + assert.is_true(run.luarocks_bool("make mdt/mixed_deploy_type-0.2.0-1.rockspec --keep")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt."..test_env.lib_extension)) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt.lua")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt.lua")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt_file")) + end) + + it("modules with same name from lua/ and lib/ when downgrading", function() + assert.is_true(run.luarocks_bool("make mdt/mixed_deploy_type-0.2.0-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt."..test_env.lib_extension)) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + + assert.is_true(run.luarocks_bool("make mdt/mixed_deploy_type-0.1.0-1.rockspec")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt."..test_env.lib_extension)) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt."..test_env.lib_extension)) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt_file")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt.lua")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt.lua")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt_file")) + end) + + it("modules with same name from lua/ and lib/ when downgrading with --keep", function() + assert.is_true(run.luarocks_bool("make mdt/mixed_deploy_type-0.2.0-1.rockspec")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt."..test_env.lib_extension)) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + + assert.is_true(run.luarocks_bool("make mdt/mixed_deploy_type-0.1.0-1.rockspec --keep")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt."..test_env.lib_extension)) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt.lua")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mdt_file")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt.lua")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_tree .. "/share/lua/"..env_variables.LUA_VERSION.."/mixed_deploy_type_0_1_0_1-mdt_file")) + end) + end) +end) diff --git a/spec/pack_spec.lua b/spec/pack_spec.lua new file mode 100644 index 0000000..2a2fb3a --- /dev/null +++ b/spec/pack_spec.lua @@ -0,0 +1,102 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run +local testing_paths = test_env.testing_paths +local write_file = test_env.write_file + +describe("luarocks pack #integration", function() + + lazy_setup(function() + test_env.setup_specs() + end) + + describe("#mock", function() + + lazy_setup(function() + test_env.setup_specs(extra_rocks, "mock") + test_env.mock_server_init() + end) + + lazy_teardown(function() + test_env.mock_server_done() + end) + + it("can pack a rockspec into a .src.rock", function() + finally(function() + os.remove("a_rock-1.0-1.src.rock") + end) + assert(run.luarocks_bool("download --rockspec --server=" .. testing_paths.fixtures_dir .. "/a_repo a_rock 1.0-1")) + assert(run.luarocks_bool("pack a_rock-1.0-1.rockspec")) + assert.is_truthy(lfs.attributes("a_rock-1.0-1.src.rock")) + end) + + it("can pack a rockspec with a bare file:// in the url", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test.lua" + } + dependencies = { + "a_rock 1.0" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]], finally) + write_file("test.lua", "return {}", finally) + + assert.is.truthy(run.luarocks_bool("pack test-1.0-1.rockspec")) + assert.is.truthy(lfs.attributes("test-1.0-1.src.rock")) + + assert.is.truthy(run.luarocks_bool("unpack test-1.0-1.src.rock")) + assert.is.truthy(lfs.attributes("test-1.0-1/test.lua")) + end, finally) + end) + + it("can pack a rockspec with a bare file:// fails if doesn't exist", function() + test_env.run_in_tmp(function(tmpdir) + write_file("test-1.0-1.rockspec", [[ + package = "test" + version = "1.0-1" + source = { + url = "file://]] .. tmpdir:gsub("\\", "/") .. [[/test_doesnt_exist.lua" + } + dependencies = { + "a_rock 1.0" + } + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + ]], finally) + + assert.is.falsy(run.luarocks_bool("pack test-1.0-1.rockspec")) + assert.is.falsy(lfs.attributes("test-1.0-1.src.rock")) + end, finally) + end) + + + it("fails packing a rockspec into a .src.rock if dir doesn't exist", function() + local output = run.luarocks("pack " .. testing_paths.fixtures_dir .. "/bad_pack-0.1-1.rockspec") + assert.match("Directory invalid_dir not found", output) + assert.is_falsy(lfs.attributes("bad_pack-0.1-1.src.rock")) + end) + + describe("namespaced dependencies", function() + it("can pack rockspec with namespaced dependencies", function() + finally(function() + os.remove("has_namespaced_dep-1.0-1.src.rock") + end) + assert(run.luarocks_bool("pack " .. testing_paths.fixtures_dir .. "/a_repo/has_namespaced_dep-1.0-1.rockspec")) + assert.is_truthy(lfs.attributes("has_namespaced_dep-1.0-1.src.rock")) + end) + end) + end) +end) diff --git a/spec/path_spec.lua b/spec/path_spec.lua new file mode 100644 index 0000000..1ad6b3a --- /dev/null +++ b/spec/path_spec.lua @@ -0,0 +1,58 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run + +describe("luarocks path #integration", function() + before_each(function() + test_env.setup_specs() + end) + + it("runs", function() + local output = run.luarocks("path") + assert.match("LUA_PATH=", output) + assert.match("LUA_CPATH=", output) + end) + + if _VERSION:match("[23]") then + local v = _VERSION:gsub("Lua (%d+)%.(%d+)", "%1_%2") + + it("with LUA_PATH_"..v, function() + local output = run.luarocks("path", { + ["LUA_PATH_"..v] = package.path, + }) + assert.match("LUA_PATH_"..v.."=", output) + end) + + it("with LUA_CPATH_"..v, function() + local output = run.luarocks("path", { + ["LUA_CPATH_"..v] = package.cpath, + }) + assert.match("LUA_CPATH_"..v.."=", output) + end) + + it("with LUA_PATH_"..v.." and LUA_CPATH_"..v, function() + local output = run.luarocks("path", { + ["LUA_PATH_"..v] = package.path, + ["LUA_CPATH_"..v] = package.cpath, + }) + assert.match("LUA_PATH_"..v.."=", output) + assert.match("LUA_CPATH_"..v.."=", output) + end) + + end + + it("--bin", function() + assert.is_true(run.luarocks_bool("path --bin")) + end) + + it("--lr-path", function() + assert.is_true(run.luarocks_bool("path --lr-path")) + end) + + it("--lr-cpath", function() + assert.is_true(run.luarocks_bool("path --lr-cpath")) + end) + + it("--tree", function() + assert.is_true(run.luarocks_bool("path --tree=lua_modules")) + end) +end) diff --git a/spec/quick/admin_make_manifest.q b/spec/quick/admin_make_manifest.q new file mode 100644 index 0000000..527e86a --- /dev/null +++ b/spec/quick/admin_make_manifest.q @@ -0,0 +1,46 @@ +SUITE: luarocks-admin make_manifest + +================================================================================ +TEST: runs + +FILE: test-1.0-1.rockspec +-------------------------------------------------------------------------------- +package = "test" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/test.lua" +} +build = { + type = "builtin", + modules = { + test = "test.lua" + } +} +-------------------------------------------------------------------------------- + +FILE: test.lua +-------------------------------------------------------------------------------- +return {} +-------------------------------------------------------------------------------- + +RUN: luarocks make --pack-binary-rock ./test-1.0-1.rockspec + +RUN: luarocks-admin make_manifest . + +FILE_CONTENTS: ./manifest-%{lua_version} +-------------------------------------------------------------------------------- +commands = {} +modules = {} +repository = { + test = { + ["1.0-1"] = { + { + arch = "all" + }, + { + arch = "rockspec" + } + } + } +} +-------------------------------------------------------------------------------- diff --git a/spec/quick/build.q b/spec/quick/build.q new file mode 100644 index 0000000..d0d6a89 --- /dev/null +++ b/spec/quick/build.q @@ -0,0 +1,405 @@ +SUITE: luarocks build + +================================================================================ +TEST: fails when given invalid argument +RUN: luarocks build aoesuthaoeusahtoeustnaou --only-server=localhost +EXIT: 1 +STDERR: +-------------------------------------------------------------------------------- +Could not find a result named aoesuthaoeusahtoeustnaou +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: with no arguments behaves as luarocks make + +FILE: c_module-1.0-1.rockspec +-------------------------------------------------------------------------------- +package = "c_module" +version = "1.0-1" +source = { + url = "http://example.com/c_module" +} +build = { + type = "builtin", + modules = { + c_module = { "c_module.c" } + } +} +-------------------------------------------------------------------------------- +FILE: c_module.c +-------------------------------------------------------------------------------- +#include +#include + +int luaopen_c_module(lua_State* L) { + lua_newtable(L); + lua_pushinteger(L, 1); + lua_setfield(L, -2, "c_module"); + return 1; +} +-------------------------------------------------------------------------------- +RUN: luarocks build +EXISTS: c_module.%{lib_extension} + + + +================================================================================ +TEST: defaults to builtin type + +FILE: a_rock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "a_rock" +version = "1.0-1" +source = { + url = "file://%{url(%{fixtures_dir})}/a_rock.lua" +} +description = { + summary = "An example rockspec", +} +dependencies = { + "lua >= 5.1" +} +build = { + modules = { + build = "a_rock.lua" + }, +} +-------------------------------------------------------------------------------- +RUN: luarocks build a_rock-1.0-1.rockspec +RUN: luarocks show a_rock +STDOUT: +-------------------------------------------------------------------------------- +a_rock 1.0 +-------------------------------------------------------------------------------- + + +================================================================================ +TEST: fails if no permissions to access the specified tree #unix + +RUN: luarocks build --tree=/usr ./a_rock-1.0-1.rockspec +EXIT: 2 +STDERR: +-------------------------------------------------------------------------------- +You may want to run as a privileged user, +or use --local to install into your local tree +or run 'luarocks config local_by_default true' to make --local the default. + +(You may need to configure your Lua package paths +to use the local tree, see 'luarocks path --help') +-------------------------------------------------------------------------------- + +We show the OS permission denied error, so we don't show the --force-lock +message. + +NOT_STDERR: +-------------------------------------------------------------------------------- +try --force-lock +-------------------------------------------------------------------------------- + +NOT_EXISTS: %{testing_sys_rocks}/a_rock/1.0-1/a_rock-1.0-1.rockspec + + + +================================================================================ +TEST: fails if tree is locked, --force-lock overrides #unix + +FILE: a_rock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "a_rock" +version = "1.0-1" +source = { + url = "file://%{url(%{fixtures_dir})}/a_rock.lua" +} +description = { + summary = "An example rockspec", +} +dependencies = { + "lua >= 5.1" +} +build = { + modules = { + build = "a_rock.lua" + }, +} +-------------------------------------------------------------------------------- + +FILE: %{testing_tree}/lockfile.lfs +-------------------------------------------------------------------------------- +dummy lock file for testing +-------------------------------------------------------------------------------- + +RUN: luarocks build --tree=%{testing_tree} ./a_rock-1.0-1.rockspec +EXIT: 4 +STDERR: +-------------------------------------------------------------------------------- +requires exclusive write access +try --force-lock +-------------------------------------------------------------------------------- + +RUN: luarocks build --tree=%{testing_tree} ./a_rock-1.0-1.rockspec --force-lock +EXIT: 0 + + + +================================================================================ +TEST: fails if no permissions to access the parent #unix + +RUN: luarocks build --tree=/usr/invalid ./a_rock-1.0-1.rockspec +EXIT: 2 +STDERR: +-------------------------------------------------------------------------------- +Error: /usr/invalid/lib/luarocks/rocks-%{lua_version} does not exist +and your user does not have write permissions in /usr + +You may want to run as a privileged user, +or use --local to install into your local tree +or run 'luarocks config local_by_default true' to make --local the default. + +(You may need to configure your Lua package paths +to use the local tree, see 'luarocks path --help') +-------------------------------------------------------------------------------- + +We show the OS permission denied error, so we don't show the --force-lock +message. + +NOT_STDERR: +-------------------------------------------------------------------------------- +try --force-lock +-------------------------------------------------------------------------------- + +NOT_EXISTS: %{testing_sys_rocks}/a_rock/1.0-1/a_rock-1.0-1.rockspec + + + +================================================================================ +TEST: luarocks build: do not rebuild when already installed + +FILE: a_rock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "a_rock" +version = "1.0-1" +source = { + url = "file://%{url(%{fixtures_dir})}/a_rock.lua" +} +description = { + summary = "An example rockspec", +} +dependencies = { + "lua >= 5.1" +} +build = { + modules = { + build = "a_rock.lua" + }, +} +-------------------------------------------------------------------------------- +RUN: luarocks build a_rock-1.0-1.rockspec + +RUN: luarocks show a_rock +STDOUT: +-------------------------------------------------------------------------------- +a_rock 1.0 +-------------------------------------------------------------------------------- + +RUN: luarocks build a_rock-1.0-1.rockspec +STDOUT: +-------------------------------------------------------------------------------- +a_rock 1.0-1 is already installed +Use --force to reinstall +-------------------------------------------------------------------------------- + + +RUN: luarocks build a_rock-1.0-1.rockspec --force +NOT_STDOUT: +-------------------------------------------------------------------------------- +a_rock 1.0-1 is already installed +Use --force to reinstall +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: supports --pin #pinning + +FILE: test-1.0-1.rockspec +-------------------------------------------------------------------------------- +package = "test" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/test.lua" +} +dependencies = { + "a_rock >= 0.8" +} +build = { + type = "builtin", + modules = { + test = "test.lua" + } +} +-------------------------------------------------------------------------------- + +FILE: test.lua +-------------------------------------------------------------------------------- +return {} +-------------------------------------------------------------------------------- + +RUN: luarocks build --only-server=%{fixtures_dir}/a_repo test-1.0-1.rockspec --pin --tree=lua_modules + +EXISTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/test/1.0-1/test-1.0-1.rockspec +EXISTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/a_rock/2.0-1/a_rock-2.0-1.rockspec + +EXISTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/test/1.0-1/luarocks.lock + +FILE_CONTENTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/test/1.0-1/luarocks.lock +-------------------------------------------------------------------------------- +return { + dependencies = { + a_rock = "2.0-1", + lua = "%{lua_version}-1" + } +} +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: supports --pin --only-deps #pinning + +FILE: test-1.0-1.rockspec +-------------------------------------------------------------------------------- +package = "test" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/test.lua" +} +dependencies = { + "a_rock >= 0.8" +} +build = { + type = "builtin", + modules = { + test = "test.lua" + } +} +-------------------------------------------------------------------------------- + +FILE: test.lua +-------------------------------------------------------------------------------- +return {} +-------------------------------------------------------------------------------- + +RUN: luarocks build --only-server=%{fixtures_dir}/a_repo test-1.0-1.rockspec --pin --only-deps --tree=lua_modules + +NOT_EXISTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/test/1.0-1/test-1.0-1.rockspec +EXISTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/a_rock/2.0-1/a_rock-2.0-1.rockspec + +EXISTS: ./luarocks.lock + +FILE_CONTENTS: ./luarocks.lock +-------------------------------------------------------------------------------- +return { + dependencies = { + a_rock = "2.0-1", + lua = "%{lua_version}-1" + } +} +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: installs bin entries correctly + +FILE: test-1.0-1.rockspec +-------------------------------------------------------------------------------- +package = "test" +version = "1.0-1" +source = { + url = "file://%{url(%{fixtures_dir})}/an_upstream_tarball-0.1.tar.gz", + dir = "an_upstream_tarball-0.1", +} +build = { + type = "builtin", + modules = { + my_module = "src/my_module.lua" + }, + install = { + bin = { + "src/my_module.lua" + } + } +} +-------------------------------------------------------------------------------- + +RUN: luarocks build test-1.0-1.rockspec --tree=lua_modules + +RM: %{fixtures_dir}/bin/something.lua + +EXISTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/test/1.0-1/test-1.0-1.rockspec + +FILE_CONTENTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/test/1.0-1/rock_manifest +-------------------------------------------------------------------------------- +rock_manifest = { + bin = { + ["my_module.lua"] = "25884dbf5be7114791018a48199d4c04" + }, + lua = { + ["my_module.lua"] = "25884dbf5be7114791018a48199d4c04" + }, + ["test-1.0-1.rockspec"] = +} +-------------------------------------------------------------------------------- + +EXISTS: ./lua_modules/bin/my_module.lua%{wrapper_extension} + + + +================================================================================ +TEST: downgrades directories correctly + +FILE: mytest-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "mytest" +version = "1.0-1" +source = { + url = "file://%{url(%{fixtures_dir})}/an_upstream_tarball-0.1.tar.gz", + dir = "an_upstream_tarball-0.1", +} +build = { + modules = { + ["parent.child.my_module"] = "src/my_module.lua" + }, +} +-------------------------------------------------------------------------------- + +FILE: mytest-2.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "mytest" +version = "2.0-1" +source = { + url = "file://%{url(%{fixtures_dir})}/an_upstream_tarball-0.1.tar.gz", + dir = "an_upstream_tarball-0.1", +} +build = { + modules = { + ["parent.child.my_module"] = "src/my_module.lua" + }, +} +-------------------------------------------------------------------------------- + +RUN: luarocks build ./mytest-2.0-1.rockspec +EXISTS: %{testing_sys_tree}/share/lua/%{lua_version}/parent/child/my_module.lua + +RUN: luarocks build ./mytest-1.0-1.rockspec +EXISTS: %{testing_sys_tree}/share/lua/%{lua_version}/parent/child/my_module.lua + +RUN: luarocks build ./mytest-2.0-1.rockspec +EXISTS: %{testing_sys_tree}/share/lua/%{lua_version}/parent/child/my_module.lua diff --git a/spec/quick/cmd.q b/spec/quick/cmd.q new file mode 100644 index 0000000..acde92b --- /dev/null +++ b/spec/quick/cmd.q @@ -0,0 +1,36 @@ +SUITE: luarocks CLI + +================================================================================ +TEST: warns but continues if given an invalid version + +RUN: luarocks --lua-version 1.0 + +STDOUT: +-------------------------------------------------------------------------------- +Version : 1.0 +LUA : (interpreter not found) +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: reports if lua.h header is not found + +RUN: luarocks LUA_INCDIR=/bad/dir + +STDOUT: +-------------------------------------------------------------------------------- +LUA_INCDIR : /bad/dir (lua.h not found) +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: reports if Lua library is not found + +RUN: luarocks LUA_LIBDIR=/bad/dir + +STDOUT: +-------------------------------------------------------------------------------- +LUA_LIBDIR : /bad/dir (Lua library itself not found) +-------------------------------------------------------------------------------- diff --git a/spec/quick/config.q b/spec/quick/config.q new file mode 100644 index 0000000..d623056 --- /dev/null +++ b/spec/quick/config.q @@ -0,0 +1,97 @@ +SUITE: luarocks config + +================================================================================ +TEST: --system-config shows the path of the system config + +FILE: %{testing_lrprefix}/etc/luarocks/config-%{LUA_VERSION}.lua +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +RUN: luarocks config --system-config + +STDOUT: +-------------------------------------------------------------------------------- +%{path(%{testing_lrprefix}/etc/luarocks/config-%{LUA_VERSION}.lua)} +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: reports when setting a bad LUA_LIBDIR + +RUN: luarocks config variables.LUA_LIBDIR /some/bad/path + +LuaRocks writes configuration values as they are given, without auto-conversion +of slashes for Windows: + +STDOUT: +-------------------------------------------------------------------------------- +Wrote +variables.LUA_LIBDIR = "/some/bad/path" +-------------------------------------------------------------------------------- + +STDERR: +-------------------------------------------------------------------------------- +Warning: Failed finding the Lua library. +Tried: + +LuaRocks may not work correctly when building C modules using this configuration. +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: reports when setting a bad LUA_INCDIR + +RUN: luarocks config variables.LUA_INCDIR /some/bad/path + +STDOUT: +-------------------------------------------------------------------------------- +Wrote +variables.LUA_INCDIR = "/some/bad/path" +-------------------------------------------------------------------------------- + +LuaRocks uses configuration values as they are given, without auto-conversion +of slashes for Windows: + +STDERR: +-------------------------------------------------------------------------------- +Warning: Failed finding Lua header lua.h (searched at /some/bad/path). You may need to install Lua development headers. + +LuaRocks may not work correctly when building C modules using this configuration. +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: rejects setting bad lua_dir + +RUN: luarocks config lua_dir /some/bad/dir +EXIT: 1 + +STDERR: +-------------------------------------------------------------------------------- +Lua interpreter not found +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: reports when setting a bad LUA_INCDIR + +RUN: luarocks config variables.LUA_INCDIR /some/bad/path + +STDOUT: +-------------------------------------------------------------------------------- +Wrote +variables.LUA_INCDIR = "/some/bad/path" +-------------------------------------------------------------------------------- + +LuaRocks uses configuration values as they are given, without auto-conversion +of slashes for Windows: + +STDERR: +-------------------------------------------------------------------------------- +Warning: Failed finding Lua header lua.h (searched at /some/bad/path). You may need to install Lua development headers. + +LuaRocks may not work correctly when building C modules using this configuration. +-------------------------------------------------------------------------------- diff --git a/spec/quick/doc.q b/spec/quick/doc.q new file mode 100644 index 0000000..4c71f83 --- /dev/null +++ b/spec/quick/doc.q @@ -0,0 +1,13 @@ +SUITE: luarocks doc + +================================================================================ +TEST: --local + +RUN: luarocks install --local --only-server=%{fixtures_dir}/a_repo a_rock + +RUN: luarocks doc a_rock --local + +STDOUT: +-------------------------------------------------------------------------------- +opening http://www.example.com +-------------------------------------------------------------------------------- diff --git a/spec/quick/install.q b/spec/quick/install.q new file mode 100644 index 0000000..e2df428 --- /dev/null +++ b/spec/quick/install.q @@ -0,0 +1,688 @@ +SUITE: luarocks install + +=============================================================================== +TEST: fails with no flags or arguments +RUN: luarocks install +EXIT: 1 + + + +=============================================================================== +TEST: fails with an unknown rock +RUN: luarocks install aoeuaoeuaoeiaoeuaoeua +EXIT: 1 + + + +=============================================================================== +TEST: fails with an invalid .rock argument +RUN: luarocks install "invalid.rock" +EXIT: 1 + + + +=============================================================================== +TEST: fails with incompatible architecture +RUN: luarocks install foo-1.0-1.impossible-x86.rock +EXIT: 1 +STDERR: +-------------------------------------------------------------------------------- +Incompatible architecture +-------------------------------------------------------------------------------- + + + +=============================================================================== +TEST: fails if not a zip file + +FILE: not_a_zipfile-1.0-1.src.rock +-------------------------------------------------------------------------------- +I am not a zip file! +-------------------------------------------------------------------------------- +RUN: luarocks install not_a_zipfile-1.0-1.src.rock +EXIT: 1 + + + +=============================================================================== +TEST: fails with an invalid patch + +FILE: invalid_patch-0.1-1.rockspec +-------------------------------------------------------------------------------- +package = "invalid_patch" +version = "0.1-1" +source = { + -- any valid URL + url = "https://raw.github.com/keplerproject/luarocks/master/src/luarocks/build.lua" +} +description = { + summary = "A rockspec with an invalid patch", +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + build = "build.lua" + }, + patches = { + ["I_am_an_invalid_patch.patch"] = +[[ +diff -Naur luadoc-3.0.1/src/luadoc/doclet/html.lua luadoc-3.0.1-new/src/luadoc/doclet/html.lua +--- luadoc-3.0.1/src/luadoc/doclet/html.lua2007-12-21 15:50:48.000000000 -0200 ++++ luadoc-3.0.1-new/src/luadoc/doclet/html.lua2008-02-28 01:59:53.000000000 -0300 +@@ -18,6 +18,7 @@ +- gabba gabba gabba ++ gobo gobo gobo +]] + } +} +-------------------------------------------------------------------------------- +RUN: luarocks invalid_patch-0.1-1.rockspec +EXIT: 1 + + + +================================================================================ +TEST: handle versioned modules when installing another version with --keep #268 + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: myrock-2.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "2.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +RUN: luarocks build myrock-1.0-1.rockspec +RUN: luarocks pack myrock +RUN: luarocks remove myrock + +RUN: luarocks build myrock-2.0-1.rockspec +RUN: luarocks pack myrock +RUN: luarocks remove myrock + +RUN: luarocks install ./myrock-2.0-1.all.rock + +EXISTS: %{testing_sys_tree}/share/lua/%{LUA_VERSION}/rock.lua + +RUN: luarocks install ./myrock-1.0-1.all.rock --keep + +EXISTS: %{testing_sys_tree}/share/lua/%{LUA_VERSION}/rock.lua +EXISTS: %{testing_sys_tree}/share/lua/%{LUA_VERSION}/myrock_1_0_1-rock.lua + +RUN: luarocks install ./myrock-2.0-1.all.rock + +EXISTS: %{testing_sys_tree}/share/lua/%{LUA_VERSION}/rock.lua +NOT_EXISTS: %{testing_sys_tree}/share/lua/%{LUA_VERSION}/myrock_1_0_1-rock.lua + + + +================================================================================ +TEST: handle versioned libraries when installing another version with --keep #268 + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/c_module.c" +} +build = { + modules = { + c_module = { "c_module.c" } + } +} +-------------------------------------------------------------------------------- + +FILE: myrock-2.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "2.0-1" +source = { + url = "file://%{url(%{tmpdir})}/c_module.c" +} +build = { + modules = { + c_module = { "c_module.c" } + } +} +-------------------------------------------------------------------------------- +FILE: c_module.c +-------------------------------------------------------------------------------- +#include +#include + +int luaopen_c_module(lua_State* L) { + lua_newtable(L); + lua_pushinteger(L, 1); + lua_setfield(L, -2, "c_module"); + return 1; +} +-------------------------------------------------------------------------------- + +RUN: luarocks build myrock-1.0-1.rockspec +RUN: luarocks pack myrock +RUN: luarocks remove myrock + +RUN: luarocks build myrock-2.0-1.rockspec +RUN: luarocks pack myrock +RUN: luarocks remove myrock + +RUN: luarocks install ./myrock-2.0-1.%{platform}.rock + +EXISTS: %{testing_sys_tree}/lib/lua/%{LUA_VERSION}/c_module.%{lib_extension} + +RUN: luarocks install ./myrock-1.0-1.%{platform}.rock --keep + +EXISTS: %{testing_sys_tree}/lib/lua/%{LUA_VERSION}/c_module.%{lib_extension} +EXISTS: %{testing_sys_tree}/lib/lua/%{LUA_VERSION}/myrock_1_0_1-c_module.%{lib_extension} + +RUN: luarocks install ./myrock-2.0-1.%{platform}.rock + +EXISTS: %{testing_sys_tree}/lib/lua/%{LUA_VERSION}/c_module.%{lib_extension} +NOT_EXISTS: %{testing_sys_tree}/lib/lua/%{LUA_VERSION}/myrock_1_0_1-c_module.%{lib_extension} + + + +================================================================================ +TEST: installs a package with a bin entry + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" }, + install = { + bin = { + ["scripty"] = "rock.lua", + } + } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +RUN: luarocks build myrock-1.0-1.rockspec +EXISTS: %{testing_sys_tree}/bin/scripty%{wrapper_extension} +RUN: luarocks pack myrock +RUN: luarocks remove myrock +NOT_EXISTS: %{testing_sys_tree}/bin/scripty%{wrapper_extension} + +RUN: luarocks install myrock-1.0-1.all.rock +EXISTS: %{testing_sys_tree}/bin/scripty%{wrapper_extension} + + + +================================================================================ +TEST: installs a package without its documentation using --no-doc + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "." +} +build = { + modules = { rock = "rock.lua" }, + install = { + bin = { + ["scripty"] = "rock.lua", + } + } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +FILE: doc/something +-------------------------------------------------------------------------------- +a doc +-------------------------------------------------------------------------------- + +RUN: luarocks make +EXISTS: %{testing_sys_rocks}/myrock/1.0-1/doc/something +RUN: luarocks pack myrock +RUN: luarocks remove myrock +NOT_EXISTS: %{testing_sys_rocks}/myrock/1.0-1/doc/something + +RUN: luarocks install myrock-1.0-1.all.rock +EXISTS: %{testing_sys_rocks}/myrock/1.0-1/doc/something +RUN: luarocks remove myrock +NOT_EXISTS: %{testing_sys_rocks}/myrock/1.0-1/doc/something + +RUN: luarocks install myrock-1.0-1.all.rock --no-doc +NOT_EXISTS: %{testing_sys_rocks}/myrock/1.0-1/doc/something + + + +================================================================================ +TEST: handle non-Lua files in build.install.lua when upgrading sailorproject/sailor#138 + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "." +} +build = { + modules = { + rock = "rock.lua", + }, + install = { + lua = { + ["sailor.blank-app.htaccess"] = "src/sailor/blank-app/.htaccess", + } + } +} +-------------------------------------------------------------------------------- + +FILE: myrock-1.0-2.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-2" +source = { + url = "." +} +build = { + modules = { + rock = "rock.lua", + }, + install = { + lua = { + ["sailor.blank-app.htaccess"] = "src/sailor/blank-app/.htaccess", + } + } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +FILE: src/sailor/blank-app/.htaccess +-------------------------------------------------------------------------------- +# I am just a file +-------------------------------------------------------------------------------- + +Prepare two versions as .rock packages with the same non-Lua asset: + +RUN: luarocks make ./myrock-1.0-1.rockspec +RUN: luarocks pack myrock +RUN: luarocks remove myrock + +RUN: luarocks make ./myrock-1.0-2.rockspec +RUN: luarocks pack myrock +RUN: luarocks remove myrock + +Now install the first one, and check that the asset was installed, with no "~" +backup leftover: + +RUN: luarocks install myrock-1.0-1.all.rock --no-doc + +EXISTS: %{testing_sys_tree}/share/lua/%{LUA_VERSION}/sailor/blank-app/.htaccess +NOT_EXISTS: %{testing_sys_tree}/share/lua/%{LUA_VERSION}/sailor/blank-app/.htaccess~ + +Then install the second one, and the asset should be replaced, again with no +"~" backup leftover: + +RUN: luarocks install myrock-1.0-2.all.rock --no-doc + +EXISTS: %{testing_sys_tree}/share/lua/%{LUA_VERSION}/sailor/blank-app/.htaccess +NOT_EXISTS: %{testing_sys_tree}/share/lua/%{LUA_VERSION}/sailor/blank-app/.htaccess~ + + + +================================================================================ +TEST: do not reinstall when already installed + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +RUN: luarocks build myrock-1.0-1.rockspec +RUN: luarocks pack myrock +RUN: luarocks remove myrock + +RUN: luarocks install ./myrock-1.0-1.all.rock + +RUN: luarocks show myrock +STDOUT: +-------------------------------------------------------------------------------- +myrock 1.0 +-------------------------------------------------------------------------------- + +RUN: luarocks install ./myrock-1.0-1.all.rock +STDOUT: +-------------------------------------------------------------------------------- +myrock 1.0-1 is already installed +Use --force to reinstall +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: installation rolls back on failure + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { + ["folder.rock"] = "rock.lua", + ["xyz"] = "xyz.lua", + }, +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return {} +-------------------------------------------------------------------------------- + +FILE: xyz.lua +-------------------------------------------------------------------------------- +return {} +-------------------------------------------------------------------------------- + +RUN: luarocks make --pack-binary-rock ./myrock-1.0-1.rockspec + +FILE: %{testing_sys_tree}/share/lua/%{lua_version}/folder +-------------------------------------------------------------------------------- +a file where a folder should be +-------------------------------------------------------------------------------- + +Try to install and fail because the file is in the folder's spot: + +RUN: luarocks install ./myrock-1.0-1.all.rock +EXIT: 1 + +EXISTS: %{testing_sys_tree}/share/lua/%{lua_version}/folder + +No leftovers from the failed installation: + +NOT_EXISTS: %{testing_sys_tree}/share/lua/%{lua_version}/xyz.lua + +Now we remove the file... + +RM: %{testing_sys_tree}/share/lua/%{lua_version}/folder + +Try again and succeed: + +RUN: luarocks install ./myrock-1.0-1.all.rock + +EXISTS: %{testing_sys_tree}/share/lua/%{lua_version}/folder/rock.lua +EXISTS: %{testing_sys_tree}/share/lua/%{lua_version}/xyz.lua + + + +================================================================================ +TEST: new install functionality based on #552: break dependencies warning + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: myrock-2.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "2.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: hasdep-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "hasdep" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/hasdep.lua" +} +dependencies = { + "myrock >= 2.0", +} +build = { + modules = { hasdep = "hasdep.lua" } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +FILE: hasdep.lua +-------------------------------------------------------------------------------- +return "hasdep" +-------------------------------------------------------------------------------- + +RUN: luarocks build myrock-2.0-1.rockspec +RUN: luarocks build hasdep-1.0-1.rockspec +RUN: luarocks build myrock-1.0-1.rockspec + +STDERR: +-------------------------------------------------------------------------------- +Will not remove myrock 2.0 +Removing it would break dependencies for +hasdep 1.0 +-------------------------------------------------------------------------------- + +EXISTS: %{testing_sys_rocks}/myrock/1.0-1 +EXISTS: %{testing_sys_rocks}/myrock/2.0-1 + + + +================================================================================ +TEST: new install functionality based on #552: break dependencies with --force + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: myrock-2.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "2.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: hasdep-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "hasdep" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/hasdep.lua" +} +dependencies = { + "myrock >= 2.0", +} +build = { + modules = { hasdep = "hasdep.lua" } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +FILE: hasdep.lua +-------------------------------------------------------------------------------- +return "hasdep" +-------------------------------------------------------------------------------- + +RUN: luarocks build myrock-2.0-1.rockspec +RUN: luarocks build hasdep-1.0-1.rockspec +RUN: luarocks build myrock-1.0-1.rockspec --force + +STDERR: +-------------------------------------------------------------------------------- +The following packages may be broken by this forced removal +hasdep 1.0 +-------------------------------------------------------------------------------- + +NOT_EXISTS: %{testing_sys_rocks}/myrock/2.0-1 +EXISTS: %{testing_sys_rocks}/myrock/1.0-1 + + + +================================================================================ +TEST: new install functionality based on #552: break dependencies with --force-fast + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: myrock-2.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "2.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: hasdep-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "hasdep" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/hasdep.lua" +} +dependencies = { + "myrock >= 2.0", +} +build = { + modules = { hasdep = "hasdep.lua" } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +FILE: hasdep.lua +-------------------------------------------------------------------------------- +return "hasdep" +-------------------------------------------------------------------------------- + +RUN: luarocks build myrock-2.0-1.rockspec +RUN: luarocks build hasdep-1.0-1.rockspec +RUN: luarocks build myrock-1.0-1.rockspec --force-fast + +NOT_STDERR: +-------------------------------------------------------------------------------- +The following packages may be broken by this forced removal +hasdep 1.0 +-------------------------------------------------------------------------------- + +NOT_EXISTS: %{testing_sys_rocks}/myrock/2.0-1 +EXISTS: %{testing_sys_rocks}/myrock/1.0-1 diff --git a/spec/quick/list.q b/spec/quick/list.q new file mode 100644 index 0000000..a40f37e --- /dev/null +++ b/spec/quick/list.q @@ -0,0 +1,45 @@ +SUITE: luarocks list + +================================================================================ +TEST: invalid tree + +RUN: luarocks --tree=%{path(/some/invalid/tree)} list + +STDOUT: +-------------------------------------------------------------------------------- +Rocks installed for Lua %{lua_version} in %{path(/some/invalid/tree)} +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: --porcelain + +FILE: a_rock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "a_rock" +version = "1.0-1" +source = { + url = "file://%{url(%{fixtures_dir})}/a_rock.lua" +} +description = { + summary = "An example rockspec", +} +dependencies = { + "lua >= 5.1" +} +build = { + modules = { + build = "a_rock.lua" + }, +} +-------------------------------------------------------------------------------- +RUN: luarocks build a_rock-1.0-1.rockspec + +RUN: luarocks list --porcelain + +STDOUT: +-------------------------------------------------------------------------------- +a_rock 1.0-1 installed %{testing_sys_rocks} +-------------------------------------------------------------------------------- diff --git a/spec/quick/make.q b/spec/quick/make.q new file mode 100644 index 0000000..eb3472b --- /dev/null +++ b/spec/quick/make.q @@ -0,0 +1,88 @@ +SUITE: luarocks make + +================================================================================ +TEST: overrides luarocks.lock with --pin #pinning + +FILE: test-2.0-1.rockspec +-------------------------------------------------------------------------------- +package = "test" +version = "2.0-1" +source = { + url = "file://%{path(tmpdir)}/test.lua" +} +dependencies = { + "a_rock >= 0.8" +} +build = { + type = "builtin", + modules = { + test = "test.lua" + } +} +-------------------------------------------------------------------------------- + +FILE: test.lua +-------------------------------------------------------------------------------- +return {} +-------------------------------------------------------------------------------- + +FILE: luarocks.lock +-------------------------------------------------------------------------------- +return { + dependencies = { + ["a_rock"] = "1.0-1", + } +} +-------------------------------------------------------------------------------- + +RUN: luarocks make --only-server=%{fixtures_dir}/a_repo --pin --tree=lua_modules + +EXISTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/test/2.0-1/test-2.0-1.rockspec +EXISTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/a_rock/2.0-1/a_rock-2.0-1.rockspec + +FILE_CONTENTS: ./lua_modules/lib/luarocks/rocks-%{lua_version}/test/2.0-1/luarocks.lock +-------------------------------------------------------------------------------- +return { + dependencies = { + a_rock = "2.0-1", + lua = "%{lua_version}-1" + } +} +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: running make twice builds twice + +FILE: test-2.0-1.rockspec +-------------------------------------------------------------------------------- +package = "test" +version = "2.0-1" +source = { + url = "file://%{path(tmpdir)}/test.lua" +} +build = { + type = "builtin", + modules = { + test = "test.lua" + } +} +-------------------------------------------------------------------------------- + +FILE: test.lua +-------------------------------------------------------------------------------- +return {} +-------------------------------------------------------------------------------- + +RUN: luarocks make --only-server=%{fixtures_dir}/a_repo --pin --tree=lua_modules +STDOUT: +-------------------------------------------------------------------------------- +test 2.0-1 is now installed +-------------------------------------------------------------------------------- + +RUN: luarocks make --only-server=%{fixtures_dir}/a_repo --pin --tree=lua_modules +STDOUT: +-------------------------------------------------------------------------------- +test 2.0-1 is now installed +-------------------------------------------------------------------------------- diff --git a/spec/quick/new_version.q b/spec/quick/new_version.q new file mode 100644 index 0000000..98426db --- /dev/null +++ b/spec/quick/new_version.q @@ -0,0 +1,213 @@ +SUITE: luarocks new_version + +================================================================================ +TEST: fails without a context + +RUN: luarocks new_version +EXIT: 1 + + + +================================================================================ +TEST: fails with invalid arg + +RUN: luarocks new_version i_dont_exist +EXIT: 1 + + + +================================================================================ +TEST: updates a version + +FILE: myexample-0.1-1.rockspec +-------------------------------------------------------------------------------- +package = "myexample" +version = "0.1-1" +source = { + url = "git+https://localhost/myexample.git", + tag = "v0.1" +} +description = { + summary = "xxx", + detailed = "xxx" +} +build = { + type = "builtin", + modules = { + foo = "src/foo.lua" + } +} +-------------------------------------------------------------------------------- + +RUN: luarocks new_version myexample-0.1-1.rockspec 0.2 + +FILE_CONTENTS: myexample-0.2-1.rockspec +-------------------------------------------------------------------------------- +package = "myexample" +version = "0.2-1" +source = { + url = "git+https://localhost/myexample.git", + tag = "v0.2" +} +description = { + summary = "xxx", + detailed = "xxx" +} +build = { + type = "builtin", + modules = { + foo = "src/foo.lua" + } +} +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: updates via tag + +FILE: myexample-0.1-1.rockspec +-------------------------------------------------------------------------------- +package = "myexample" +version = "0.1-1" +source = { + url = "git+https://localhost/myexample.git", + tag = "v0.1" +} +description = { + summary = "xxx", + detailed = "xxx" +} +build = { + type = "builtin", + modules = { + foo = "src/foo.lua" + } +} +-------------------------------------------------------------------------------- + +RUN: luarocks new_version myexample-0.1-1.rockspec --tag v0.2 + +FILE_CONTENTS: myexample-0.2-1.rockspec +-------------------------------------------------------------------------------- +package = "myexample" +version = "0.2-1" +source = { + url = "git+https://localhost/myexample.git", + tag = "v0.2" +} +description = { + summary = "xxx", + detailed = "xxx" +} +build = { + type = "builtin", + modules = { + foo = "src/foo.lua" + } +} +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: updates URL + +FILE: myexample-0.1-1.rockspec +-------------------------------------------------------------------------------- +package = "myexample" +version = "0.1-1" +source = { + url = "https://localhost/myexample-0.1.tar.gz", +} +description = { + summary = "xxx", + detailed = "xxx" +} +build = { + type = "builtin", + modules = { + foo = "src/foo.lua" + } +} +-------------------------------------------------------------------------------- + +RUN: luarocks new_version myexample-0.1-1.rockspec 0.2 https://localhost/newpath/myexample-0.2.tar.gz + +FILE_CONTENTS: myexample-0.2-1.rockspec +-------------------------------------------------------------------------------- +package = "myexample" +version = "0.2-1" +source = { + url = "https://localhost/newpath/myexample-0.2.tar.gz" +} +description = { + summary = "xxx", + detailed = "xxx" +} +build = { + type = "builtin", + modules = { + foo = "src/foo.lua" + } +} +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: updates MD5 + +FILE: test-1.0-1.rockspec +-------------------------------------------------------------------------------- +package = "test" +version = "1.0-1" +source = { + url = "file://%{url(%{fixtures_dir})}/an_upstream_tarball-0.1.tar.gz", + md5 = "dca2ac30ce6c27cbd8dac4dd8f447630", +} +build = { + type = "builtin", + modules = { + my_module = "src/my_module.lua" + }, + install = { + bin = { + "src/my_module.lua" + } + } +} +-------------------------------------------------------------------------------- + +RUN: luarocks new_version test-1.0-1.rockspec 2.0 file://%{url(%{fixtures_dir})}/busted_project-0.1.tar.gz + +FILE_CONTENTS: test-2.0-1.rockspec +-------------------------------------------------------------------------------- +package = "test" +version = "2.0-1" +source = { + url = "file://%{url(%{fixtures_dir})}/busted_project-0.1.tar.gz", + md5 = "adfdfb8f1caa2b1f935a578fb07536eb", +} +build = { + type = "builtin", + modules = { + my_module = "src/my_module.lua" + }, + install = { + bin = { + "src/my_module.lua" + } + } +} +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: takes a URL, downloads and bumps revision by default + +RUN: luarocks new_version file://%{url(%{fixtures_dir})}/a_rock-1.0-1.rockspec + +EXISTS: a_rock-1.0-1.rockspec +EXISTS: a_rock-1.0-2.rockspec diff --git a/spec/quick/pack.q b/spec/quick/pack.q new file mode 100644 index 0000000..ee44a45 --- /dev/null +++ b/spec/quick/pack.q @@ -0,0 +1,128 @@ +SUITE: luarocks pack + +================================================================================ +TEST: fails no arguments + +RUN: luarocks pack +EXIT: 1 + + + +================================================================================ +TEST: fails with invalid rockspec + +RUN: luarocks pack $%{fixtures_dir}/invalid_say-1.3-1.rockspec +EXIT: 1 + + + +================================================================================ +TEST: fails with rock that is not installed + +RUN: luarocks pack notinstalled +EXIT: 1 + + + +================================================================================ +TEST: fails with non existing path + +RUN: luarocks pack /notexists/notinstalled +EXIT: 1 + + + +================================================================================ +TEST: packs latest version version of rock + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: myrock-2.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "2.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +RUN: luarocks build myrock-1.0-1.rockspec +RUN: luarocks build myrock-2.0-1.rockspec --keep +RUN: luarocks pack myrock + +EXISTS: myrock-2.0-1.all.rock + + + +================================================================================ +TEST: --sign #gpg +PENDING: true + +FILE: myrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: myrock-2.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +package = "myrock" +version = "2.0-1" +source = { + url = "file://%{url(%{tmpdir})}/rock.lua" +} +build = { + modules = { rock = "rock.lua" } +} +-------------------------------------------------------------------------------- + +FILE: rock.lua +-------------------------------------------------------------------------------- +return "hello" +-------------------------------------------------------------------------------- + +RUN: luarocks build myrock-1.0-1.rockspec +RUN: luarocks build myrock-2.0-1.rockspec --keep +RUN: luarocks pack myrock --sign + +EXISTS: myrock-2.0-1.all.rock +EXISTS: myrock-2.0-1.all.rock.asc + + + +================================================================================ +TEST: packs a namespaced rock #namespaces + +RUN: luarocks build a_user/a_rock --server=%{fixtures_dir}/a_repo +RUN: luarocks build a_rock --keep --server=%{fixtures_dir}/a_repo +RUN: luarocks pack a_user/a_rock + +EXISTS: a_rock-2.0-1.all.rock diff --git a/spec/quick/path.q b/spec/quick/path.q new file mode 100644 index 0000000..77fc188 --- /dev/null +++ b/spec/quick/path.q @@ -0,0 +1,22 @@ +================================================================================ +TEST: luarocks path: --project-tree + + +RUN: luarocks path --project-tree=foo +STDOUT: +-------------------------------------------------------------------------------- +%{path(foo/share/lua/%{lua_version}/?.lua)} +%{path(foo/share/lua/%{lua_version}/?/init.lua)} +-------------------------------------------------------------------------------- + +RUN: luarocks path --project-tree=foo --tree=bar +NOT_STDOUT: +-------------------------------------------------------------------------------- +%{path(foo/share/lua/%{lua_version}/?.lua)} +%{path(foo/share/lua/%{lua_version}/?/init.lua)} +-------------------------------------------------------------------------------- +STDOUT: +-------------------------------------------------------------------------------- +%{path(bar/share/lua/%{lua_version}/?.lua)} +%{path(bar/share/lua/%{lua_version}/?/init.lua)} +-------------------------------------------------------------------------------- diff --git a/spec/quick/purge.q b/spec/quick/purge.q new file mode 100644 index 0000000..1fb6129 --- /dev/null +++ b/spec/quick/purge.q @@ -0,0 +1,103 @@ +SUITE: luarocks purge + +================================================================================ +TEST: needs a --tree argument +RUN: luarocks purge +EXIT: 1 + +================================================================================ +TEST: missing tree +RUN: luarocks purge --tree=missing-tree +EXIT: 1 + +================================================================================ +TEST: missing --tree argument +RUN: luarocks purge --tree= +EXIT: 1 + + +================================================================================ +TEST: runs + +FILE: testrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +package = "testrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/testrock.lua" +} +dependencies = { + "a_rock >= 0.8" +} +build = { + type = "builtin", + modules = { + testrock = "testrock.lua" + } +} +-------------------------------------------------------------------------------- + +FILE: testrock.lua +-------------------------------------------------------------------------------- +return {} +-------------------------------------------------------------------------------- + +RUN: luarocks build --only-server=%{fixtures_dir}/a_repo testrock-1.0-1.rockspec + +EXISTS: %{testing_sys_rocks}/testrock +EXISTS: %{testing_sys_rocks}/a_rock + +RUN: luarocks purge --tree=%{testing_sys_tree} + +NOT_EXISTS: %{testing_sys_rocks}/testrock +NOT_EXISTS: %{testing_sys_rocks}/a_rock + + + +================================================================================ +TEST: works with missing files + +FILE: testrock-1.0-1.rockspec +-------------------------------------------------------------------------------- +package = "testrock" +version = "1.0-1" +source = { + url = "file://%{url(%{tmpdir})}/testrock.lua" +} +dependencies = { + "a_rock >= 0.8" +} +build = { + type = "builtin", + modules = { + testrock = "testrock.lua" + } +} +-------------------------------------------------------------------------------- + +FILE: testrock.lua +-------------------------------------------------------------------------------- +return {} +-------------------------------------------------------------------------------- + +RUN: luarocks build --only-server=%{fixtures_dir}/a_repo testrock-1.0-1.rockspec + +RMDIR: %{testing_sys_tree}/share/lua/%{lua_version}/testrock + +RUN: luarocks purge --tree=%{testing_sys_tree} + +NOT_EXISTS: %{testing_sys_rocks}/testrock +NOT_EXISTS: %{testing_sys_rocks}/a_rock + + + +================================================================================ +TEST: --old-versions + +RUN: luarocks install --only-server=%{fixtures_dir}/a_repo a_rock 2.0 +RUN: luarocks install --only-server=%{fixtures_dir}/a_repo a_rock 1.0 --keep + +RUN: luarocks purge --old-versions --tree=%{testing_sys_tree} + +EXISTS: %{testing_sys_rocks}/a_rock/2.0-1 +NOT_EXISTS: %{testing_sys_rocks}/a_rock/1.0-1 diff --git a/spec/quick/test.q b/spec/quick/test.q new file mode 100644 index 0000000..cb5ccd7 --- /dev/null +++ b/spec/quick/test.q @@ -0,0 +1,51 @@ +================================================================================ +TEST: luarocks test: handle if test.command is not a string + +Regression test for #1055. + +FILE: example-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +source = { + url = "", +} +package = "example" +version = "1.0-1" +test = { + type = "command", + command = {"./unit.lua"}, +} +-------------------------------------------------------------------------------- + +RUN: luarocks test +EXIT: 1 +STDERR: +-------------------------------------------------------------------------------- +'command' expects a string +-------------------------------------------------------------------------------- + + + +================================================================================ +TEST: luarocks test: handle if test.script is not a string + +FILE: example-1.0-1.rockspec +-------------------------------------------------------------------------------- +rockspec_format = "3.0" +source = { + url = "", +} +package = "example" +version = "1.0-1" +test = { + type = "command", + script = {"./unit.lua"}, +} +-------------------------------------------------------------------------------- + +RUN: luarocks test +EXIT: 1 +STDERR: +-------------------------------------------------------------------------------- +'script' expects a string +-------------------------------------------------------------------------------- diff --git a/spec/quick_spec.lua b/spec/quick_spec.lua new file mode 100644 index 0000000..c2d8bb5 --- /dev/null +++ b/spec/quick_spec.lua @@ -0,0 +1,22 @@ +local lfs = require("lfs") +local test_env = require("spec.util.test_env") +local quick = require("spec.util.quick") + +describe("quick tests: #quick", function() + before_each(function() + test_env.setup_specs() + end) + + local spec_quick = test_env.testing_paths.spec_dir .. "/quick" + for f in lfs.dir(spec_quick) do + if f:match("%.q$") then + local tests = quick.compile(spec_quick .. "/" .. f, getfenv and getfenv() or _ENV) + for _, t in ipairs(tests) do + if not t.pending then + it(t.name, t.fn) + end + end + end + end +end) + diff --git a/spec/refresh_cache_spec.lua b/spec/refresh_cache_spec.lua new file mode 100644 index 0000000..73ba9a9 --- /dev/null +++ b/spec/refresh_cache_spec.lua @@ -0,0 +1,13 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run + +describe("luarocks-admin refresh_cache #integration", function() + + before_each(function() + test_env.setup_specs() + end) + + it("runs #ssh", function() + assert.is_true(run.luarocks_admin_bool("--server=testing refresh_cache")) + end) +end) diff --git a/spec/remove_spec.lua b/spec/remove_spec.lua new file mode 100644 index 0000000..3bcfbb2 --- /dev/null +++ b/spec/remove_spec.lua @@ -0,0 +1,129 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run +local testing_paths = test_env.testing_paths +local env_variables = test_env.env_variables +local V = test_env.V +local P = test_env.P + +local extra_rocks = { + "/abelhas-1.1-1.src.rock", + "/copas-${COPAS}.src.rock", + "/coxpcall-1.16.0-1.src.rock", + "/coxpcall-1.16.0-1.rockspec", + "/luafilesystem-${LUAFILESYSTEM}.src.rock", + "/luafilesystem-${LUAFILESYSTEM_OLD}.src.rock", +} + +describe("luarocks remove #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + describe("basic tests", function() + it("with no flags/arguments", function() + assert.is_false(run.luarocks_bool("remove")) + end) + + it("invalid rock", function() + assert.is_false(run.luarocks_bool("remove invalid.rock")) + end) + + it("missing rock", function() + assert.is_false(run.luarocks_bool("remove missing_rock")) + end) + + it("invalid argument", function() + assert.is_false(run.luarocks_bool("remove luacov --deps-mode")) + end) + + it("built abelhas", function() + assert.is_true(run.luarocks_bool("build abelhas 1.1")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/abelhas")) + assert.is_true(run.luarocks_bool("remove abelhas 1.1")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/abelhas")) + end) + + it("built abelhas with uppercase name", function() + assert.is_true(run.luarocks_bool("build abelhas 1.1")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/abelhas")) + assert.is_true(run.luarocks_bool("remove Abelhas 1.1")) + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/abelhas")) + end) + end) + + describe("more complex tests", function() + before_each(function() + assert.is.truthy(test_env.need_rock("coxpcall")) + end) + + it("fail, break dependencies", function() + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/coxpcall")) + assert.is_true(run.luarocks_bool("build copas")) + + assert.is_false(run.luarocks_bool("remove coxpcall")) + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/coxpcall")) + end) + + it("force", function() + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/coxpcall")) + assert.is_true(run.luarocks_bool("build copas")) + + local output = run.luarocks("remove --force coxpcall") + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/coxpcall")) + assert.is.truthy(output:find("Checking stability of dependencies")) + end) + + it("force fast", function() + assert.is.truthy(lfs.attributes(testing_paths.testing_sys_rocks .. "/coxpcall")) + assert.is_true(run.luarocks_bool("build copas")) + + local output = run.luarocks("remove --force-fast coxpcall") + assert.is.falsy(lfs.attributes(testing_paths.testing_sys_rocks .. "/coxpcall")) + assert.is.falsy(output:find("Checking stability of dependencies")) + end) + + it("restores old versions", function() + local libdir = P(testing_paths.testing_sys_tree .. "/lib/lua/"..env_variables.LUA_VERSION) + + assert.is_true(run.luarocks_bool("install luafilesystem ${LUAFILESYSTEM_OLD_V}")) + assert.is.truthy(lfs.attributes(libdir.."/lfs."..test_env.lib_extension)) + + if test_env.TEST_TARGET_OS ~= "windows" then + local fd = io.open(libdir.."/lfs."..test_env.lib_extension, "r") + assert(fd:read("*a"):match(V"LuaFileSystem ${LUAFILESYSTEM_OLD_V}", 1, true)) + fd:close() + end + + local suffix = (V"${LUAFILESYSTEM_OLD}"):gsub("[%.%-]", "_") + + assert.is_true(run.luarocks_bool("install luafilesystem ${LUAFILESYSTEM_V} --keep")) + assert.is.truthy(lfs.attributes(libdir.."/lfs."..test_env.lib_extension)) + assert.is.truthy(lfs.attributes(libdir.."/luafilesystem_"..suffix.."-lfs."..test_env.lib_extension)) + + if test_env.TEST_TARGET_OS ~= "windows" then + local fd = io.open(libdir.."/lfs."..test_env.lib_extension, "r") + assert(fd:read("*a"):match(V"LuaFileSystem ${LUAFILESYSTEM_V}", 1, true)) + fd:close() + end + + assert.is_true(run.luarocks_bool("remove luafilesystem ${LUAFILESYSTEM_V}")) + assert.is.truthy(lfs.attributes(libdir.."/lfs."..test_env.lib_extension)) + + if test_env.TEST_TARGET_OS ~= "windows" then + local fd = io.open(libdir.."/lfs."..test_env.lib_extension, "r") + assert(fd:read("*a"):match(V"LuaFileSystem ${LUAFILESYSTEM_OLD_V}", 1, true)) + fd:close() + end + end) + end) + + it("#admin remove #ssh", function() + assert.is_true(run.luarocks_admin_bool("--server=testing remove coxpcall-1.16.0-1.src.rock")) + end) + + it("#admin remove missing", function() + assert.is_false(run.luarocks_admin_bool("--server=testing remove")) + end) +end) diff --git a/spec/search_spec.lua b/spec/search_spec.lua new file mode 100644 index 0000000..6d71e00 --- /dev/null +++ b/spec/search_spec.lua @@ -0,0 +1,33 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run + +local extra_rocks = { +"/lzlib-0.4.1.53-1.src.rock" +} + +describe("luarocks search #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + it("with no flags/arguments", function() + assert.is_false(run.luarocks_bool("search")) + end) + + it("zlib", function() + assert.is_true(run.luarocks_bool("search zlib")) + end) + + it("zlib 1.1", function() + assert.is_true(run.luarocks_bool("search zlib 1.1")) + end) + + it("missing rock", function() + assert.is_true(run.luarocks_bool("search missing_rock")) + end) + + it("with flag all", function() + assert.is_true(run.luarocks_bool("search --all")) + end) +end) diff --git a/spec/show_spec.lua b/spec/show_spec.lua new file mode 100644 index 0000000..cd34b5a --- /dev/null +++ b/spec/show_spec.lua @@ -0,0 +1,105 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run +local testing_paths = test_env.testing_paths + +describe("luarocks show #integration", function() + + before_each(function() + test_env.setup_specs() + end) + + it("with no flags/arguments", function() + assert.is_false(run.luarocks_bool("show")) + end) + + describe("basic tests with flags", function() + it("invalid", function() + assert.is_false(run.luarocks_bool("show invalid")) + end) + + it("luacov", function() + local output = run.luarocks("show luacov") + assert.is.truthy(output:match("LuaCov")) + end) + + it("luacov with uppercase name", function() + local output = run.luarocks("show LuaCov") + assert.is.truthy(output:match("LuaCov")) + end) + + it("modules of luacov", function() + local output = run.luarocks("show --modules luacov") + assert.match("luacov.*luacov.defaults.*luacov.reporter.*luacov.reporter.default.*luacov.runner.*luacov.stats.*luacov.tick", output) + end) + + it("--deps", function() + assert(run.luarocks_bool("build has_namespaced_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + local output = run.luarocks("show --deps has_namespaced_dep") + assert.match("a_user/a_rock", output) + end) + + it("list dependencies", function() + assert(run.luarocks_bool("build has_namespaced_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + local output = run.luarocks("show has_namespaced_dep") + assert.match("a_user/a_rock.*2.0", output) + end) + + it("rockspec of luacov", function() + local output = run.luarocks("show --rockspec luacov") + assert.is.truthy(output:match("luacov--0.15.0--1.rockspec")) + end) + + it("mversion of luacov", function() + local output = run.luarocks("show --mversion luacov") + assert.is.truthy(output:match("0.15.0--1")) + end) + + it("rock tree of luacov", function() + local output = run.luarocks("show --rock-tree luacov") + end) + + it("rock directory of luacov", function() + local output = run.luarocks("show --rock-dir luacov") + end) + + it("issues URL of luacov", function() + local output = run.luarocks("show --issues luacov") + end) + + it("labels of luacov", function() + local output = run.luarocks("show --labels luacov") + end) + end) + + it("old version of luacov", function() + run.luarocks("install luacov 0.15.0") + run.luarocks_bool("show luacov 0.15.0") + end) + + it("can find by substring", function() + assert(run.luarocks_bool("install has_build_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert.match("a_build_dep", run.luarocks("show has_")) + end) + + it("fails when substring matches multiple", function() + assert(run.luarocks_bool("install has_build_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert(run.luarocks_bool("install a_build_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert.match("multiple installed packages match the name 'dep'", run.luarocks("show dep")) + end) + + it("shows #build_dependencies", function() + assert(run.luarocks_bool("install has_build_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert.match("a_build_dep", run.luarocks("show has_build_dep")) + end) + + it("gets #build_dependencies via --build-deps", function() + assert(run.luarocks_bool("install has_build_dep --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert.match("a_build_dep", run.luarocks("show has_build_dep --build-deps")) + end) + + it("shows #namespaces via --rock-namespace", function() + assert(run.luarocks_bool("build a_user/a_rock --server=" .. testing_paths.fixtures_dir .. "/a_repo" )) + assert.match("a_user", run.luarocks("show a_rock --rock-namespace")) + end) + +end) diff --git a/spec/test_spec.lua b/spec/test_spec.lua new file mode 100644 index 0000000..bc5f047 --- /dev/null +++ b/spec/test_spec.lua @@ -0,0 +1,93 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run +local testing_paths = test_env.testing_paths +local write_file = test_env.write_file + +local extra_rocks = { + "/busted-2.2.0-1.src.rock", + "/lua_cliargs-3.0-1.src.rock", + "/luafilesystem-${LUAFILESYSTEM}.src.rock", + "/luasystem-0.2.1-0.src.rock", + "/dkjson-${DKJSON}.src.rock", + "/say-1.4.1-3.src.rock", + "/luassert-1.9.0-1.src.rock", + "/penlight-1.13.1-1.src.rock", + "/lua-term-0.8-1.rockspec", + "/mediator_lua-1.1.2-0.rockspec", +} + +describe("luarocks test #integration", function() + + lazy_setup(function() + test_env.setup_specs(extra_rocks) + end) + + it("fails with no flags/arguments", function() + finally(function() + lfs.chdir(testing_paths.testrun_dir) + test_env.remove_dir("empty") + end) + assert(lfs.mkdir("empty")) + assert(lfs.chdir("empty")) + assert.is_false(run.luarocks_bool("test")) + end) + + describe("busted backend", function() + + it("with rockspec, installing busted", function() + finally(function() + -- delete downloaded and unpacked files + lfs.chdir(testing_paths.testrun_dir) + test_env.remove_dir("busted_project-0.1-1") + os.remove("busted_project-0.1-1.src.rock") + end) + + -- make luassert + assert.is_true(run.luarocks_bool("download --server="..testing_paths.fixtures_repo_dir.." busted_project 0.1-1")) + assert.is_true(run.luarocks_bool("unpack busted_project-0.1-1.src.rock")) + lfs.chdir("busted_project-0.1-1/busted_project") + assert.is_true(run.luarocks_bool("make")) + local output = run.luarocks("test") + print(output) + -- Assert that busted ran, whether successfully or not + assert.match("%d+ success.* / %d+ failure.* / %d+ error.* / %d+ pending", output) + end) + + it("prepare", function() + finally(function() + -- delete downloaded and unpacked files + lfs.chdir(testing_paths.testrun_dir) + test_env.remove_dir("busted_project-0.1-1") + os.remove("busted_project-0.1-1.src.rock") + end) + + -- make luassert + assert.is_true(run.luarocks_bool("download --server="..testing_paths.fixtures_repo_dir.." busted_project 0.1-1")) + assert.is_true(run.luarocks_bool("unpack busted_project-0.1-1.src.rock")) + lfs.chdir("busted_project-0.1-1/busted_project") + assert.is_true(run.luarocks_bool("make")) + + run.luarocks_bool("remove busted") + local prepareOutput = run.luarocks_bool("test --prepare") + assert.is_true(run.luarocks_bool("show busted")) + + -- Assert that "test --prepare" run successfully + assert.is_true(prepareOutput) + + local output = run.luarocks("test") + assert.not_match(tostring(prepareOutput), output) + + end) + end) + + describe("command backend", function() + describe("prepare", function() + it("works with non-busted rocks", function() + write_file("test.lua", "", finally) + assert.is_true(run.luarocks_bool("test --prepare " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec")) + end) + end) + end) +end) + diff --git a/spec/unit/build_spec.lua b/spec/unit/build_spec.lua new file mode 100644 index 0000000..e8f1394 --- /dev/null +++ b/spec/unit/build_spec.lua @@ -0,0 +1,365 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local get_tmp_path = test_env.get_tmp_path +local run = test_env.run +local testing_paths = test_env.testing_paths +local write_file = test_env.write_file +local P = test_env.P + +test_env.setup_specs() +local cfg = require("luarocks.core.cfg") +local deps = require("luarocks.deps") +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local rockspecs = require("luarocks.rockspecs") +local build_builtin = require("luarocks.build.builtin") + +local c_module_source = [[ + #include + #include + + int luaopen_c_module(lua_State* L) { + lua_newtable(L); + lua_pushinteger(L, 1); + lua_setfield(L, -2, "c_module"); + return 1; + } +]] + +describe("LuaRocks build #unit", function() + local runner + + lazy_setup(function() + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + cfg.init() + fs.init() + deps.check_lua_incdir(cfg.variables) + deps.check_lua_libdir(cfg.variables) + end) + + lazy_teardown(function() + runner.save_stats() + end) + + describe("build.builtin", function() + it("builtin auto installs files in lua subdir", function() + test_env.run_in_tmp(function(tmpdir) + lfs.mkdir("lua") + write_file("lua_module-1.0-1.rockspec", [[ + package = "lua_module" + version = "1.0-1" + source = { + url = "http://example.com/lua_module" + } + build = { + type = "builtin", + modules = {} + } + ]], finally) + write_file("lua/lua_module.lua", "return 123", finally) + + assert.is_true(run.luarocks_bool("build")) + assert.match("[\\/]lua_module%.lua", run.luarocks("show lua_module")) + end, finally) + end) + + describe("builtin.autodetect_modules", function() + local tmpdir + local olddir + + before_each(function() + tmpdir = get_tmp_path() + olddir = lfs.currentdir() + lfs.mkdir(tmpdir) + lfs.chdir(tmpdir) + fs.change_dir(tmpdir) + end) + + after_each(function() + if olddir then + lfs.chdir(olddir) + fs.change_dir(olddir) + if tmpdir then + lfs.rmdir(tmpdir) + end + end + end) + + local libs = { "foo1", "foo2" } + local incdirs = { "$(FOO1_INCDIR)", "$(FOO2_INCDIR)" } + local libdirs = { "$(FOO1_LIBDIR)", "$(FOO2_LIBDIR)" } + + it("returns a table of the modules having as location the current directory", function() + write_file("module1.lua", "", finally) + write_file("module2.c", "", finally) + write_file("module3.c", "int luaopen_my_module()", finally) + write_file("test.lua", "", finally) + write_file("tests.lua", "", finally) + + local modules = build_builtin.autodetect_modules(libs, incdirs, libdirs) + assert.same(modules, { + module1 = "module1.lua", + module2 = { + sources = "module2.c", + libraries = libs, + incdirs = incdirs, + libdirs = libdirs + }, + my_module = { + sources = "module3.c", + libraries = libs, + incdirs = incdirs, + libdirs = libdirs + } + }) + end) + + local test_with_location = function(location) + lfs.mkdir(location) + lfs.mkdir(location .. "/dir1") + lfs.mkdir(location .. "/dir1/dir2") + + write_file(location .. "/module1.lua", "", finally) + write_file(location .. "/dir1/module2.c", "", finally) + write_file(location .. "/dir1/dir2/module3.c", "int luaopen_my_module()", finally) + write_file(location .. "/test.lua", "", finally) + write_file(location .. "/tests.lua", "", finally) + + local modules = build_builtin.autodetect_modules(libs, incdirs, libdirs) + assert.same(modules, { + module1 = P(location .. "/module1.lua"), + ["dir1.module2"] = { + sources = P(location .. "/dir1/module2.c"), + libraries = libs, + incdirs = incdirs, + libdirs = libdirs + }, + my_module = { + sources = P(location .. "/dir1/dir2/module3.c"), + libraries = libs, + incdirs = incdirs, + libdirs = libdirs + } + }) + + lfs.rmdir(location .. "/dir1/dir2") + lfs.rmdir(location .. "/dir1") + lfs.rmdir(location) + end + + it("returns a table of the modules having as location the src directory", function() + test_with_location("src") + end) + + it("returns a table of the modules having as location the lua directory", function() + test_with_location("lua") + end) + + it("returns as second and third argument tables of the bin files and copy directories", function() + lfs.mkdir("doc") + lfs.mkdir("docs") + lfs.mkdir("samples") + lfs.mkdir("tests") + lfs.mkdir("bin") + write_file("bin/binfile", "", finally) + + local _, install, copy_directories = build_builtin.autodetect_modules({}, {}, {}) + assert.same(install, { bin = { P"bin/binfile" } }) + assert.same(copy_directories, { "doc", "docs", "samples", "tests" }) + + lfs.rmdir("doc") + lfs.rmdir("docs") + lfs.rmdir("samples") + lfs.rmdir("tests") + lfs.rmdir("bin") + end) + end) + + describe("builtin.run", function() + local tmpdir + local olddir + + before_each(function() + tmpdir = get_tmp_path() + olddir = lfs.currentdir() + lfs.mkdir(tmpdir) + lfs.chdir(tmpdir) + fs.change_dir(tmpdir) + path.use_tree(lfs.currentdir()) + end) + + after_each(function() + if olddir then + lfs.chdir(olddir) + fs.change_dir(olddir) + if tmpdir then + lfs.rmdir(tmpdir) + end + end + end) + + it("returns false if the rockspec has no build modules and its format does not support autoextraction", function() + local rockspec = { + package = "test", + version = "1.0-1", + source = { + url = "http://example.com/test" + }, + build = {} + } + + rockspecs.from_persisted_table("test-1.0-1.rockspec", rockspec) + assert.falsy(build_builtin.run(rockspec)) + rockspec.rockspec_format = "1.0" + assert.falsy(build_builtin.run(rockspec)) + end) + + it("returns false if lua.h could not be found", function() + local rockspec = { + package = "c_module", + version = "1.0-1", + source = { + url = "http://example.com/c_module" + }, + build = { + type = "builtin", + modules = { + c_module = "c_module.c" + } + } + } + write_file("c_module.c", c_module_source, finally) + + rockspecs.from_persisted_table("c_module-1.0-1.rockspec", rockspec) + rockspec.variables = { LUA_INCDIR = "invalid" } + assert.falsy(build_builtin.run(rockspec)) + end) + + it("returns false if the build fails", function() + local rockspec = { + package = "c_module", + version = "1.0-1", + source = { + url = "http://example.com/c_module" + }, + build = { + type = "builtin", + modules = { + c_module = "c_module.c" + } + } + } + write_file("c_module.c", c_module_source .. "invalid", finally) + + rockspecs.from_persisted_table("c_module-1.0-1.rockspec", rockspec) + assert.falsy(build_builtin.run(rockspec)) + end) + + it("returns true if the build succeeds with C module", function() + local rockspec = { + package = "c_module", + version = "1.0-1", + source = { + url = "http://example.com/c_module" + }, + build = { + type = "builtin", + modules = { + c_module = "c_module.c" + } + } + } + write_file("c_module.c", c_module_source, finally) + + rockspecs.from_persisted_table("c_module-1.0-1.rockspec", rockspec) + assert.truthy(build_builtin.run(rockspec)) + assert.truthy(lfs.attributes("lib/luarocks/rocks-" .. test_env.lua_version .. "/c_module/1.0-1/lib/c_module." .. test_env.lib_extension)) + end) + + it("returns true if the build succeeds with Lua module", function() + local rockspec = { + rockspec_format = "1.0", + package = "test", + version = "1.0-1", + source = { + url = "http://example.com/test" + }, + build = { + type = "builtin", + modules = { + test = "test.lua" + } + } + } + write_file("test.lua", "return {}", finally) + + rockspecs.from_persisted_table("test-1.0-1.rockspec", rockspec) + assert.truthy(build_builtin.run(rockspec)) + assert.truthy(lfs.attributes("lib/luarocks/rocks-" .. test_env.lua_version .. "/test/1.0-1/lua/test.lua")) + end) + + it("automatically extracts the modules and libraries if they are not given and builds against any external dependencies", function() + local fdir = testing_paths.fixtures_dir + if test_env.TEST_TARGET_OS == "windows" then + if test_env.MINGW then + os.execute("gcc -shared -o " .. fdir .. "/libfixturedep.dll -Wl,--out-implib," .. fdir .."/libfixturedep.a " .. fdir .. "/fixturedep.c") + else + os.execute("cl " .. fdir .. "\\fixturedep.c /link /export:fixturedep_fn /out:" .. fdir .. "\\fixturedep.dll /implib:" .. fdir .. "\\fixturedep.lib") + end + elseif test_env.TEST_TARGET_OS == "linux" then + os.execute("gcc -shared -o " .. fdir .. "/libfixturedep.so " .. fdir .. "/fixturedep.c") + elseif test_env.TEST_TARGET_OS == "osx" then + os.execute("cc -dynamiclib -o " .. fdir .. "/libfixturedep.dylib " .. fdir .. "/fixturedep.c") + end + + local rockspec = { + rockspec_format = "3.0", + package = "c_module", + version = "1.0-1", + source = { + url = "http://example.com/c_module" + }, + external_dependencies = { + FIXTUREDEP = { + library = "fixturedep" + } + }, + build = { + type = "builtin" + } + } + write_file("c_module.c", c_module_source, finally) + + rockspecs.from_persisted_table("c_module-1.0-1.rockspec", rockspec) + rockspec.variables["FIXTUREDEP_LIBDIR"] = testing_paths.fixtures_dir + assert.truthy(build_builtin.run(rockspec)) + end) + + it("returns false if any external dependency is missing", function() + local rockspec = { + rockspec_format = "3.0", + package = "c_module", + version = "1.0-1", + source = { + url = "https://example.com/c_module" + }, + external_dependencies = { + EXTDEP = { + library = "missing" + } + }, + build = { + type = "builtin" + } + } + write_file("c_module.c", c_module_source, finally) + + rockspecs.from_persisted_table("c_module-1.0-1.rockspec", rockspec) + rockspec.variables["EXTDEP_INCDIR"] = lfs.currentdir() + rockspec.variables["EXTDEP_LIBDIR"] = lfs.currentdir() + assert.falsy(build_builtin.run(rockspec)) + end) + end) + end) +end) diff --git a/spec/unit/deps_spec.lua b/spec/unit/deps_spec.lua new file mode 100644 index 0000000..3be80b9 --- /dev/null +++ b/spec/unit/deps_spec.lua @@ -0,0 +1,143 @@ +local test_env = require("spec.util.test_env") +local testing_paths = test_env.testing_paths + +local cfg = require("luarocks.core.cfg") +local deps = require("luarocks.deps") +local fs = require("luarocks.fs") + +describe("LuaRocks deps #unit", function() + local runner + + lazy_setup(function() + cfg.init() + fs.init() + deps.check_lua_incdir(cfg.variables) + deps.check_lua_libdir(cfg.variables) + + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + end) + + lazy_teardown(function() + runner.save_stats() + end) + + describe("deps", function() + describe("deps.autodetect_external_dependencies", function() + it("returns false if the given build table has no external dependencies", function() + local build_table = { + type = "builtin" + } + + assert.falsy(deps.autodetect_external_dependencies(build_table)) + end) + + it("returns a table of the external dependencies found in the given build table", function() + local build_table = { + type = "builtin", + modules = { + module1 = { + libraries = { "foo1", "foo2" }, + }, + module2 = { + libraries = "foo3" + }, + } + } + + local extdeps = deps.autodetect_external_dependencies(build_table) + assert.same(extdeps["FOO1"], { library = "foo1" }) + assert.same(extdeps["FOO2"], { library = "foo2" }) + assert.same(extdeps["FOO3"], { library = "foo3" }) + end) + + it("adds proper include and library dirs to the given build table", function() + local build_table + + build_table = { + type = "builtin", + modules = { + module1 = { + libraries = "foo" + } + } + } + deps.autodetect_external_dependencies(build_table) + assert.same(build_table, { + type = "builtin", + modules = { + module1 = { + libraries = "foo", + incdirs = { "$(FOO_INCDIR)" }, + libdirs = { "$(FOO_LIBDIR)" } + } + } + }) + + build_table = { + type = "builtin", + modules = { + module1 = { + libraries = "foo", + incdirs = { "INCDIRS" } + } + } + } + deps.autodetect_external_dependencies(build_table) + assert.same(build_table, { + type = "builtin", + modules = { + module1 = { + libraries = "foo", + incdirs = { "INCDIRS" }, + libdirs = { "$(FOO_LIBDIR)" } + } + } + }) + + build_table = { + type = "builtin", + modules = { + module1 = { + libraries = "foo", + libdirs = { "LIBDIRS" } + } + } + } + deps.autodetect_external_dependencies(build_table) + assert.same(build_table, { + type = "builtin", + modules = { + module1 = { + libraries = "foo", + incdirs = { "$(FOO_INCDIR)" }, + libdirs = { "LIBDIRS" } + } + } + }) + + build_table = { + type = "builtin", + modules = { + module1 = { + libraries = "foo", + incdirs = { "INCDIRS" }, + libdirs = { "LIBDIRS" } + } + } + } + deps.autodetect_external_dependencies(build_table) + assert.same(build_table, { + type = "builtin", + modules = { + module1 = { + libraries = "foo", + incdirs = { "INCDIRS" }, + libdirs = { "LIBDIRS" } + } + } + }) + end) + end) + end) +end) diff --git a/spec/unit/dir_spec.lua b/spec/unit/dir_spec.lua new file mode 100644 index 0000000..55dd6e0 --- /dev/null +++ b/spec/unit/dir_spec.lua @@ -0,0 +1,70 @@ +local test_env = require("spec.util.test_env") +local testing_paths = test_env.testing_paths +local P = test_env.P + +test_env.setup_specs() +local dir = require("luarocks.dir") + +describe("luarocks.dir #unit", function() + local runner + + lazy_setup(function() + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + end) + + lazy_teardown(function() + runner.save_stats() + end) + + describe("dir.is_basic_protocol", function() + it("checks whether the arguments represent a valid protocol and returns the result of the check", function() + assert.truthy(dir.is_basic_protocol("http")) + assert.truthy(dir.is_basic_protocol("https")) + assert.truthy(dir.is_basic_protocol("ftp")) + assert.truthy(dir.is_basic_protocol("file")) + assert.falsy(dir.is_basic_protocol("git")) + assert.falsy(dir.is_basic_protocol("git+https")) + assert.falsy(dir.is_basic_protocol("invalid")) + end) + end) + + describe("dir.deduce_base_dir", function() + it("deduces the base dir from archives", function() + assert.are.same("v0.3", dir.deduce_base_dir("https://example.com/hishamhm/lua-compat-5.2/archive/v0.3.zip")) + assert.are.same("lua-compat-5.2", dir.deduce_base_dir("https://example.com/hishamhm/lua-compat-5.2.zip")) + assert.are.same("lua-compat-5.2", dir.deduce_base_dir("https://example.com/hishamhm/lua-compat-5.2.tar.gz")) + assert.are.same("lua-compat-5.2", dir.deduce_base_dir("https://example.com/hishamhm/lua-compat-5.2.tar.bz2")) + end) + it("returns the basename when not given an archive", function() + assert.are.same("parser.moon", dir.deduce_base_dir("git://example.com/Cirru/parser.moon")) + assert.are.same("v0.3", dir.deduce_base_dir("https://example.com/hishamhm/lua-compat-5.2/archive/v0.3")) + end) + end) + + describe("dir.normalize", function() + it("converts backslashes and removes trailing slashes", function() + assert.are.same(P"/foo/ovo", dir.normalize("\\foo\\ovo\\")) + assert.are.same(P"c:/some/dir", dir.normalize("c:\\..\\some\\foo\\..\\dir")) + assert.are.same("http://example.com/foo/ovo", dir.normalize("http://example.com/foo\\ovo\\")) + end) + it("strips unneeded /../ and /./", function() + assert.are.same(P"/some/dir/file.txt", dir.normalize("/../../../some/./foo/bar/.././../dir/bla/../file.txt")) + assert.are.same(P"/some/dir/file.txt", dir.normalize("/../../../some/./foo/bar/.././../dir/bla/../file.txt")) + assert.are.same(P"/some/dir", dir.normalize("/../../../some/./foo/bar/.././../dir/./some/subdir/../..")) + assert.are.same(P"/some/dir", dir.normalize("/../../../some/./foo/bar/.././../dir/./.")) + end) + it("respects relative paths", function() + assert.are.same(P".", dir.normalize(".")) + assert.are.same(P"boo", dir.normalize("./boo")) + assert.are.same(P"/boo", dir.normalize("/./boo")) + assert.are.same(P"../../../../boo", dir.normalize("../../../hello/world/../../../boo")) + end) + it("respects root directory", function() + assert.are.same(P"/", dir.normalize("/")) + assert.are.same(P"/", dir.normalize("/////")) + assert.are.same(P"/", dir.normalize("/a/b/.././../c/./../../")) + end) + end) + +end) diff --git a/spec/unit/fetch_spec.lua b/spec/unit/fetch_spec.lua new file mode 100644 index 0000000..bea50d7 --- /dev/null +++ b/spec/unit/fetch_spec.lua @@ -0,0 +1,486 @@ +local test_env = require("spec.util.test_env") + +test_env.setup_specs() +local cfg = require("luarocks.core.cfg") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local rockspecs = require("luarocks.rockspecs") +local lfs = require("lfs") +local get_tmp_path = test_env.get_tmp_path +local testing_paths = test_env.testing_paths +local write_file = test_env.write_file +local P = test_env.P + +describe("luarocks fetch #unit", function() + local are_same_files = function(file1, file2) + return file1 == file2 or lfs.attributes(file1).ino == lfs.attributes(file2).ino + end + + local runner + + lazy_setup(function() + cfg.init() + fs.init() + + -- mock network access + fs.download = function(url, destfile) + local mockfile = P(url:gsub("http://localhost:8080/file", testing_paths.fixtures_dir)) + if not destfile then + destfile = dir.base_name(mockfile) + end + destfile = fs.absolute_name(destfile) + + local fdr = io.open(mockfile, "rb") + if not fdr then + return nil, "mock failed opening for reading" + end + + local fdw = io.open(destfile, "wb") + if not fdr then + return nil, "mock failed opening for writing" + end + + local data = fdr:read("*a") + if not data then + return nil, "mock failed reading" + end + + local ok = fdw:write(data) + if not ok then + return nil, "mock failed writing" + end + + fdr:close() + fdw:close() + + return true, destfile + end + + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + end) + + lazy_teardown(function() + runner.save_stats() + end) + + + describe("fetch.fetch_url", function() + + it("fetches the url argument and returns the absolute path of the fetched file", function() + test_env.run_in_tmp(function() + local fetchedfile, err = fetch.fetch_url("http://localhost:8080/file/a_rock.lua") + assert(fetchedfile, err) + assert.truthy(are_same_files(fetchedfile, lfs.currentdir() .. "/a_rock.lua")) + local fd = assert(io.open(fetchedfile, "r")) + local fetchedcontent = assert(fd:read("*a")) + fd:close() + fd = assert(io.open(testing_paths.fixtures_dir .. "/a_rock.lua", "r")) + local filecontent = assert(fd:read("*a")) + fd:close() + assert.same(fetchedcontent, filecontent) + end, finally) + end) + + it("returns the absolute path of the filename argument if the url represents a file", function() + test_env.run_in_tmp(function() + write_file("test.lua", "return {}") + + local file, err = fetch.fetch_url("file://test.lua") + assert.truthy(file, err) + assert.truthy(are_same_files(file, lfs.currentdir() .. "/test.lua")) + fs.pop_dir() + end, finally) + end) + + it("fails if local path is invalid and returns a helpful hint for relative paths", function() + test_env.run_in_tmp(function() + local ok, err = fetch.fetch_url("file://boo.lua") + assert.falsy(ok) + assert.match("note that given path in rockspec is not absolute: file://boo.lua", err) + end, finally) + end) + + it("returns false and does nothing if the url argument contains a nonexistent file", function() + assert.falsy(fetch.fetch_url("http://localhost:8080/file/nonexistent")) + end) + + it("returns false and does nothing if the url argument is invalid", function() + assert.falsy(fetch.fetch_url("invalid://url", "file")) + end) + end) + + describe("fetch.fetch_url_at_temp_dir", function() + + it("returns the absolute path and the parent directory of the file specified by the url", function() + test_env.run_in_tmp(function(tmpdir) + local tmpfile = tmpdir .. "/tmpfile" + assert(io.open(tmpfile, "w")) + local pathname, dirname = fetch.fetch_url_at_temp_dir("file://" .. tmpfile, "test") + assert.truthy(are_same_files(tmpfile, pathname)) + assert.truthy(are_same_files(tmpdir, dirname)) + end, finally) + end) + + it("returns true and fetches the url into a temporary dir", function() + test_env.run_in_tmp(function() + local fetchedfile, tmpdir = fetch.fetch_url_at_temp_dir("http://localhost:8080/file/a_rock.lua", "test") + assert(fetchedfile, tmpdir) + assert.truthy(are_same_files(fetchedfile, tmpdir .. "/a_rock.lua")) + local fd = assert(io.open(fetchedfile, "r")) + local fetchedcontent = assert(fd:read("*a")) + fd:close() + fd = assert(io.open(testing_paths.fixtures_dir .. "/a_rock.lua", "r")) + local filecontent = assert(fd:read("*a")) + fd:close() + assert.same(fetchedcontent, filecontent) + end, finally) + end) + + it("returns true and fetches the url into a temporary dir with custom filename", function() + test_env.run_in_tmp(function() + local fetchedfile, tmpdir = fetch.fetch_url_at_temp_dir("http://localhost:8080/file/a_rock.lua", "test", "my_a_rock.lua") + assert(fetchedfile, tmpdir) + assert.truthy(are_same_files(fetchedfile, tmpdir .. "/my_a_rock.lua")) + assert.truthy(string.find(tmpdir, "test")) + local fd = assert(io.open(fetchedfile, "r")) + local fetchedcontent = assert(fd:read("*a")) + fd:close() + fd = assert(io.open(testing_paths.fixtures_dir .. "/a_rock.lua", "r")) + local filecontent = assert(fd:read("*a")) + fd:close() + assert.same(fetchedcontent, filecontent) + end, finally) + end) + + it("returns false and does nothing if the file specified in the url is nonexistent", function() + assert.falsy(fetch.fetch_url_at_temp_dir("file://nonexistent", "test")) + assert.falsy(fetch.fetch_url_at_temp_dir("http://localhost:8080/file/nonexistent", "test")) + end) + + it("returns false and does nothing if the url is invalid", function() + assert.falsy(fetch.fetch_url_at_temp_dir("url://invalid", "test")) + end) + end) + + describe("fetch.find_base_dir", function() + it("extracts the archive given by the file argument and returns the inferred and the actual root directory in the archive", function() + test_env.run_in_tmp(function() + local url = "http://localhost:8080/file/an_upstream_tarball-0.1.tar.gz" + local file, tmpdir = assert(fetch.fetch_url_at_temp_dir(url, "test")) + local inferreddir, founddir = fetch.find_base_dir(file, tmpdir, url) + assert.truthy(are_same_files(inferreddir, founddir)) + assert.truthy(lfs.attributes(tmpdir .. "/" .. founddir)) + end, finally) + end) + + it("extracts the archive given by the file argument with given base directory and returns the inferred and the actual root directory in the archive", function() + test_env.run_in_tmp(function() + local url = "http://localhost:8080/file/an_upstream_tarball-0.1.tar.gz" + local file, tmpdir = assert(fetch.fetch_url_at_temp_dir(url, "test")) + local inferreddir, founddir = fetch.find_base_dir(file, tmpdir, url, "basedir") + assert.truthy(are_same_files(inferreddir, "basedir")) + assert.truthy(are_same_files(founddir, "an_upstream_tarball-0.1")) + assert.truthy(lfs.attributes(tmpdir .. "/" .. founddir)) + end, finally) + end) + + it("returns false and does nothing if the temporary directory doesn't exist", function() + assert.falsy(fetch.find_base_dir("file", "nonexistent", "url")) + end) + end) + + describe("fetch.fetch_and_unpack_rock", function() + + it("unpacks the rock file from the url and returns its resulting temporary parent directory", function() + test_env.run_in_tmp(function() + local tmpdir = fetch.fetch_and_unpack_rock("http://localhost:8080/file/a_rock-1.0-1.src.rock") + assert.truthy(string.find(tmpdir, "a_rock%-1%.0%-1")) + assert.truthy(lfs.attributes(tmpdir .. "/a_rock-1.0-1.rockspec")) + assert.truthy(lfs.attributes(tmpdir .. "/a_rock.lua")) + end, finally) + end) + + it("unpacks the rock file from the url with custom unpacking directory", function() + test_env.run_in_tmp(function() + local tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + local resultingdir = fetch.fetch_and_unpack_rock("http://localhost:8080/file/a_rock-1.0-1.src.rock", tmpdir) + assert.truthy(are_same_files(resultingdir, tmpdir)) + assert.truthy(lfs.attributes(resultingdir .. "/a_rock-1.0-1.rockspec")) + assert.truthy(lfs.attributes(resultingdir .. "/a_rock.lua")) + end, finally) + end) + + it("does nothing if the url doesn't represent a rock file", function() + assert.falsy(pcall(fetch.fetch_and_unpack_rock, "http://localhost:8080/file/a_rock.lua")) + end) + + it("does nothing if the rock file url is invalid", function() + assert.falsy(pcall(fetch.fetch_and_unpack_rock, "url://invalid")) + end) + + it("does nothing if the rock file url represents a nonexistent file", function() + assert.falsy(pcall(fetch.fetch_and_unpack_rock, "url://invalid")) + assert.falsy(pcall(fetch.fetch_and_unpack_rock, "http://localhost:8080/file/nonexistent")) + end) + end) + + describe("fetch.load_local_rockspec", function() + it("returns a table representing the rockspec from the given file skipping some checks if the quick argument is enabled", function() + test_env.run_in_tmp(function() + local rockspec = fetch.load_local_rockspec(testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec", true) + assert.same(rockspec.name, "a_rock") + assert.same(rockspec.version, "1.0-1") + assert.same(rockspec.source.url, "http://localhost:8080/file/a_rock.lua") + assert.same(rockspec.description, { summary = "An example rockspec" }) + + write_file("missing_mandatory_field-1.0-1.rockspec", [[ + package="missing_mandatory_field" + version="1.0-1" + source = { + url = "http://example.com/foo.tar.gz" + } + ]]) + rockspec = fetch.load_local_rockspec("missing_mandatory_field-1.0-1.rockspec", true) + assert.same(rockspec.name, "missing_mandatory_field") + assert.same(rockspec.version, "1.0-1") + assert.same(rockspec.source.url, "http://example.com/foo.tar.gz") + + write_file("unknown_field-1.0-1.rockspec", [[ + package="unknown_field" + version="1.0-1" + source = { + url = "http://example.com/foo.tar.gz" + } + unknown="foo" + ]]) + rockspec = fetch.load_local_rockspec("unknown_field-1.0-1.rockspec", true) + assert.same(rockspec.name, "unknown_field") + assert.same(rockspec.version, "1.0-1") + assert.same(rockspec.source.url, "http://example.com/foo.tar.gz") + + -- The previous calls fail if the detailed checking is done + path.use_tree(testing_paths.testing_tree) + assert.falsy(fetch.load_local_rockspec("missing_mandatory_field-1.0-1.rockspec")) + assert.falsy(fetch.load_local_rockspec("unknown_field-1.0-1.rockspec")) + end, finally) + end) + + it("returns a table representing the rockspec from the given file", function() + test_env.run_in_tmp(function() + path.use_tree(testing_paths.testing_tree) + local rockspec = fetch.load_local_rockspec(testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec") + assert.same(rockspec.name, "a_rock") + assert.same(rockspec.version, "1.0-1") + assert.same(rockspec.description, { summary = "An example rockspec" }) + assert.same(rockspec.source.url, "http://localhost:8080/file/a_rock.lua") + end, finally) + end) + + it("returns false if the rockspec in invalid", function() + assert.falsy(fetch.load_local_rockspec(testing_paths.fixtures_dir .. "/invalid_say-1.3-1.rockspec")) + end) + + it("returns false if the rockspec version is not supported", function() + assert.falsy(fetch.load_local_rockspec("invalid_version.rockspec")) + end) + + it("returns false if the rockspec doesn't pass the type checking", function() + test_env.run_in_tmp(function() + write_file("type_mismatch_string-1.0-1.rockspec", [[ + package="type_mismatch_version" + version=1.0 + ]]) + assert.falsy(fetch.load_local_rockspec("type_mismatch_string-1.0-1.rockspec")) + end, finally) + end) + + it("returns false if the rockspec file name is not right", function() + test_env.run_in_tmp(function() + write_file("invalid_rockspec_name.rockspec", [[ + package="invalid_rockspec_name" + version="1.0-1" + source = { + url = "http://example.com/foo.tar.gz" + } + build = { + + } + ]]) + assert.falsy(fetch.load_local_rockspec("invalid_rockspec_name.rockspec")) + end, finally) + end) + + it("returns false if the version in the rockspec file name doesn't match the version declared in the rockspec", function() + test_env.run_in_tmp(function() + write_file("inconsistent_versions-1.0-1.rockspec", [[ + package="inconsistent_versions" + version="1.0-2" + source = { + url = "http://example.com/foo.tar.gz" + } + build = { + + } + ]]) + assert.falsy(fetch.load_local_rockspec("inconsistent_versions-1.0-1.rockspec")) + end, finally) + end) + end) + + describe("fetch.load_rockspec", function() + + it("returns a table containing the requested rockspec by downloading it into a temporary directory", function() + test_env.run_in_tmp(function() + path.use_tree(testing_paths.testing_tree) + local rockspec = fetch.load_rockspec("http://localhost:8080/file/a_rock-1.0-1.rockspec") + assert.same(rockspec.name, "a_rock") + assert.same(rockspec.version, "1.0-1") + assert.same(rockspec.description, { summary = "An example rockspec" }) + assert.same(rockspec.source.url, "http://localhost:8080/file/a_rock.lua") + rockspec = fetch.load_rockspec(testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec") + assert.same(rockspec.name, "a_rock") + assert.same(rockspec.version, "1.0-1") + assert.same(rockspec.description, { summary = "An example rockspec" }) + assert.same(rockspec.source.url, "http://localhost:8080/file/a_rock.lua") + end, finally) + end) + + it("returns a table containing the requested rockspec by downloading it into a given directory", function() + test_env.run_in_tmp(function() + local tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + + path.use_tree(testing_paths.testing_tree) + local rockspec = fetch.load_rockspec("http://localhost:8080/file/a_rock-1.0-1.rockspec", tmpdir) + assert.same(rockspec.name, "a_rock") + assert.same(rockspec.version, "1.0-1") + assert.same(rockspec.description, { summary = "An example rockspec" }) + assert.same(rockspec.source.url, "http://localhost:8080/file/a_rock.lua") + assert.truthy(lfs.attributes(tmpdir .. "/a_rock-1.0-1.rockspec")) + + lfs.rmdir(tmpdir) + end, finally) + end) + + it("returns false if the given download directory doesn't exist", function() + assert.falsy(fetch.load_rockspec("http://localhost:8080/file/a_rock-1.0-1.rockspec", "nonexistent")) + end) + + it("returns false if the given filename is not a valid rockspec name", function() + assert.falsy(fetch.load_rockspec("http://localhost:8080/file/a_rock.lua")) + end) + end) + + describe("fetch.get_sources", function() + + it("downloads the sources for building a rock and returns the resulting source filename and its parent directory", function() + test_env.run_in_tmp(function() + local rockspec = assert(fetch.load_rockspec("http://localhost:8080/file/a_rock-1.0-1.rockspec")) + local file, dirname = fetch.get_sources(rockspec, false) + assert.truthy(are_same_files(dirname .. "/a_rock.lua", file)) + end, finally) + end) + + it("downloads the sources for building a rock into a given directory and returns the resulting source filename and its parent directory", function() + test_env.run_in_tmp(function() + local tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + local rockspec = assert(fetch.load_rockspec("http://localhost:8080/file/a_rock-1.0-1.rockspec")) + local file, dirname = fetch.get_sources(rockspec, false, tmpdir) + assert.truthy(are_same_files(tmpdir, dirname)) + assert.truthy(are_same_files(dirname .. "/a_rock.lua", file)) + lfs.rmdir(tmpdir) + end, finally) + end) + + it("downloads the sources for building a rock, extracts the downloaded tarball and returns the resulting source filename and its parent directory", function() + test_env.run_in_tmp(function() + local rockspec = assert(fetch.load_rockspec("http://localhost:8080/file/busted_project-0.1-1.rockspec")) + local file, dirname = fetch.get_sources(rockspec, true) + assert.truthy(are_same_files(dirname .. "/busted_project-0.1.tar.gz", file)) + assert.truthy(lfs.attributes(dirname .. "/busted_project")) + assert.truthy(lfs.attributes(dirname .. "/busted_project/sum.lua")) + assert.truthy(lfs.attributes(dirname .. "/busted_project/spec/sum_spec.lua")) + end, finally) + end) + + it("returns false and does nothing if the destination directory doesn't exist", function() + test_env.run_in_tmp(function() + local rockspec = assert(fetch.load_rockspec("http://localhost:8080/file/a_rock-1.0-1.rockspec")) + assert.falsy(fetch.get_sources(rockspec, false, "nonexistent")) + end, finally) + end) + + it("returns false and does nothing if the rockspec source url is invalid", function() + test_env.run_in_tmp(function(tmpdir) + write_file(tmpdir .. "/invalid_url-1.0-1.rockspec", [[ + package="invalid_url" + version="1.0-1" + source = { + url = "http://localhost:8080/file/nonexistent" + } + build = { + + } + ]]) + local rockspec = assert(fetch.load_rockspec(tmpdir .. "/invalid_url-1.0-1.rockspec")) + assert.falsy(fetch.get_sources(rockspec, false)) + end, finally) + end) + + it("returns false and does nothing if the downloaded rockspec has an invalid md5 checksum", function() + test_env.run_in_tmp(function() + write_file("invalid_checksum-1.0-1.rockspec", [[ + package="invalid_checksum" + version="1.0-1" + source = { + url = "http://localhost:8080/file/a_rock.lua", + md5 = "invalid" + } + build = { + + } + ]]) + local rockspec = assert(fetch.load_rockspec("invalid_checksum-1.0-1.rockspec")) + assert.falsy(fetch.get_sources(rockspec, false)) + end, finally) + end) + end) + + describe("fetch_sources #unix #git", function() + local git_repo = require("spec.util.git_repo") + + local git + + setup(function() + git = git_repo.start() + end) + + teardown(function() + if git then + git:stop() + end + end) + + it("from #git", function() + local rockspec, err = rockspecs.from_persisted_table("testrock-dev-1.rockspec", { + rockspec_format = "3.0", + package = "testrock", + version = "dev-1", + source = { + url = "git://localhost/testrock", + }, + }, nil) + assert.falsy(err) + local pathname, tmpdir = fetch.fetch_sources(rockspec, false) + assert.are.same("testrock", pathname) + assert.match("luarocks_testrock%-dev%-1%-", tmpdir) + assert.match("^%d%d%d%d%d%d%d%d.%d%d%d%d%d%d.%x+$", tostring(rockspec.source.identifier)) + end) + end) + +end) diff --git a/spec/unit/fs_spec.lua b/spec/unit/fs_spec.lua new file mode 100644 index 0000000..c2a842b --- /dev/null +++ b/spec/unit/fs_spec.lua @@ -0,0 +1,1595 @@ +local test_env = require("spec.util.test_env") + +test_env.setup_specs() +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local cfg = require("luarocks.core.cfg") +local lfs = require("lfs") +local is_win = test_env.TEST_TARGET_OS == "windows" +local posix_ok = pcall(require, "posix") +local testing_paths = test_env.testing_paths +local get_tmp_path = test_env.get_tmp_path +local write_file = test_env.write_file +local P = test_env.P + +-- A chdir that works in both full and minimal mode, setting +-- both the real process current dir and the LuaRocks internal stack in minimal mode +local function chdir(d) + lfs.chdir(d) + fs.change_dir(d) +end + +describe("luarocks.fs #unit", function() + local exists_file = function(path) + local ok, err, code = os.rename(path, path) + if not ok and code == 13 then + return true + end + return ok + end + + local create_file = function(path, content) + local fd = assert(io.open(path, "w")) + if not content then + content = "foo" + end + assert(fd:write(content)) + fd:close() + end + + local make_unreadable = function(path) + if is_win then + fs.execute("icacls " .. fs.Q(path) .. " /inheritance:d /deny \"%USERNAME%\":(R)") + else + fs.execute("chmod -r " .. fs.Q(path)) + end + end + + local make_unwritable = function(path) + if is_win then + fs.execute("icacls " .. fs.Q(path) .. " /inheritance:d /deny \"%USERNAME%\":(W,M)") + else + fs.execute("chmod -w " .. fs.Q(path)) + end + end + + local make_unexecutable = function(path) + if is_win then + fs.execute("icacls " .. fs.Q(path) .. " /inheritance:d /deny \"%USERNAME%\":(X)") + else + fs.execute("chmod -x " .. fs.Q(path)) + end + end + + local runner + + lazy_setup(function() + cfg.init() + fs.init() + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + end) + + lazy_teardown(function() + runner.save_stats() + end) + + describe("fs.Q", function() + it("simple argument", function() + assert.are.same(is_win and '"foo"' or "'foo'", fs.Q("foo")) + end) + + it("argument with quotes", function() + assert.are.same(is_win and [["it's \"quoting\""]] or [['it'\''s "quoting"']], fs.Q([[it's "quoting"]])) + end) + + it("argument with special characters", function() + assert.are.same(is_win and [["\\"%" \\\\" \\\\\\"]] or [['\% \\" \\\']], fs.Q([[\% \\" \\\]])) + end) + end) + + describe("fs.absolute_name", function() + it("unchanged if already absolute", function() + if is_win then + assert.are.same(P"c:\\foo\\bar", fs.absolute_name("\"c:\\foo\\bar\"")) + assert.are.same(P"c:\\foo\\bar", fs.absolute_name("c:\\foo\\bar")) + assert.are.same(P"d:\\foo\\bar", fs.absolute_name("d:\\foo\\bar")) + assert.are.same(P"\\foo\\bar", fs.absolute_name("\\foo\\bar")) + else + assert.are.same(P"/foo/bar", fs.absolute_name("/foo/bar")) + end + end) + + it("converts to absolute if relative", function() + local cur = fs.current_dir() + if is_win then + assert.are.same(P(cur .. "/foo\\bar"), fs.absolute_name("\"foo\\bar\"")) + assert.are.same(P(cur .. "/foo\\bar"), fs.absolute_name("foo\\bar")) + else + assert.are.same(P(cur .. "/foo/bar"), fs.absolute_name("foo/bar")) + end + end) + + it("converts a relative to specified base if given", function() + if is_win then + assert.are.same(P"c:\\bla/foo\\bar", fs.absolute_name("\"foo\\bar\"", "c:\\bla")) + assert.are.same(P"c:\\bla/foo\\bar", fs.absolute_name("foo/bar", "c:\\bla")) + assert.are.same(P"c:\\bla/foo\\bar", fs.absolute_name("foo\\bar", "c:\\bla\\")) + else + assert.are.same(P"/bla/foo/bar", fs.absolute_name("foo/bar", "/bla")) + assert.are.same(P"/bla/foo/bar", fs.absolute_name("foo/bar", "/bla/")) + end + end) + end) + + describe("fs.execute_string", function() + local tmpdir + + after_each(function() + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns the status code and runs the command given in the argument", function() + tmpdir = get_tmp_path() + assert.truthy(fs.execute_string("mkdir " .. fs.Q(tmpdir))) + assert.truthy(fs.is_dir(tmpdir)) + assert.falsy(fs.execute_string("invalidcommand")) + end) + end) + + describe("fs.dir_iterator", function() + local tmpfile1 + local tmpfile2 + local tmpdir + local intdir + + after_each(function() + if tmpfile1 then + os.remove(tmpfile1) + tmpfile1 = nil + end + if tmpfile2 then + os.remove(tmpfile2) + tmpfile2 = nil + end + if intdir then + lfs.rmdir(intdir) + intdir = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("yields all files and directories in the directory given as argument during the iterations", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + tmpfile1 = tmpdir .. "/file1" + create_file(tmpfile1) + tmpfile2 = tmpdir .. "/file2" + create_file(tmpfile2) + intdir = tmpdir .. "/intdir" + lfs.mkdir(intdir) + local dirTable = {} + local dirCount = 0 + local crt = coroutine.create(fs.dir_iterator) + while coroutine.status(crt) ~= "dead" do + local ok, val = coroutine.resume(crt, tmpdir) + if ok and val ~= nil then + dirTable[val] = true + dirCount = dirCount + 1 + end + end + assert.same(dirCount, 3) + assert.is_not.same(dirTable["file1"], nil) + assert.is_not.same(dirTable["file2"], nil) + assert.is_not.same(dirTable["intdir"], nil) + dirCount = 0 + crt = coroutine.create(fs.dir_iterator) + while coroutine.status(crt) ~= "dead" do + local ok, val = coroutine.resume(crt, intdir) + if ok and val ~= nil then + dirCount = dirCount + 1 + end + end + assert.same(dirCount, 0) + end) + + it("does nothing if the argument is a file", function() + tmpfile1 = get_tmp_path() + create_file(tmpfile1) + local crt = coroutine.create(fs.dir_iterator) + while coroutine.status(crt) ~= "dead" do + local ok, val = coroutine.resume(crt, tmpfile1) + assert.falsy(ok and res) + end + end) + + it("does nothing if the argument is invalid", function() + local crt = coroutine.create(fs.dir_iterator) + while coroutine.status(crt) ~= "dead" do + local ok, val = coroutine.resume(crt, "/nonexistent") + assert.falsy(ok and res) + end + end) + end) + + describe("fs.is_writable", function() + local tmpfile + local tmpdir + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true if the file given as argument is writable", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + assert.truthy(fs.is_writable(tmpfile)) + end) + + it("returns true if the directory given as argument is writable", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + assert.truthy(fs.is_writable(tmpdir)) + tmpfile = tmpdir .. "/internalfile" + create_file(tmpfile) + make_unwritable(tmpfile) + assert.truthy(fs.is_writable(tmpdir)) + end) + + it("returns false if the file given as argument is not writable", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + make_unwritable(tmpfile) + assert.falsy(fs.is_writable(tmpfile)) + end) + + it("returns false if the directory given as argument is not writable", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + make_unwritable(tmpdir) + assert.falsy(fs.is_writable(tmpdir)) + end) + + it("returns false if the file or directory given as argument does not exist", function() + assert.falsy(fs.is_writable("/nonexistent")) + end) + end) + + describe("fs.set_time #unix", function() + local tmpfile + local tmpdir + local intdir + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if intdir then + os.remove(intdir) + intdir = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true and modifies the access time of the file given as argument", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + local newtime = os.time() - 100 + assert.truthy(fs.set_time(tmpfile, newtime)) + assert.same(lfs.attributes(tmpfile, "access"), newtime) + assert.same(lfs.attributes(tmpfile, "modification"), newtime) + end) + + it("returns true and modifies the access time of the directory given as argument", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + tmpfile = tmpdir .. "/internalfile" + create_file(tmpfile) + local newtime = os.time() - 100 + assert.truthy(fs.set_time(tmpdir, newtime)) + assert.same(lfs.attributes(tmpdir, "access"), newtime) + assert.same(lfs.attributes(tmpdir, "modification"), newtime) + assert.is_not.same(lfs.attributes(tmpfile, "access"), newtime) + assert.is_not.same(lfs.attributes(tmpfile, "modification"), newtime) + end) + + it("returns false and does nothing if the file or directory given as arguments doesn't exist", function() + assert.falsy(fs.set_time("/nonexistent")) + end) + end) + + describe("fs.set_permissions", function() + local readfile + local execfile + local tmpdir + + after_each(function() + if readfile then + os.remove(readfile) + readfile = nil + end + if execfile then + os.remove(execfile) + execfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true and sets the permissions of the argument accordingly", function() + readfile = get_tmp_path() + create_file(readfile) + make_unreadable(readfile) + assert.falsy(io.open(readfile, "r")) + assert.truthy(fs.set_permissions(readfile, "read", "user")) + assert.truthy(io.open(readfile, "r")) + + if is_win then + execfile = get_tmp_path() .. ".exe" + create_file(execfile) + else + execfile = get_tmp_path() .. ".sh" + create_file(execfile, "#!/bin/bash") + end + make_unexecutable(execfile) + local fd = assert(io.popen(execfile .. " 2>&1")) + local result = assert(fd:read("*a")) + assert.truthy(result:match("denied")) + fd:close() + assert.truthy(fs.set_permissions(execfile, "exec", "user")) + fd = assert(io.popen(execfile .. " 2>&1")) + result = assert(fd:read("*a")) + assert.falsy(result:match("denied")) + fd:close() + + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + make_unexecutable(tmpdir) + fd = assert(io.popen("cd " .. fs.Q(tmpdir) .. " 2>&1")) + result = assert(fd:read("*a")) + assert.truthy(result:match("denied") or result:match("can't cd")) + fd:close() + assert.truthy(fs.set_permissions(tmpdir, "exec", "user")) + fd = assert(io.popen("cd " .. fs.Q(tmpdir) .. " 2>&1")) + result = assert(fd:read("*a")) + assert.falsy(result:match("denied") or result:match("can't cd")) + fd:close() + end) + + it("returns false and does nothing if the argument is nonexistent", function() + assert.falsy(fs.set_permissions("/nonexistent", "read", "user")) + end) + end) + + describe("fs.is_file", function() + local tmpfile + local tmpdir + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true when the argument is a file", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + assert.same(true, fs.is_file(tmpfile)) + end) + + it("returns false when the argument does not exist", function() + assert.same(false, fs.is_file("/nonexistent")) + end) + + it("returns false when the argument exists but is not a file", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + assert.same(false, fs.is_file("/nonexistent")) + end) + + it("#unix returns false when the argument is a symlink to a directory", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + local linkname = tmpdir .. "/symlink" + finally(function() os.remove(linkname) end) + lfs.link(tmpdir, linkname, true) + assert.falsy(fs.is_file(linkname)) + end) + + it("#unix returns true when the argument is a symlink to a file", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + local linkname = tmpfile .. "_symlink" + finally(function() os.remove(linkname) end) + lfs.link(tmpfile, linkname, true) + assert.truthy(fs.is_file(linkname)) + end) + end) + + describe("fs.is_dir", function() + local tmpfile + local tmpdir + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true when the argument is a directory", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + assert.truthy(fs.is_dir(tmpdir)) + end) + + it("returns false when the argument is a file", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + assert.falsy(fs.is_dir(tmpfile)) + end) + + it("#unix returns true when the argument is a symlink to a directory", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + local linkname = tmpdir .. "/symlink" + finally(function() os.remove(linkname) end) + lfs.link(tmpdir, linkname, true) + assert.truthy(fs.is_dir(linkname)) + end) + + it("#unix returns false when the argument is a symlink to a file", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + local linkname = tmpfile .. "_symlink" + finally(function() os.remove(linkname) end) + lfs.link(tmpfile, linkname, true) + assert.falsy(fs.is_dir(linkname)) + end) + + it("returns false when the argument does not exist", function() + assert.falsy(fs.is_dir("/nonexistent")) + end) + end) + + describe("fs.exists", function() + local tmpfile + local tmpdir + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true when the argument is a file", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + assert.truthy(fs.exists(tmpfile)) + end) + + it("returns true when the argument is a directory", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + assert.truthy(fs.exists(tmpdir)) + end) + + it("returns false when the argument does not exist", function() + assert.falsy(fs.exists("/nonexistent")) + end) + end) + + describe("fs.current_dir", function() + local tmpdir + local olddir + + before_each(function() + olddir = lfs.currentdir() + end) + + after_each(function() + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + if olddir then + chdir(olddir) + olddir = nil + end + end) + + it("returns the current working directory", function() + local currentdir = lfs.currentdir() + assert.same(currentdir, fs.current_dir()) + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + assert.truthy(fs.change_dir(tmpdir)) + if is_win then + assert.same(tmpdir, fs.current_dir()) + else + assert.same(lfs.attributes(tmpdir).ino, lfs.attributes((fs.current_dir())).ino) + end + end) + end) + + describe("fs.change_dir", function() + local tmpfile + local tmpdir + local olddir + + before_each(function() + olddir = lfs.currentdir() + end) + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + if olddir then + chdir(olddir) + olddir = nil + end + end) + + it("returns true and changes the current working directory if the argument is a directory", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + assert.truthy(fs.change_dir(tmpdir)) + if is_win then + assert.same(tmpdir, fs.current_dir()) + else + assert.same(lfs.attributes(tmpdir).ino, lfs.attributes(fs.current_dir()).ino) + end + end) + + it("returns false and does nothing when the argument is a file", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + assert.falsy(fs.change_dir(tmpfile)) + assert.same(olddir, lfs.currentdir()) + end) + + it("returns false and does nothing when the argument does not exist", function() + assert.falsy(fs.change_dir("/nonexistent")) + assert.same(olddir, lfs.currentdir()) + end) + end) + + describe("fs.change_dir_to_root", function() + local tmpdir + local olddir + + before_each(function() + olddir = lfs.currentdir() + end) + + after_each(function() + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + if olddir then + chdir(olddir) + end + end) + + it("returns true and changes the current directory to root if the current directory is valid", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + assert.truthy(fs.change_dir(tmpdir)) + assert.truthy(fs.change_dir_to_root()) + if is_win then + local curr_dir = fs.current_dir() + assert.truthy(curr_dir == "C:\\" or curr_dir == P"/") + else + assert.same(P"/", fs.current_dir()) + end + end) + + it("returns false and does nothing if the current directory is not valid #unix", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + chdir(tmpdir) + lfs.rmdir(tmpdir) + assert.falsy(fs.change_dir_to_root()) + assert.is_not.same("/", lfs.currentdir()) + end) + end) + + describe("fs.pop_dir", function() + local tmpdir + local olddir + + before_each(function() + olddir = lfs.currentdir() + end) + + after_each(function() + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + if olddir then + chdir(olddir) + end + end) + + it("returns true and changes the current directory to the previous one in the dir stack if the dir stack is not empty", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + assert.truthy(fs.change_dir(tmpdir)) + assert.truthy(fs.pop_dir()) + assert.same(olddir, lfs.currentdir()) + end) + end) + + describe("fs.make_dir", function() + local tmpfile + local tmpdir + local intdir + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if intdir then + lfs.rmdir(intdir) + intdir = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true and creates the directory specified by the argument", function() + tmpdir = get_tmp_path() + assert.truthy(fs.make_dir(tmpdir)) + assert.same("directory", lfs.attributes(tmpdir, "mode")) + end) + + it("returns true and creates the directory path specified by the argument", function() + tmpdir = get_tmp_path() + intdir = "/internaldir" + local dirpath = tmpdir .. intdir + assert.truthy(fs.make_dir(dirpath)) + assert.same("directory", lfs.attributes(tmpdir, "mode")) + assert.same("directory", lfs.attributes(dirpath, "mode")) + end) + + it("returns false and does nothing if the argument is not valid (file in the path)", function() + tmpfile = get_tmp_path() + local fd = assert(io.open(tmpfile, "w")) + assert(fd:write("foo")) + fd:close() + intdir = "/internaldir" + local dirpath = tmpfile .. intdir + assert.falsy(fs.make_dir(dirpath)) + end) + + it("returns false and does nothing if the argument already exists", function() + tmpfile = get_tmp_path() + create_file(tmpfile) + assert.falsy(fs.make_dir(tmpfile)) + end) + end) + + describe("fs.remove_dir_if_empty", function() + local tmpfile + local tmpdir + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("removes the directory specified by the argument if it is empty", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + fs.remove_dir_if_empty(tmpdir) + assert.falsy(exists_file(tmpdir)) + end) + + it("does nothing if the directory specified by the argument is not empty", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + tmpfile = "/internalfile" + local filepath = tmpdir .. tmpfile + create_file(filepath) + fs.remove_dir_if_empty(tmpdir) + assert.truthy(exists_file(tmpdir)) + end) + end) + + describe("fs.remove_dir_tree_if_empty", function() + local tmpfile + local tmpdir + local intdir + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + if intdir then + lfs.rmdir(intdir) + intdir = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("removes the directory path specified by the argument if it is empty", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + fs.remove_dir_tree_if_empty(tmpdir) + assert.falsy(exists_file(tmpdir)) + end) + + it("does nothing if the directory path specified by the argument is not empty", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + intdir = "/internaldir" + local dirpath = tmpdir .. intdir + lfs.mkdir(dirpath) + tmpfile = "/internalfile" + local filepath = dirpath .. tmpfile + fs.remove_dir_tree_if_empty(tmpdir) + assert.truthy(exists_file(dirpath)) + assert.truthy(exists_file(tmpdir)) + end) + end) + + describe("fs.list_dir", function() + local intfile1 + local intfile2 + local intdir + local tmpdir + + before_each(function() + if intfile1 then + os.remove(intfile1) + intfile1 = nil + end + if intfile2 then + os.remove(intfile2) + intfile2 = nil + end + if intdir then + lfs.rmdir(intdir) + intdir = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns a table with the contents of the given directory", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + intfile1 = tmpdir .. "/intfile1" + create_file(intfile1) + intdir = tmpdir .. "/intdir" + lfs.mkdir(intdir) + intfile2 = intdir .. "/intfile2" + create_file(intfile2) + local result = fs.list_dir(tmpdir) + assert.same(#result, 2) + assert.truthy(result[1] == "intfile1" or result[1] == "intdir") + assert.truthy(result[2] == "intfile1" or result[2] == "intdir") + assert.is_not.same(result[1], result[2]) + end) + + it("returns an empty table if the argument is a file", function() + intfile1 = get_tmp_path() + create_file(intfile1) + local result = fs.list_dir(intfile1) + assert.same(#result, 0) + end) + + it("does nothing if the argument is nonexistent", function() + assert.same(fs.list_dir("/nonexistent"), {}) + end) + + it("does nothing if the argument doesn't have the proper permissions", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + make_unreadable(tmpdir) + assert.same(fs.list_dir(tmpdir), {}) + end) + end) + + describe("fs.copy", function() + local srcfile + local dstfile + local tmpdir + + after_each(function() + if srcfile then + os.remove(srcfile) + srcfile = nil + end + if dstfile then + os.remove(dstfile) + dstfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true and copies the contents and the permissions of the source file to the destination file", function() + srcfile = get_tmp_path() + create_file(srcfile, srccontent) + dstfile = get_tmp_path() + assert.truthy(fs.copy(srcfile, dstfile)) + local fd = assert(io.open(dstfile, "r")) + local dstcontent = fd:read("*a") + assert.same("foo", dstcontent) + if posix_ok then + assert.same(lfs.attributes(srcfile, "permissions"), lfs.attributes(dstfile, "permissions")) + end + end) + + it("returns true and copies contents of the source file to the destination file with custom permissions", function() + srcfile = get_tmp_path() + create_file(srcfile, srccontent) + dstfile = get_tmp_path() + assert.truthy(fs.copy(srcfile, dstfile, "exec")) + local fd = assert(io.open(dstfile, "r")) + local dstcontent = fd:read("*a") + assert.same("foo", dstcontent) + end) + + it("returns false and does nothing if the source file does not exist", function() + srcfile = get_tmp_path() + dstfile = get_tmp_path() + local ok, err = fs.copy(srcfile, dstfile, nil) + assert.falsy(ok) + assert.not_match("are the same file", err) + assert.falsy(exists_file(dstfile)) + end) + + it("returns false and does nothing if the source file doesn't have the proper permissions", function() + srcfile = get_tmp_path() + create_file(srcfile) + make_unreadable(srcfile) + dstfile = get_tmp_path() + assert.falsy(fs.copy(srcfile, dstfile, nil)) + assert.falsy(exists_file(dstfile)) + end) + + it("returns false and does nothing if the destination file directory doesn't have the proper permissions", function() + srcfile = get_tmp_path() + create_file(srcfile) + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + make_unwritable(tmpdir) + dstfile = tmpdir .. "/dstfile" + assert.falsy(fs.copy(srcfile, dstfile, nil)) + assert(fs.set_permissions(tmpdir, "exec", "all")) + assert.falsy(exists_file(dstfile)) + end) + end) + + describe("fs.copy_contents", function() + local srcfile + local dstfile + local srcintdir + local dstintdir + local srcdir + local dstdir + + after_each(function() + if srcfile then + os.remove(srcfile) + srcfile = nil + end + if dstfile then + os.remove(dstfile) + dstfile = nil + end + if srcintdir then + lfs.rmdir(srcintdir) + srcintdir = nil + end + if dstintdir then + lfs.rmdir(dstintdir) + dstintdir = nil + end + if srcdir then + lfs.rmdir(srcdir) + srcdir = nil + end + if dstdir then + lfs.rmdir(dstdir) + dstdir = nil + end + end) + + local create_dir_tree = function() + srcdir = get_tmp_path() + lfs.mkdir(srcdir) + srcintdir = srcdir .. "/internaldir" + lfs.mkdir(srcintdir) + srcfile = srcintdir .. "/internalfile" + create_file(srcfile) + dstdir = get_tmp_path() + end + + it("returns true and copies the contents (with their permissions) of the source dir to the destination dir", function() + create_dir_tree() + assert.truthy(fs.copy_contents(srcdir, dstdir)) + assert.truthy(exists_file(dstdir)) + dstintdir = dstdir .. "/internaldir" + assert.truthy(exists_file(dstintdir)) + dstfile = dstdir .. "/internaldir/internalfile" + local fd = assert(io.open(dstfile, "r")) + local dstfilecontent = fd:read("*a") + assert.same("foo", dstfilecontent) + if posix_ok then + assert.same(lfs.attributes(srcfile, "permissions"), lfs.attributes(dstfile, "permissions")) + end + end) + + it("returns true and copies the contents of the source dir to the destination dir with custom permissions", function() + create_dir_tree() + assert.truthy(fs.copy_contents(srcdir, dstdir, "read")) + assert.truthy(exists_file(dstdir)) + dstintdir = dstdir .. "/internaldir" + assert.truthy(exists_file(dstintdir)) + dstfile = dstdir .. "/internaldir/internalfile" + local fd = assert(io.open(dstfile, "r")) + local dstfilecontent = fd:read("*a") + assert.same("foo", dstfilecontent) + end) + + it("returns false and does nothing if the source dir doesn't exist", function() + srcdir = get_tmp_path() + dstdir = get_tmp_path() + assert.falsy(fs.copy_contents(srcdir, dstdir)) + assert.falsy(exists_file(dstdir)) + end) + + it("returns false if the source argument is a file", function() + srcdir = get_tmp_path() + create_file(srcdir) + dstdir = get_tmp_path() + assert.falsy(fs.copy_contents(srcdir, dstdir)) + assert.falsy(exists_file(dstdir)) + end) + + it("returns false and does nothing if the source dir doesn't have the proper permissions", function() + create_dir_tree() + make_unreadable(srcdir) + assert.falsy(fs.copy_contents(srcdir, dstdir)) + assert.falsy(exists_file(dstdir .. "/internaldir")) + assert.falsy(exists_file(dstdir .. "/internalfile")) + end) + end) + + describe("fs.find", function() + local tmpdir + local intdir + local intfile1 + local intfile2 + + after_each(function() + if intfile1 then + os.remove(intfile1) + intfile1 = nil + end + if intfile2 then + os.remove(intfile2) + intfile2 = nil + end + if intdir then + lfs.rmdir(intdir) + intdir = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + local create_dir_tree = function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + intfile1 = tmpdir .. "/intfile1" + create_file(intfile1) + intdir = tmpdir .. "/intdir" + lfs.mkdir(intdir) + intfile2 = intdir .. "/intfile2" + create_file(intfile2) + end + + it("returns a table of all the contents in the directory given as argument", function() + create_dir_tree() + local contents = {} + local count = 0 + for _, file in pairs(fs.find(tmpdir)) do + contents[file] = true + count = count + 1 + end + assert.same(count, 3) + assert.is_not.same(contents[tmpdir], true) + assert.same(contents[P"intfile1"], true) + assert.same(contents[P"intdir"], true) + assert.same(contents[P"intdir/intfile2"], true) + end) + + it("uses the current working directory if the argument is nil", function() + create_dir_tree() + local olddir = fs.current_dir() + fs.change_dir(intdir) + local contents = {} + local count = 0 + for _, file in pairs(fs.find()) do + contents[file] = true + count = count + 1 + end + assert.same(count, 1) + assert.is_not.same(contents["intfile1"], true) + assert.is_not.same(contents["intdir"], true) + assert.same(contents["intfile2"], true) + fs.change_dir(olddir) + end) + + it("returns an empty table if the argument is nonexistent", function() + local contents = fs.find("/nonexistent") + local count = 0 + for _, file in pairs(contents) do + count = count + 1 + end + assert.same(count, 0) + end) + + it("returns an empty table if the argument is a file", function() + intfile1 = get_tmp_path() + create_file(intfile1) + local contents = fs.find(intfile1) + local count = 0 + for _, file in pairs(contents) do + count = count + 1 + end + assert.same(count, 0) + end) + + it("does nothing if the argument doesn't have the proper permissions", function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + make_unreadable(tmpdir) + assert.same(fs.find(tmpdir), {}) + end) + end) + + describe("fs.move", function() + local srcfile + local dstfile + local tmpdir + + after_each(function() + if srcfile then + os.remove(srcfile) + srcfile = nil + end + if dstfile then + os.remove(dstfile) + dstfile = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + it("returns true and moves the source (together with its permissions) to the destination", function() + srcfile = get_tmp_path() + create_file(srcfile) + dstfile = get_tmp_path() + local oldperms = lfs.attributes(srcfile, "permissions") + assert.truthy(fs.move(srcfile, dstfile)) + assert.truthy(fs.exists(dstfile)) + assert.falsy(fs.exists(srcfile)) + local fd = assert(io.open(dstfile, "r")) + local dstcontents = assert(fd:read("*a")) + assert.same(dstcontents, "foo") + if posix_ok then + assert.same(oldperms, lfs.attributes(dstfile, "permissions")) + end + end) + + it("returns true and moves the source (with custom permissions) to the destination", function() + srcfile = get_tmp_path() + create_file(srcfile) + dstfile = get_tmp_path() + assert.truthy(fs.move(srcfile, dstfile, "read")) + assert.truthy(fs.exists(dstfile)) + assert.falsy(fs.exists(srcfile)) + local fd = assert(io.open(dstfile, "r")) + local dstcontents = assert(fd:read("*a")) + assert.same(dstcontents, "foo") + end) + + it("returns false and does nothing if the source doesn't exist", function() + dstfile = get_tmp_path() + assert.falsy(fs.move("/nonexistent", dstfile)) + assert.falsy(fs.exists(dstfile)) + end) + + it("returns false and does nothing if the destination already exists", function() + srcfile = get_tmp_path() + create_file(srcfile) + dstfile = get_tmp_path() + create_file(dstfile, "bar") + assert.falsy(fs.move(srcfile, dstfile)) + assert.truthy(fs.exists(srcfile)) + local fd = assert(io.open(dstfile, "r")) + local dstcontents = assert(fd:read("*a")) + assert.same(dstcontents, "bar") + end) + + it("returns false and does nothing if the destination path doesn't have the proper permissions", function() + srcfile = get_tmp_path() + create_file(srcfile) + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + make_unwritable(tmpdir) + assert.falsy(fs.move(srcfile, tmpdir .. "/dstfile")) + assert.falsy(fs.exists(tmpdir .. "/dstfile")) + end) + end) + + describe("fs.is_lua", function() + local tmpfile + + after_each(function() + if tmpfile then + os.remove(tmpfile) + tmpfile = nil + end + end) + + it("returns true if the argument is a valid lua script", function() + tmpfile = get_tmp_path() + create_file(tmpfile, "print(\"foo\")") + assert.truthy(fs.is_lua(tmpfile)) + end) + + it("returns true if the argument is a valid lua script with shebang", function() + tmpfile = get_tmp_path() + create_file(tmpfile, "#!/usr/bin/env lua\n\nprint(\"foo\")") + assert.truthy(fs.is_lua(tmpfile)) + end) + + it("returns false if the argument is not a valid lua script", function() + tmpfile = os.tmpname() + create_file(tmpfile) + assert.falsy(fs.is_lua(tmpfile)) + end) + + it("returns false if the argument is a valid lua script but doesn't have the proper permissions", function() + tmpfile = get_tmp_path() + create_file(tmpfile, "print(\"foo\")") + make_unreadable(tmpfile) + assert.falsy(fs.is_lua(tmpfile)) + end) + end) + + describe("fs.delete", function() + local tmpfile1 + local tmpfile2 + local tmpintdir + local tmpdir + + after_each(function() + if tmpfile1 then + os.remove(tmpfile1) + tmpfile1 = nil + end + if tmpfile2 then + os.remove(tmpfile2) + tmpfile2 = nil + end + if tmpintdir then + lfs.rmdir(tmpintdir) + tmpintdir = nil + end + if tmpdir then + lfs.rmdir(tmpdir) + tmpdir = nil + end + end) + + local create_dir_tree = function() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + tmpintdir = tmpdir .. "/internaldir" + lfs.mkdir(tmpintdir) + tmpfile1 = tmpdir .. "/internalfile1" + create_file(tmpfile1) + tmpfile2 = tmpdir .. "/internalfile2" + create_file(tmpfile2) + end + + it("deletes the file specified by the argument", function() + tmpfile1 = get_tmp_path() + tmpfile2 = get_tmp_path() + fs.delete(tmpfile1) + fs.delete(tmpfile2) + assert.falsy(exists_file(tmpfile1)) + assert.falsy(exists_file(tmpfile2)) + end) + + it("deletes the contents of the directory specified by the argument", function() + create_dir_tree() + fs.delete(tmpdir) + assert.falsy(exists_file(tmpfile2)) + assert.falsy(exists_file(tmpintdir)) + assert.falsy(exists_file(tmpfile1)) + assert.falsy(exists_file(tmpdir)) + end) + end) + + describe("fs.zip", function() + local tmpdir + local olddir + + before_each(function() + olddir = lfs.currentdir() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + chdir(tmpdir) + + write_file("file1", "content1", finally) + write_file("file2", "content2", finally) + lfs.mkdir("dir") + write_file("dir/file3", "content3", finally) + end) + + after_each(function() + if olddir then + chdir(olddir) + if tmpdir then + lfs.rmdir(tmpdir .. "/dir") + lfs.rmdir(tmpdir) + end + end + end) + + it("returns true and creates a zip archive of the given files", function() + assert.truthy(fs.zip("archive.zip", "file1", "file2", "dir")) + assert.truthy(exists_file("archive.zip")) + end) + + it("returns false and does nothing if the files specified in the arguments are invalid", function() + assert.falsy(fs.zip("archive.zip", "nonexistent")) + assert.falsy(exists_file("nonexistent")) + end) + end) + + describe("fs.bunzip2", function() + + it("uncompresses a .bz2 file", function() + local input = testing_paths.fixtures_dir .. "/abc.bz2" + local output = os.tmpname() + assert.truthy(fs.bunzip2(input, output)) + local fd = io.open(output, "r") + local content = fd:read("*a") + fd:close() + assert.same(300000, #content) + local abc = ("a"):rep(100000)..("b"):rep(100000)..("c"):rep(100000) + assert.same(abc, content) + end) + + end) + + describe("fs.unzip", function() + local tmpdir + local olddir + + before_each(function() + olddir = lfs.currentdir() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + chdir(tmpdir) + + write_file("file1", "content1", finally) + write_file("file2", "content2", finally) + lfs.mkdir("dir") + write_file("dir/file3", "content3", finally) + end) + + after_each(function() + if olddir then + chdir(olddir) + if tmpdir then + lfs.rmdir(tmpdir .. "/dir") + lfs.rmdir(tmpdir) + end + end + end) + + it("returns true and unzips the given zip archive", function() + assert.truthy(fs.zip("archive.zip", "file1", "file2", "dir")) + os.remove("file1") + os.remove("file2") + lfs.rmdir("dir") + + assert.truthy(fs.unzip("archive.zip")) + assert.truthy(exists_file("file1")) + assert.truthy(exists_file("file2")) + assert.truthy(exists_file("dir/file3")) + + local fd + + fd = assert(io.open("file1", "r")) + assert.same(fd:read("*a"), "content1") + fd:close() + + fd = assert(io.open("file2", "r")) + assert.same(fd:read("*a"), "content2") + fd:close() + + fd = assert(io.open("dir/file3", "r")) + assert.same(fd:read("*a"), "content3") + fd:close() + end) + + it("does nothing if the given archive is invalid", function() + assert.falsy(fs.unzip("archive.zip")) + end) + end) + + describe("fs.wrap_script", function() + local tmpdir + local olddir + + before_each(function() + olddir = lfs.currentdir() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + chdir(tmpdir) + end) + + after_each(function() + if olddir then + chdir(olddir) + if tmpdir then + lfs.rmdir(tmpdir) + end + end + end) + + it("produces a wrapper for a Lua script", function() + write_file("my_script", "io.write('Hello ' .. arg[1])", finally) + path.use_tree(testing_paths.testing_tree) + local wrapper_name = fs.absolute_name("wrapper") .. test_env.wrapper_extension + fs.wrap_script("my_script", wrapper_name, "one", nil, nil, "World") + local pd = assert(io.popen(wrapper_name)) + local data = pd:read("*a") + pd:close() + assert.same("Hello World", data) + end) + end) + + describe("fs.copy_binary", function() + local tmpdir + local olddir + + before_each(function() + olddir = lfs.currentdir() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + chdir(tmpdir) + + write_file("test.exe", "", finally) + end) + + after_each(function() + if olddir then + chdir(olddir) + if tmpdir then + lfs.rmdir(tmpdir) + end + end + end) + + it("returns true and copies the given binary file to the file specified in the dest argument", function() + assert.truthy(fs.copy_binary("test.exe", lfs.currentdir() .. "/copy.exe")) + assert.truthy(exists_file("copy.exe")) + if is_win then + assert.truthy(exists_file("test.lua")) + local fd = assert(io.open("test.lua", "r")) + local content = assert(fd:read("*a")) + assert.truthy(content:find("package.path", 1, true)) + assert.truthy(content:find("package.cpath", 1, true)) + fd:close() + end + end) + + it("returns false and does nothing if the source file is invalid", function() + assert.falsy(fs.copy_binary("invalid.exe", "copy.exe")) + end) + end) + + describe("fs.modules", function() + local tmpdir + local olddir + local oldpath + + before_each(function() + olddir = lfs.currentdir() + tmpdir = get_tmp_path() + lfs.mkdir(tmpdir) + chdir(tmpdir) + lfs.mkdir("lib") + write_file("lib/module1.lua", "", finally) + write_file("lib/module2.lua", "", finally) + write_file("lib/module1.LuA", "", finally) + write_file("lib/non_lua", "", finally) + lfs.mkdir("lib/internal") + write_file("lib/internal/module11.lua", "", finally) + write_file("lib/internal/module22.lua", "", finally) + + oldpath = package.path + package.path = package.path .. tmpdir .. "/?.lua;" + end) + + after_each(function() + if olddir then + chdir(olddir) + if tmpdir then + lfs.rmdir(tmpdir .. "/lib/internal") + lfs.rmdir(tmpdir .. "/lib") + lfs.rmdir(tmpdir) + end + end + if oldpath then + package.path = oldpath + end + end) + + it("returns a table of the lua modules at a specific require path", function() + local result + + result = fs.modules("lib") + assert.same(#result, 2) + assert.truthy(result[1] == "module1" or result[2] == "module1") + assert.truthy(result[1] == "module2" or result[2] == "module2") + + result = fs.modules("lib.internal") + assert.same(#result, 2) + assert.truthy(result[1] == "module11" or result[2] == "module11") + assert.truthy(result[1] == "module22" or result[2] == "module22") + end) + + it("returns an empty table if the modules couldn't be found in package.path", function() + package.path = "" + assert.same(fs.modules("lib"), {}) + end) + end) + + describe("#unix fs._unix_rwx_to_number", function() + + it("converts permissions in rwx notation to numeric ones", function() + assert.same(tonumber("0644", 8), fs._unix_rwx_to_number("rw-r--r--")) + assert.same(tonumber("0755", 8), fs._unix_rwx_to_number("rwxr-xr-x")) + assert.same(tonumber("0000", 8), fs._unix_rwx_to_number("---------")) + assert.same(tonumber("0777", 8), fs._unix_rwx_to_number("rwxrwxrwx")) + assert.same(tonumber("0700", 8), fs._unix_rwx_to_number("rwx------")) + assert.same(tonumber("0600", 8), fs._unix_rwx_to_number("rw-------")) + end) + + it("produces a negated mask if asked to", function() + assert.same(tonumber("0133", 8), fs._unix_rwx_to_number("rw-r--r--", true)) + assert.same(tonumber("0022", 8), fs._unix_rwx_to_number("rwxr-xr-x", true)) + assert.same(tonumber("0777", 8), fs._unix_rwx_to_number("---------", true)) + assert.same(tonumber("0000", 8), fs._unix_rwx_to_number("rwxrwxrwx", true)) + assert.same(tonumber("0077", 8), fs._unix_rwx_to_number("rwx------", true)) + assert.same(tonumber("0177", 8), fs._unix_rwx_to_number("rw-------", true)) + end) + end) + + describe("fs.execute_env", function() + local tmpname + local tmplua + local LUA = "lua" + + local function readfile(pathname) + local file = assert(io.open(pathname, "rb")) + local data = file:read "*a" + file:close() + return data + end + + lazy_setup(function() + tmpname = os.tmpname() + + tmplua = os.tmpname() + local f = assert(io.open(tmplua, 'wb')) + f:write [[ + local out = io.open((...), 'wb') + out:write(os.getenv 'FOO') + out:close() + ]] + f:close() + LUA = test_env.testing_paths.lua + end) + + after_each(function() + os.remove(tmpname) + end) + + lazy_teardown(function() + os.remove(tmpname) + end) + + it("passes variables w/o spaces correctly", function() + fs.execute_env({ + FOO = "BAR", + }, LUA, tmplua, tmpname) + local data = readfile(tmpname) + assert.same("BAR", data) + end) + + it("passes variables w/ spaces correctly", function() + fs.execute_env({ + FOO = "BAR with spaces", + }, LUA, tmplua, tmpname) + local data = readfile(tmpname) + assert.same("BAR with spaces", data) + end) + end) + +end) diff --git a/spec/unit/fun_spec.lua b/spec/unit/fun_spec.lua new file mode 100644 index 0000000..bffb60d --- /dev/null +++ b/spec/unit/fun_spec.lua @@ -0,0 +1,128 @@ +local test_env = require("spec.util.test_env") +local testing_paths = test_env.testing_paths + +local fun = require("luarocks.fun") + +describe("luarocks.fun #unit", function() + local runner + + lazy_setup(function() + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + end) + + lazy_teardown(function() + runner.save_stats() + end) + + describe("fun.concat", function() + it("returns the concatenation of the two tables given as arguments", function() + local t1, t2 + + t1 = {1, 2, 3} + t2 = {4, 5, 6} + assert.same(fun.concat(t1, t2), {1, 2, 3, 4, 5, 6}) + assert.same(fun.concat(t2, t1), {4, 5, 6, 1, 2, 3}) + t1 = {1, 2, 3} + t2 = {} + assert.same(fun.concat(t1, t2), {1, 2, 3}) + assert.same(fun.concat(t2, t1), {1, 2, 3}) + t1 = {} + t2 = {} + assert.same(fun.concat(t1, t2), {}) + end) + end) + + describe("fun.contains", function() + it("checks whether a table contains a given value", function() + local t + + t = {1, 2, 3} + assert.truthy(fun.contains(t, 1)) + assert.falsy(fun.contains(t, 4)) + t = {} + assert.falsy(fun.contains(t, 1)) + end) + end) + + local addOne = function(x) return x + 1 end + + describe("fun.map", function() + it("applies a function to each element in the given table and returns the results in a new table", function() + local t + + t = {1, 2, 3} + assert.same(fun.map(t, addOne), {2, 3, 4}) + t = {} + assert.same(fun.map(t, addOne), {}) + end) + end) + + describe("fun.traverse", function() + it("recursively applies a function to each element in a given table and returns the results in a new table", function() + local t + + t = {1, 2, {3, 4, {5, 6}}} + assert.same(fun.traverse(t, addOne), {2, 3, {4, 5, {6, 7}}}) + t = {1, 2, {}, {1, {}, 2}} + assert.same(fun.traverse(t, addOne), {2, 3, {}, {2, {}, 3}}) + end) + end) + + describe("fun.filter", function() + it("filters the elements in the given table and returns the result in a new table", function() + local t + + t = {1, 2, "a", "b", 3} + assert.same(fun.filter(t, function(x) return type(x) == "number" end), {1, 2, 3}) + t = {2, 4, 7, 8} + assert.same(fun.filter(t, function(x) return x % 2 == 0 end), {2, 4, 8}) + end) + end) + + describe("fun.reverse_in", function() + it("reverses the elements in the given table and returns the result in a new table", function() + local t + + t = {1, 2, 3, 4} + assert.same(fun.reverse_in(t), {4, 3, 2, 1}) + t = {"a", "b", "c"} + assert.same(fun.reverse_in(t), {"c", "b", "a"}) + end) + end) + + describe("fun.sort_in", function() + it("sorts the elements in the given table and returns the result in a new table", function() + local t + + t = {4, 2, 3, 1} + assert.same(fun.sort_in(t), {1, 2, 3, 4}) + t = {"d", "a", "c", "b"} + assert.same(fun.sort_in(t), {"a", "b", "c", "d"}) + end) + end) + + describe("fun.flip", function() + it("returns a function behaving as the one given in the argument but with the arguments interchanged", function() + local a, b = fun.flip(function(a, b) return a, b end)(5, 6) + assert.same(a, 6) + assert.same(b, 5) + end) + end) + + describe("fun.partial", function() + it("partially applies the given arguments to the given function and returns it", function() + local addOne = fun.partial(function(x, y) return x + y end, 1) + assert.same(addOne(1), 2) + assert.same(addOne(2), 3) + + local addTwo = fun.partial(function(x, y, z) return x + y + z end, 1, 1) + assert.same(addTwo(1), 3) + assert.same(addTwo(2), 4) + + local addThree = fun.partial(function(x, y, z, t, u) return x + y + z + t + u end, 1, 1, 1) + assert.same(addThree(1, 1), 5) + assert.same(addThree(1, 2), 6) + end) + end) +end) diff --git a/spec/unit/loader_spec.lua b/spec/unit/loader_spec.lua new file mode 100644 index 0000000..cde3059 --- /dev/null +++ b/spec/unit/loader_spec.lua @@ -0,0 +1,18 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run + +test_env.setup_specs() + +describe("luarocks.loader", function() + describe("#unit", function() + it("starts", function() + assert(run.lua_bool([[-e "require 'luarocks.loader'; print(package.loaded['luarocks.loaded'])"]])) + end) + + describe("which", function() + it("finds modules using package.path", function() + assert(run.lua_bool([[-e "loader = require 'luarocks.loader'; local x,y,z,p = loader.which('luarocks.loader', 'p'); assert(p == 'p')"]])) + end) + end) + end) +end) diff --git a/spec/unit/persist_spec.lua b/spec/unit/persist_spec.lua new file mode 100644 index 0000000..ed78345 --- /dev/null +++ b/spec/unit/persist_spec.lua @@ -0,0 +1,74 @@ +local test_env = require("spec.util.test_env") +local testing_paths = test_env.testing_paths + +local persist = require("luarocks.persist") + +describe("luarocks.persist #unit", function() + local runner + + lazy_setup(function() + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + end) + + lazy_teardown(function() + runner.save_stats() + end) + + describe("persist.save_from_table_to_string", function() + it("simple table", function() + assert.are.same([[ +bar = 1234 +foo = "string" +]], persist.save_from_table_to_string({foo = "string", bar = 1234})) + end) + + it("nested tables", function() + assert.are.same([[ +bar = { + baz = "string" +} +foo = { + 1, 2, 3, 4 +} +]], persist.save_from_table_to_string({foo = {1, 2, 3, 4}, bar = {baz = "string"}})) + end) + + it("table with a keyword key (#947)", function() + assert.are.same([[ +bar = { + ["function"] = "foo" +} +]], persist.save_from_table_to_string({bar = {["function"] = "foo"}})) + end) + + it("strings with quotes", function() + assert.are.same([[ +bar = "a \\backslash?" +foo = "a \"quote\"?" +]], persist.save_from_table_to_string({foo = 'a "quote"?', bar = 'a \\backslash?'})) + end) + + it("multiline strings", function() + assert.are.same([===[ +bar = [==[ +]] +]=]]==] +foo = [[ +First line +Second line]] +]===], persist.save_from_table_to_string({foo = "First line\nSecond line", bar = "]]\n]=]"})) + end) + + it("multiline strings ending with brackets", function() + assert.are.same([===[ +bar = [==[ +]] +]=]==] +foo = [=[ +First line +Second line [1]]=] +]===], persist.save_from_table_to_string({foo = "First line\nSecond line [1]", bar = "]]\n]="})) + end) + end) +end) diff --git a/spec/unit/rockspecs_spec.lua b/spec/unit/rockspecs_spec.lua new file mode 100644 index 0000000..7eb033c --- /dev/null +++ b/spec/unit/rockspecs_spec.lua @@ -0,0 +1,126 @@ + +local rockspecs = require("luarocks.rockspecs") +local cfg = require("luarocks.core.cfg") +local test_env = require("spec.util.test_env") +local lfs = require("lfs") + +describe("luarocks.rockspecs #unit", function() + + lazy_setup(function() + cfg.init() + end) + + it("auto adds a build dependency for non-vendored build types", function() + local filename = "test-1.0-1.rockspec" + local rockspec = { + package = "test", + source = { + url = "", + }, + build = { + type = "foo" + }, + } + local globals = {} + local quick = true + + local out = rockspecs.from_persisted_table(filename, rockspec, globals, quick) + + assert(rockspec == out) + assert.same(rockspec.build_dependencies, { + { name = "luarocks-build-foo", constraints = {} }, + }) + end) + + it("does not add a build dependency for non-vendored build type if it's already ther", function() + local filename = "test-1.0-1.rockspec" + local rockspec = { + package = "test", + source = { + url = "", + }, + build_dependencies = { + "luarocks-build-cpp >= 1.0", + }, + build = { + type = "cpp" + }, + } + local globals = {} + local quick = true + + local out = rockspecs.from_persisted_table(filename, rockspec, globals, quick) + + assert(rockspec == out) + + assert.same(rockspec.build_dependencies, { + { name = "luarocks-build-cpp", constraints = { { op = ">=", version = { string = "1.0", 1, 0 } } } }, + }) + end) + + it("does not add a build dependency for 'none' build type", function() + local filename = "test-1.0-1.rockspec" + local rockspec = { + package = "test", + source = { + url = "", + }, + build = { + type = "none" + }, + } + local globals = {} + local quick = true + + local out = rockspecs.from_persisted_table(filename, rockspec, globals, quick) + + assert(rockspec == out) + assert.same(rockspec.build_dependencies, {}) + end) + + it("does not add a build dependency for 'module' build type", function() + local filename = "test-1.0-1.rockspec" + local rockspec = { + package = "test", + source = { + url = "", + }, + build = { + type = "none" + }, + } + local globals = {} + local quick = true + + local out = rockspecs.from_persisted_table(filename, rockspec, globals, quick) + + assert(rockspec == out) + assert.same(rockspec.build_dependencies, {}) + end) + + for d in lfs.dir(test_env.testing_paths.src_dir .. "/luarocks/build") do + local name = d:match("(.*)%.lua") + if name then + it("does not add a build dependency for vendored '" .. name .. "' type", function() + local filename = "test-1.0-1.rockspec" + local rockspec = { + package = "test", + source = { + url = "", + }, + build = { + type = name + }, + } + local globals = {} + local quick = true + + local out = rockspecs.from_persisted_table(filename, rockspec, globals, quick) + + assert(rockspec == out) + assert.same(rockspec.build_dependencies, {}) + end) + end + end + +end) diff --git a/spec/unit/sysdetect_spec.lua b/spec/unit/sysdetect_spec.lua new file mode 100644 index 0000000..d3b1695 --- /dev/null +++ b/spec/unit/sysdetect_spec.lua @@ -0,0 +1,79 @@ + +local sysdetect = require("luarocks.core.sysdetect") +local lfs = require("lfs") + +describe("luarocks.core.sysdetect #unix #unit", function() + + lazy_setup(function() + os.execute([=[ + [ -e binary-samples ] || { + git clone --depth=1 https://github.com/hishamhm/binary-samples + ( cd binary-samples && git pull ) + } + ]=]) + end) + + local files = { + ["."] = "ignore", + [".."] = "ignore", + ["README.md"] = "ignore", + [".git"] = "ignore", + ["MIT_LICENSE"] = "ignore", + ["anti-disassembler"] = "ignore", + ["elf-Linux-lib-x64.so"] = "ignore", + ["elf-Linux-lib-x86.so"] = "ignore", + + ["elf-Linux-x64-bash"] = {"linux", "x86_64"}, + ["elf-Linux-ia64-bash"] = {"linux", "ia_64"}, + ["MachO-OSX-ppc-and-i386-bash"] = {"macosx", "x86"}, + ["MachO-OSX-ppc-openssl-1.0.1h"] = {"macosx", "ppc"}, + ["MachO-iOS-armv7-armv7s-arm64-Helloworld"] = {"macosx", "arm"}, + ["pe-Windows-x64-cmd"] = {"windows", "x86_64"}, + ["MachO-iOS-armv7s-Helloworld"] = {"macosx", "arm"}, + ["elf-Linux-SparcV8-bash"] = {"linux", "sparcv8"}, + ["elf-HPUX-ia64-bash"] = {"hpux", "ia_64"}, + ["MachO-OSX-x64-ls"] = {"macosx", "x86_64"}, + ["pe-Windows-ARMv7-Thumb2LE-HelloWorld"] = {"windows", "armv7l"}, + ["elf-ARMv6-static-gofmt"] = {"sysv", "arm"}, + ["elf-Linux-s390-bash"] = {"linux", "s390"}, + ["elf-Linux-Alpha-bash"] = {"linux", "alpha"}, + ["elf-Linux-hppa-bash"] = {"linux", "hppa"}, + ["elf-Linux-x86_64-static-sln"] = {"linux", "x86_64"}, + ["elf-Linux-Mips4-bash"] = {"linux", "mips"}, + ["elf-ARMv6-dynamic-go"] = {"linux", "arm"}, + ["elf-Linux-SuperH4-bash"] = {"linux", "superh"}, + ["elf-Linux-x86-bash"] = {"linux", "x86"}, + ["elf-Linux-PowerPC-bash"] = {"linux", "ppc"}, + ["libSystem.B.dylib"] = {"macosx", "x86_64"}, + ["MachO-iOS-arm1176JZFS-bash"] = {"macosx", "arm"}, + ["pe-Windows-x86-cmd"] = {"windows", "x86"}, + ["elf-Linux-ARMv7-ls"] = {"linux", "arm"}, + ["elf-Linux-ARM64-bash"] = {"linux", "aarch64"}, + ["MachO-OSX-x86-ls"] = {"macosx", "x86"}, + ["elf-solaris-sparc-ls"] = {"solaris", "sparc"}, + ["elf-solaris-x86-ls"] = {"solaris", "x86"}, + ["pe-mingw32-strip.exe"] = {"windows", "x86"}, + ["elf-OpenBSD-x86_64-sh"] = {"openbsd", "x86_64"}, + ["elf-NetBSD-x86_64-echo"] = {"netbsd", "x86_64"}, + ["elf-FreeBSD-x86_64-echo"] = {"freebsd", "x86_64"}, + ["elf-Haiku-GCC2-ls"] = {"haiku", "x86"}, + ["elf-Haiku-GCC7-WebPositive"] = {"haiku", "x86"}, + ["pe-cygwin-ls.exe"] = {"cygwin", "x86"}, + ["elf-DragonFly-x86_64-less"] = {"dragonfly", "x86_64"}, + + } + + describe("detect_file", function() + it("detects system and processor", function() + for f in lfs.dir("binary-samples") do + if files[f] ~= "ignore" then + assert.table(files[f], "unknown binary " .. f) + local expected_s, expected_p = files[f][1], files[f][2] + local s, p = sysdetect.detect_file("binary-samples/" .. f) + assert.same(expected_s, s, "bad system for " .. f) + assert.same(expected_p, p, "bad processor for " .. f) + end + end + end) + end) +end) diff --git a/spec/unit/test_spec.lua b/spec/unit/test_spec.lua new file mode 100644 index 0000000..4a31777 --- /dev/null +++ b/spec/unit/test_spec.lua @@ -0,0 +1,173 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local get_tmp_path = test_env.get_tmp_path +local testing_paths = test_env.testing_paths +local write_file = test_env.write_file + +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local path = require("luarocks.path") +local test = require("luarocks.test") +local test_busted = require("luarocks.test.busted") +local test_command = require("luarocks.test.command") + +describe("LuaRocks test #unit", function() + local runner + + lazy_setup(function() + cfg.init() + fs.init() + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + end) + + lazy_teardown(function() + runner.save_stats() + end) + + local tmpdir + local olddir + + local create_tmp_dir = function() + tmpdir = get_tmp_path() + olddir = lfs.currentdir() + lfs.mkdir(tmpdir) + lfs.chdir(tmpdir) + fs.change_dir(tmpdir) + end + + local destroy_tmp_dir = function() + if olddir then + lfs.chdir(olddir) + if tmpdir then + lfs.rmdir(tmpdir) + end + end + end + + describe("test.command", function() + describe("command.detect_type", function() + before_each(function() + create_tmp_dir() + end) + + after_each(function() + destroy_tmp_dir() + end) + + it("returns true if test.lua exists", function() + write_file("test.lua", "", finally) + assert.truthy(test_command.detect_type()) + end) + + it("returns false if test.lua doesn't exist", function() + assert.falsy(test_command.detect_type()) + end) + end) + + describe("command.run_tests", function() + before_each(function() + create_tmp_dir() + end) + + after_each(function() + destroy_tmp_dir() + end) + + it("returns the result of the executed tests", function() + write_file("test.lua", "assert(1==1)", finally) + assert.truthy(test_command.run_tests(nil, {})) + + write_file("test.lua", "assert(1==2)", finally) + assert.falsy(test_command.run_tests(nil, {})) + end) + + it("returns the result of the executed tests with custom arguments and test command", function() + write_file("test.lua", "assert(1==1)", finally) + + local test = { + script = "test.lua", + flags = { + arg1 = "1", + arg2 = "2" + }, + command = fs.Q(testing_paths.lua) + } + assert.truthy(test_command.run_tests(test, {})) + end) + + it("returns false and does nothing if the test script doesn't exist", function() + assert.falsy(test_command.run_tests(nil, {})) + end) + end) + end) + + describe("test.busted", function() + describe("busted.detect_type", function() + before_each(function() + create_tmp_dir() + end) + + after_each(function() + destroy_tmp_dir() + end) + + it("returns true if .busted exists", function() + write_file(".busted", "", finally) + assert.truthy(test_busted.detect_type()) + end) + + it("returns false if .busted doesn't exist", function() + assert.falsy(test_busted.detect_type()) + end) + end) + + describe("busted.run_tests", function() + before_each(function() + path.use_tree(testing_paths.testing_sys_tree) + create_tmp_dir() + end) + + after_each(function() + destroy_tmp_dir() + end) + + pending("returns the result of the executed tests", function() + -- FIXME: busted issue + write_file("test_spec.lua", "assert(1==1)", finally) + assert.truthy(test_busted.run_tests(nil, {})) + + write_file("test_spec.lua", "assert(1==2)", finally) + assert.falsy(test_busted.run_tests()) + end) + end) + end) + + describe("test", function() + describe("test.run_test_suite", function() + before_each(function() + create_tmp_dir() + end) + + after_each(function() + destroy_tmp_dir() + end) + + it("returns false if the given rockspec cannot be loaded", function() + assert.falsy(test.run_test_suite("invalid", nil, {})) + end) + + it("returns false if no test type was detected", function() + assert.falsy(test.run_test_suite({ package = "test" }, nil, {})) + end) + + it("returns the result of executing the tests specified in the given rockspec", function() + write_file("test.lua", "assert(1==1)", finally) + assert.truthy(test.run_test_suite({ test_dependencies = {} }, nil, {})) + + write_file("test.lua", "assert(1==2)", finally) + assert.falsy(test.run_test_suite({ test_dependencies = {} }, nil, {})) + end) + end) + end) +end) diff --git a/spec/unit/tools_spec.lua b/spec/unit/tools_spec.lua new file mode 100644 index 0000000..5b85c86 --- /dev/null +++ b/spec/unit/tools_spec.lua @@ -0,0 +1,251 @@ +local test_env = require("spec.util.test_env") +local get_tmp_path = test_env.get_tmp_path +local testing_paths = test_env.testing_paths +local write_file = test_env.write_file + +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local patch = require("luarocks.tools.patch") + +local lao = +[[The Nameless is the origin of Heaven and Earth; +The named is the mother of all things. + +Therefore let there always be non-being, + so we may see their subtlety, +And let there always be being, + so we may see their outcome. +The two are the same, +But after they are produced, + they have different names. +They both may be called deep and profound. +Deeper and more profound, +The door of all subtleties!]] + +local tzu = +[[The Way that can be told of is not the eternal Way; +The name that can be named is not the eternal name. +The Nameless is the origin of Heaven and Earth; +The Named is the mother of all things. +Therefore let there always be non-being, + so we may see their subtlety, +And let there always be being, + so we may see their outcome. +The two are the same, +But after they are produced, + they have different names.]] + +local valid_patch1 = +[[--- lao 2002-02-21 23:30:39.942229878 -0800 ++++ tzu 2002-02-21 23:30:50.442260588 -0800 +@@ -1,7 +1,6 @@ +-The Way that can be told of is not the eternal Way; +-The name that can be named is not the eternal name. + The Nameless is the origin of Heaven and Earth; +-The Named is the mother of all things. ++The named is the mother of all things. ++ + Therefore let there always be non-being, + so we may see their subtlety, + And let there always be being, +@@ -9,3 +8,6 @@ + The two are the same, + But after they are produced, + they have different names. ++They both may be called deep and profound. ++Deeper and more profound, ++The door of all subtleties!]] + +local valid_patch2 = +[[--- /dev/null 1969-02-21 23:30:39.942229878 -0800 ++++ tzu 2002-02-21 23:30:50.442260588 -0800 +@@ -1,7 +1,6 @@ +-The Way that can be told of is not the eternal Way; +-The name that can be named is not the eternal name. + The Nameless is the origin of Heaven and Earth; +-The Named is the mother of all things. ++The named is the mother of all things. ++ + Therefore let there always be non-being, + so we may see their subtlety, + And let there always be being, +@@ -9,3 +8,6 @@ + The two are the same, + But after they are produced, + they have different names. ++They both may be called deep and profound. ++Deeper and more profound, ++The door of all subtleties!]] + +local invalid_patch1 = +[[--- lao 2002-02-21 23:30:39.942229878 -0800 ++++ tzu 2002-02-21 23:30:50.442260588 -0800 +@@ -1,7 +1,6 @@ +-The Way that can be told of is not the eternal Way; +-The name that can be named is not the eternal name. + The Nameless is the origin of Heaven and Earth; +-The Named is the mother of all things. +--- Extra ++The named is the mother of all things. ++ + Therefore let there always be non-being, + so we may see their subtlety, + And let there always be being, +--- Extra +@@ -9,3 +8,7 @@ + The two are the same, + But after they are produced, + they have different names. ++They both may be called deep and profound. ++Deeper and more profound, ++The door of all subtleties!]] + +local invalid_patch2 = +[[--- lao 2002-02-21 23:30:39.942229878 -0800 ++++ tzu 2002-02-21 23:30:50.442260588 -0800 +@@ -1,7 +1,6 @@ +-The Way that can be told of is not the eternal Way; +-The name that can be named is not the eternal name. + The Nameless is the origin of Heaven and Earth; +-The Named is the mother of all things. ++The named is the mother of all things. ++ + Therefore let there always be non-being, + so we may see their subtlety, + And let there always be being, +@@ -9,3 +8,6 @@ + The two are the same, + But after they are produced, + they have different names. ++They both may be called deep and profound. ++Deeper and more profound, +? ... ++The door of all subtleties!]] + +local invalid_patch3 = +[[--- lao 2002-02-21 23:30:39.942229878 -0800 ++++ tzu 2002-02-21 23:30:50.442260588 -0800 +@@ -1,7 +1,6 @@ +-The Way that can be told of is not the eternal Way; +-The name that can be named is not the eternal name. + The Nameless is the origin of Heaven and Earth; +-The Named is the mother of all things. ++The named is the mother of all things. ++ + Therefore let there always be non-being, + so we may see their subtlety, + And let there always be being, +@@ -9,3 +8,6 @@ + The two are the same, + But after they are produced, + they have different names. ++They both may be called deep and profound. ++Deeper and more profound, +? ... ++The door of all subtleties!]] + +describe("Luarocks patch test #unit", function() + local runner + + lazy_setup(function() + cfg.init() + fs.init() + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + end) + + lazy_teardown(function() + runner.save_stats() + end) + + describe("patch.read_patch", function() + it("returns a table with the patch file info and the result of parsing the file", function() + local t, result + + write_file("test.patch", valid_patch1, finally) + t, result = patch.read_patch("test.patch") + assert.truthy(result) + assert.truthy(t) + + write_file("test.patch", invalid_patch1, finally) + t, result = patch.read_patch("test.patch") + assert.falsy(result) + assert.truthy(t) + + write_file("test.patch", invalid_patch2, finally) + t, result = patch.read_patch("test.patch") + assert.falsy(result) + assert.truthy(t) + + write_file("test.patch", invalid_patch3, finally) + t, result = patch.read_patch("test.patch") + assert.falsy(result) + assert.truthy(t) + end) + end) + + describe("patch.apply_patch", function() + local tmpdir + local olddir + + before_each(function() + tmpdir = get_tmp_path() + olddir = lfs.currentdir() + lfs.mkdir(tmpdir) + lfs.chdir(tmpdir) + + write_file("lao", tzu, finally) + write_file("tzu", lao, finally) + end) + + after_each(function() + if olddir then + lfs.chdir(olddir) + if tmpdir then + lfs.rmdir(tmpdir) + end + end + end) + + it("applies the given patch and returns the result of patching", function() + write_file("test.patch", valid_patch1, finally) + local p = patch.read_patch("test.patch") + local result = patch.apply_patch(p) + assert.truthy(result) + end) + + it("applies the given patch with custom arguments and returns the result of patching", function() + write_file("test.patch", valid_patch2, finally) + local p = patch.read_patch("test.patch") + local result = patch.apply_patch(p, nil, true) + assert.truthy(result) + end) + + it("fails if the patch file is invalid", function() + write_file("test.patch", invalid_patch1, finally) + local p = patch.read_patch("test.patch") + local result = pcall(patch.apply_patch, p) + assert.falsy(result) + end) + + it("returns false if the files from the patch doesn't exist", function() + os.remove("lao") + os.remove("tzu") + + write_file("test.patch", valid_patch1, finally) + local p = patch.read_patch("test.patch") + local result = patch.apply_patch(p) + assert.falsy(result) + end) + + it("returns false if the target file was already patched", function() + write_file("test.patch", valid_patch1, finally) + local p = patch.read_patch("test.patch") + local result = patch.apply_patch(p) + assert.truthy(result) + + result = patch.apply_patch(p) + assert.falsy(result) + end) + end) +end) diff --git a/spec/unit/util_spec.lua b/spec/unit/util_spec.lua new file mode 100644 index 0000000..8b234b2 --- /dev/null +++ b/spec/unit/util_spec.lua @@ -0,0 +1,160 @@ +local test_env = require("spec.util.test_env") +local testing_paths = test_env.testing_paths +local P = test_env.P + +local util = require("luarocks.util") +local core_util = require("luarocks.core.util") + +describe("luarocks.util #unit", function() + local runner + + lazy_setup(function() + runner = require("luacov.runner") + runner.init(testing_paths.testrun_dir .. "/luacov.config") + end) + + lazy_teardown(function() + runner.save_stats() + end) + + describe("util.variable_substitutions", function() + it("replaces variables", function() + local t = { + ["hello"] = "$(KIND) world", + } + util.variable_substitutions(t, { + ["KIND"] = "happy", + }) + assert.are.same({ + ["hello"] = "happy world", + }, t) + end) + + it("missing variables are empty", function() + local t = { + ["hello"] = "$(KIND) world", + } + util.variable_substitutions(t, { + }) + assert.are.same({ + ["hello"] = " world", + }, t) + end) + end) + + describe("util.sortedpairs", function() + local function collect(iter, state, var) + local collected = {} + + while true do + local returns = {iter(state, var)} + + if returns[1] == nil then + return collected + else + table.insert(collected, returns) + var = returns[1] + end + end + end + + it("default sort", function() + assert.are.same({}, collect(util.sortedpairs({}))) + assert.are.same({ + {1, "v1"}, + {2, "v2"}, + {3, "v3"}, + {"bar", "v5"}, + {"foo", "v4"} + }, collect(util.sortedpairs({"v1", "v2", "v3", foo = "v4", bar = "v5"}))) + end) + + it("sort by function", function() + local function compare(a, b) return a > b end + assert.are.same({}, collect(util.sortedpairs({}, compare))) + assert.are.same({ + {3, "v3"}, + {2, "v2"}, + {1, "v1"} + }, collect(util.sortedpairs({"v1", "v2", "v3"}, compare))) + end) + + it("sort by priority table", function() + assert.are.same({}, collect(util.sortedpairs({}, {"k1", "k2"}))) + assert.are.same({ + {"k3", "v3"}, + {"k2", "v2", {"sub order"}}, + {"k1", "v1"}, + {"k4", "v4"}, + {"k5", "v5"}, + }, collect(util.sortedpairs({ + k1 = "v1", k2 = "v2", k3 = "v3", k4 = "v4", k5 = "v5" + }, {"k3", {"k2", {"sub order"}}, "k1"}))) + end) + end) + + describe("core.util.show_table", function() + it("returns a pretty-printed string containing the representation of the given table", function() + local result + + local t1 = {1, 2, 3} + result = core_util.show_table(t1) + assert.truthy(result:find("[1] = 1", 1, true)) + assert.truthy(result:find("[2] = 2", 1, true)) + assert.truthy(result:find("[3] = 3", 1, true)) + + local t2 = {a = 1, b = 2, c = 3} + result = core_util.show_table(t2) + assert.truthy(result:find("[\"a\"] = 1", 1, true)) + assert.truthy(result:find("[\"b\"] = 2", 1, true)) + assert.truthy(result:find("[\"c\"] = 3", 1, true)) + + local t3 = {a = 1, b = "2", c = {3}} + result = core_util.show_table(t3) + assert.truthy(result:find("[\"a\"] = 1", 1, true)) + assert.truthy(result:find("[\"b\"] = \"2\"", 1, true)) + assert.truthy(result:find("[\"c\"] = {", 1, true)) + assert.truthy(result:find("[1] = 3", 1, true)) + + local t4 = {a = 1, b = {c = 2, d = {e = "4"}}} + result = core_util.show_table(t4) + assert.truthy(result:find("[\"a\"] = 1", 1, true)) + assert.truthy(result:find("[\"b\"] = {", 1, true)) + assert.truthy(result:find("[\"c\"] = 2", 1, true)) + assert.truthy(result:find("[\"d\"] = {", 1, true)) + assert.truthy(result:find("[\"e\"] = \"4\"", 1, true)) + end) + end) + + describe("core.util.cleanup_path", function() + it("does not change order of existing items of prepended path", function() + local sys_path = P'/usr/local/bin;/usr/bin' + local lr_path = P'/home/user/.luarocks/bin;/usr/bin' + local path = lr_path .. ';' .. sys_path + + local result = core_util.cleanup_path(path, ';', '5.3', false) + assert.are.equal(P'/home/user/.luarocks/bin;/usr/local/bin;/usr/bin', result) + end) + + it("does not change order of existing items of appended path", function() + local sys_path = P'/usr/local/bin;/usr/bin' + local lr_path = P'/home/user/.luarocks/bin;/usr/bin' + local path = sys_path .. ';' .. lr_path + + local result = core_util.cleanup_path(path, ';', '5.3', true) + assert.are.equal(P'/usr/local/bin;/usr/bin;/home/user/.luarocks/bin', result) + end) + + it("rewrites versions that do not match the provided version", function() + local expected = P'a/b/lua/5.3/?.lua;a/b/c/lua/5.3/?.lua' + local result = core_util.cleanup_path(P'a/b/lua/5.2/?.lua;a/b/c/lua/5.3/?.lua', ';', '5.3') + assert.are.equal(expected, result) + end) + + it("does not rewrite versions for which the provided version is a substring", function() + local expected = P'a/b/lua/5.3/?.lua;a/b/c/lua/5.3.4/?.lua' + local result = core_util.cleanup_path(P'a/b/lua/5.2/?.lua;a/b/c/lua/5.3.4/?.lua', ';', '5.3') + assert.are.equal(expected, result) + end) + end) +end) diff --git a/spec/unpack_spec.lua b/spec/unpack_spec.lua new file mode 100644 index 0000000..59f7eb0 --- /dev/null +++ b/spec/unpack_spec.lua @@ -0,0 +1,69 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run +local testing_paths = test_env.testing_paths + +local extra_rocks = { + "/cprint-${CPRINT}.src.rock", + "/cprint-${CPRINT}.rockspec", + "/luazip-1.2.4-1.rockspec" +} + +describe("luarocks unpack #integration", function() + + before_each(function() + test_env.setup_specs(extra_rocks) + end) + + describe("basic fail tests", function() + it("with no flags/arguments", function() + assert.is_false(run.luarocks_bool("unpack")) + end) + + it("with invalid rockspec", function() + assert.is_false(run.luarocks_bool("unpack invalid.rockspec")) + end) + + it("with invalid patch", function() + assert.is_false(run.luarocks_bool("unpack " .. testing_paths.fixtures_dir .. "/invalid_patch-0.1-1.rockspec")) + end) + end) + + describe("more complex tests", function() + it("download", function() + assert.is_true(run.luarocks_bool("unpack cprint")) + test_env.remove_dir("cprint-${CPRINT}") + end) + + it("src", function() + assert.is_true(run.luarocks_bool("download --source cprint")) + assert.is_true(run.luarocks_bool("unpack cprint-${CPRINT}.src.rock")) + os.remove("cprint-${CPRINT}.src.rock") + test_env.remove_dir("cprint-${CPRINT}") + end) + + it("src", function() + assert.is_true(run.luarocks_bool("download --rockspec cprint")) + assert.is_true(run.luarocks_bool("unpack cprint-${CPRINT}.rockspec")) + os.remove("cprint-${CPRINT}.rockspec") + os.remove("lua-cprint") + test_env.remove_dir("cprint-${CPRINT}") + end) + + -- #595 luarocks unpack of a git:// rockspec fails to copy the rockspec + it("git:// rockspec", function() + assert.is_true(run.luarocks_bool("download --rockspec luazip")) + assert.is_true(run.luarocks_bool("unpack luazip-1.2.4-1.rockspec")) + assert.is_truthy(lfs.attributes("luazip-1.2.4-1/luazip/luazip-1.2.4-1.rockspec")) + test_env.remove_dir("luazip-1.2.4-1") + end) + + it("binary", function() + assert.is_true(run.luarocks_bool("build cprint")) + assert.is_true(run.luarocks_bool("pack cprint")) + assert.is_true(run.luarocks_bool("unpack cprint-${CPRINT}." .. test_env.platform .. ".rock")) + test_env.remove_dir("cprint-${CPRINT}") + os.remove("cprint-${CPRINT}." .. test_env.platform .. ".rock") + end) + end) +end) diff --git a/spec/upload_spec.lua b/spec/upload_spec.lua new file mode 100644 index 0000000..cc0b606 --- /dev/null +++ b/spec/upload_spec.lua @@ -0,0 +1,63 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run +local testing_paths = test_env.testing_paths + +describe("luarocks upload #integration", function() + + describe("general tests", function() + before_each(function() + test_env.setup_specs(nil) + end) + + it("with no flags/arguments", function() + assert.is_false(run.luarocks_bool("upload")) + end) + + it("invalid rockspec", function() + assert.is_false(run.luarocks_bool("upload invalid.rockspec")) + end) + + it("api key invalid", function() + assert.is_false(run.luarocks_bool("upload --api-key=invalid invalid.rockspec")) + end) + + it("api key invalid and skip-pack", function() + assert.is_false(run.luarocks_bool("upload --api-key=\"invalid\" --skip-pack " .. testing_paths.testing_server .. "/luasocket-${LUASOCKET}.rockspec")) + end) + + it("force #unix", function() + assert.is_false(run.luarocks_bool("upload --api-key=\"invalid\" --force " .. testing_paths.testing_server .. "/luasocket-${LUASOCKET}.rockspec")) + end) + end) + + describe("tests with Xavante server #mock", function() + lazy_setup(function() + test_env.setup_specs(nil, "mock") + test_env.mock_server_init() + end) + + lazy_teardown(test_env.mock_server_done) + + it("rockspec with api-key", function() + assert.is_true(run.luarocks_bool("upload " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec " .. test_env.openssl_dirs .. " --api-key=123", {LUAROCKS_CONFIG = testing_paths.testrun_dir .. "/luarocks_site.lua"})) + end) + + it("#gpg rockspec with --sign", function() + os.remove(testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec.asc") + os.remove(testing_paths.testrun_dir .. "/a_rock-1.0-1.src.rock.asc") + print(run.luarocks("upload " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec " .. test_env.openssl_dirs .. " --api-key=123 --sign", {LUAROCKS_CONFIG = testing_paths.testrun_dir .. "/luarocks_site.lua"})) + end) + + it("with .rockspec and .src.rock", function() + assert.is_true(run.luarocks_bool("upload " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.src.rock " .. test_env.openssl_dirs, {LUAROCKS_CONFIG = testing_paths.testrun_dir .. "/luarocks_site.lua"})) + end) + + it("with arguments .src.rock and .rockspec out of order", function() + assert.is_false(run.luarocks_bool("upload " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.src.rock " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec " .. test_env.openssl_dirs, {LUAROCKS_CONFIG = testing_paths.testrun_dir .. "/luarocks_site.lua"})) + end) + + it("rockspec with api-key and skip-pack", function() + assert.is_true(run.luarocks_bool("upload --skip-pack " .. testing_paths.fixtures_dir .. "/a_rock-1.0-1.rockspec " .. test_env.openssl_dirs .. " --api-key=123", {LUAROCKS_CONFIG = testing_paths.testrun_dir .. "/luarocks_site.lua"})) + end) + end) +end) diff --git a/spec/util/git_repo.lua b/spec/util/git_repo.lua new file mode 100644 index 0000000..b3ddd9e --- /dev/null +++ b/spec/util/git_repo.lua @@ -0,0 +1,108 @@ +local git_repo = {} + +local test_env = require("spec.util.test_env") +local lfs = require("lfs") + +local files = { +---------------------------------------- +["testrock-dev-1.rockspec"] = [[ +package = "testrock" +version = "dev-1" +source = { + url = "git://localhost:20000/testrock" +} +description = { + homepage = "https://localhost", + license = "MIT" +} +dependencies = {} +build = { + type = "builtin", + modules = { + testrock = "testrock.lua" + } +} +]], +---------------------------------------- +["testrock.lua"] = [[ +local testrock = {} + +function testrock.say() + return "Hello, world!" +end + +return testrock +]], +---------------------------------------- +["foo.c"] = [[ +#include +int luaopen_foo(lua_State* L) { + lua_pushnumber(L, 42); + return 1; +} +]], +---------------------------------------- +["test.lua"] = [[ +print("this should be ignored!") +]], +} + +local function write_file(filename, contents) + local fd = assert(io.open(filename, "w")) + assert(fd:write(contents)) + fd:close() +end + +local function handling(args) + local pok, ret = pcall(args.try) + if not pok then + pok, ret = pcall(args.catch, ret) + end + args.finally() + if not pok then + error(ret) + end + return ret +end + +function git_repo.start() + local dir = lfs.currentdir() + return handling { + try = function() + local pidfile = os.tmpname() + local basedir = test_env.testing_paths.testrun_dir .. "/git_repo" + local repodir = basedir .. "/testrock" + test_env.remove_dir(basedir) + lfs.mkdir(basedir) + lfs.mkdir(repodir) + lfs.chdir(repodir) + assert(test_env.execute("git init")) + for name, contents in pairs(files) do + write_file(name, contents) + test_env.execute("git add " .. name) + end + assert(test_env.execute("git commit -a -m 'initial commit'")) + assert(test_env.execute("git branch test-branch")) + print("git daemon --reuseaddr --pid-file="..pidfile.." --base-path="..basedir.." --export-all "..repodir.." &") + assert(test_env.execute("git daemon --reuseaddr --pid-file="..pidfile.." --base-path="..basedir.." --export-all "..repodir.." &")) + assert(test_env.execute("sleep 0.1; netstat -ln | grep '0.0.0.0:9418 .* LISTEN'")) + return { + stop = function() + local fd = io.open(pidfile) + local pid = fd:read("*a") + fd:close() + assert(test_env.execute("kill -HUP " .. pid)) + test_env.remove_dir(basedir) + end + } + end, + catch = function(err) + error(err) + end, + finally = function() + lfs.chdir(dir) + end, + } +end + +return git_repo diff --git a/spec/util/mock-server.lua b/spec/util/mock-server.lua new file mode 100644 index 0000000..1b09cd6 --- /dev/null +++ b/spec/util/mock-server.lua @@ -0,0 +1,98 @@ +#!/usr/bin/env lua + +--- A simple LuaRocks mock-server for testing. +local restserver = require("restserver") +local server = restserver:new():port(8080) + +server:add_resource("api/tool_version", { + { + method = "GET", + path = "/", + produces = "application/json", + handler = function(query) + local json = { version = query.current } + return restserver.response():status(200):entity(json) + end + } +}) + +server:add_resource("api/1/{id:[0-9]+}/status", { + { + method = "GET", + path = "/", + produces = "application/json", + handler = function(query) + local json = { user_id = "123", created_at = "29.1.1993" } + return restserver.response():status(200):entity(json) + end + } +}) + +server:add_resource("/api/1/{id:[0-9]+}/check_rockspec", { + { + method = "GET", + path = "/", + produces = "application/json", + handler = function(query) + local json = {} + return restserver.response():status(200):entity(json) + end + } +}) + +server:add_resource("/api/1/{id:[0-9]+}/upload", { + { + method = "POST", + path = "/", + produces = "application/json", + handler = function(query) + local json = {module = "luasocket", version = {id = "1.0"}, module_url = "http://localhost/luasocket", manifests = "root", is_new = "is_new"} + return restserver.response():status(200):entity(json) + end + } +}) + +server:add_resource("/api/1/{id:[0-9]+}/upload_rock/{id:[0-9]+}", { + { + method = "POST", + path = "/", + produces = "application/json", + handler = function(query) + local json = {"rock","module_url"} + return restserver.response():status(200):entity(json) + end + } +}) + +server:add_resource("/file/{name:[^/]+}", { + { + method = "GET", + path = "/", + produces = "text/plain", + handler = function(query, name) + local basedir = arg[1] or "./spec/fixtures" + local fd = io.open(basedir .. "/" .. name, "rb") + if not fd then + return restserver.response():status(404) + end + local data = fd:read("*a") + fd:close() + return restserver.response():status(200):entity(data) + end + } +}) + +-- SHUTDOWN this mock-server +server:add_resource("/shutdown", { + { + method = "GET", + path = "/", + handler = function(query) + os.exit() + return restserver.response():status(200):entity() + end + } +}) + +-- This loads the restserver.xavante plugin +server:enable("restserver.xavante"):start() diff --git a/spec/util/quick.lua b/spec/util/quick.lua new file mode 100644 index 0000000..a525aa9 --- /dev/null +++ b/spec/util/quick.lua @@ -0,0 +1,472 @@ +local quick = {} + +local dir_sep = package.config:sub(1, 1) + +local cfg, dir, fs, versions +local initialized = false + +local function initialize() + if initialized then + return + end + initialized = true + + cfg = require("luarocks.core.cfg") + dir = require("luarocks.dir") + fs = require("luarocks.fs") + versions = require("spec.util.versions") + cfg.init() + fs.init() +end + +local function native_slash(pathname) + return (pathname:gsub("[/\\]", dir_sep)) +end + +local function parse_cmd(line) + local cmd, arg = line:match("^%s*([A-Z_]+):%s*(.*)%s*$") + return cmd, arg +end + +local function is_blank(line) + return not not line:match("^%s*$") +end + +local function is_hr(line) + return not not line:match("^%-%-%-%-%-") +end + +local function parse(filename) + local fd = assert(io.open(filename, "rb")) + local input = assert(fd:read("*a")) + fd:close() + + initialize() + + local tests = {} + + local cur_line = 0 + local cur_suite = "" + local cur_test + local cur_op + local cur_block + local cur_block_name + local stack = { "start" } + + local function start_test(arg) + cur_test = { + name = cur_suite .. arg, + ops = {}, + } + cur_op = nil + table.insert(tests, cur_test) + table.insert(stack, "test") + end + + local function fail(msg) + io.stderr:write("Error reading " .. filename .. ":" .. cur_line .. ": " .. msg .. "\n") + os.exit(1) + end + + local function bool_arg(cmd, cur, field, arg) + if arg ~= "true" and arg ~= "false" then + fail(cmd .. " argument must be 'true' or 'false'") + end + cur[field] = (arg == "true") + end + + local function block_start_arg(cmd, cur, field) + if not cur or cur.op ~= "RUN" then + fail(cmd .. " must be given in the context of a RUN") + end + if cur[field] then + fail(cmd .. " was already declared") + end + + cur[field] = { + data = {} + } + cur_block = cur[field] + cur_block_name = cmd + table.insert(stack, "block start") + end + + local test_env = require("spec.util.test_env") + local function expand_vars(line) + if not line then + return nil + end + return (line:gsub("%%%b{}", function(var) + var = var:sub(3, -2) + local fn, fnarg = var:match("^%s*([a-z_]+)%s*%(%s*([^)]+)%s*%)%s*$") + + local value + if var == "tmpdir" then + value = "%{tmpdir}" + elseif var == "url(%{tmpdir})" then + value = "%{url(%{tmpdir})}" + elseif fn == "url" then + value = expand_vars(fnarg) + value = value:gsub("\\", "/") + elseif fn == "path" then + value = expand_vars(fnarg) + value = value:gsub("[/\\]", dir_sep) + elseif fn == "version" then + value = versions[fnarg:lower()] or "" + elseif fn == "version_" then + value = (versions[fnarg:lower()] or ""):gsub("[%.%-]", "_") + else + value = test_env.testing_paths[var] + or test_env.env_variables[var] + or test_env[var] + or "" + end + + return value + end)) + end + + if input:sub(#input, #input) ~= "\n" then + input = input .. "\n" + end + + for line in input:gmatch("([^\r\n]*)\r?\n?") do + cur_line = cur_line + 1 + + local state = stack[#stack] + if state == "start" then + local cmd, arg = parse_cmd(line) + if cmd == "TEST" then + start_test(arg) + elseif cmd == "SUITE" then + cur_suite = arg .. ": " + elseif cmd then + fail("expected TEST, got " .. cmd) + elseif is_blank(line) then + -- skip blank lines and arbitrary text, + -- which is interpreted as a comment + end + elseif state == "test" then + local cmd, arg = parse_cmd(line) + arg = expand_vars(arg) + if cmd == "FILE" then + cur_op = { + op = "FILE", + name = arg, + data = {}, + } + table.insert(cur_test.ops, cur_op) + cur_block = cur_op + cur_block_name = "FILE" + table.insert(stack, "block start") + elseif cmd == "FILE_CONTENTS" then + cur_op = { + op = "FILE_CONTENTS", + name = arg, + data = {}, + } + table.insert(cur_test.ops, cur_op) + cur_block = cur_op + cur_block_name = "FILE_CONTENTS" + table.insert(stack, "block start") + elseif cmd == "RUN" then + local program, args = arg:match("([^ ]+)%s*(.*)$") + if not program then + fail("expected a program argument in RUN") + end + + cur_op = { + op = "RUN", + exit = 0, + exit_line = cur_line, + line = cur_line, + program = program, + args = args, + } + table.insert(cur_test.ops, cur_op) + elseif cmd == "EXISTS" then + cur_op = { + op = "EXISTS", + name = dir.normalize(arg), + line = cur_line, + } + table.insert(cur_test.ops, cur_op) + elseif cmd == "NOT_EXISTS" then + cur_op = { + op = "NOT_EXISTS", + name = dir.normalize(arg), + line = cur_line, + } + table.insert(cur_test.ops, cur_op) + elseif cmd == "MKDIR" then + cur_op = { + op = "MKDIR", + name = dir.normalize(arg), + line = cur_line, + } + table.insert(cur_test.ops, cur_op) + elseif cmd == "RMDIR" then + cur_op = { + op = "RMDIR", + name = dir.normalize(arg), + line = cur_line, + } + table.insert(cur_test.ops, cur_op) + elseif cmd == "RM" then + cur_op = { + op = "RM", + name = dir.normalize(arg), + line = cur_line, + } + table.insert(cur_test.ops, cur_op) + elseif cmd == "EXIT" then + if not cur_op or cur_op.op ~= "RUN" then + fail("EXIT must be given in the context of a RUN") + end + + local code = tonumber(arg) + if not code and not (code >= 0 and code <= 128) then + fail("EXIT code must be a number in the range 0-128, got " .. arg) + end + + cur_op.exit = code + cur_op.exit_line = cur_line + elseif cmd == "VERBOSE" then + if not cur_op or cur_op.op ~= "RUN" then + fail("VERBOSE must be given in the context of a RUN") + end + + bool_arg("VERBOSE", cur_op, "verbose", arg) + elseif cmd == "STDERR" then + block_start_arg("STDERR", cur_op, "stderr") + elseif cmd == "NOT_STDERR" then + block_start_arg("NOT_STDERR", cur_op, "not_stderr") + elseif cmd == "STDOUT" then + block_start_arg("STDOUT", cur_op, "stdout") + elseif cmd == "NOT_STDOUT" then + block_start_arg("NOT_STDOUT", cur_op, "not_stdout") + elseif cmd == "PENDING" then + bool_arg("PENDING", cur_test, "pending", arg) + elseif cmd == "TEST" then + table.remove(stack) + start_test(arg) + elseif cmd then + fail("expected a command, got " .. cmd) + else + -- skip blank lines and arbitrary text, + -- which is interpreted as a comment + end + elseif state == "block start" then + local cmd, arg = parse_cmd(line) + if is_blank(line) then + -- skip + elseif is_hr(line) then + stack[#stack] = "block data" + cur_block.start = cur_line + elseif cmd == "PLAIN" then + bool_arg("PLAIN", cur_block, "plain", arg) + else + fail("expected '-----' to start " .. cur_block_name .. " block") + end + elseif state == "block data" then + if is_hr(line) then + cur_block = nil + table.remove(stack) + else + if not cur_block.plain then + line = expand_vars(line) + end + table.insert(cur_block.data, line) + end + end + end + + return tests +end + +local function check_output(write, block, block_name, data_var) + if block then + local is_positive = not block_name:match("NOT") + local err_msg = is_positive and "did not match" or "did match unwanted output" + + write([=[ do ]=]) + write([=[ local block_at = 1 ]=]) + write([=[ local s, e, line, ok ]=]) + for i, line in ipairs(block.data) do + write(([=[ line = %q ]=]):format(line)) + write(([=[ s, e = string.find(%s, line, block_at, true) ]=]):format(data_var)) + write(is_positive and ([=[ ok = s; if e then block_at = e + 1 end ]=]):format(i) + or ([=[ ok = not s ]=])) + write(([=[ assert(ok, error_message(%d, "%s %s: " .. line, %s)) ]=]):format(block.start + i, block_name, err_msg, data_var)) + end + write([=[ end ]=]) + end +end + +function quick.compile(filename, env) + local tests = parse(filename) + +-- local dev_null = (package.config:sub(1, 1) == "/") +-- and "/dev/null" +-- or "NUL" + + local cmd_helpers = { + ["luarocks"] = "luarocks_cmd", + ["luarocks-admin"] = "luarocks_admin_cmd", + } + + for tn, t in ipairs(tests) do + local code = {} + local function write(...) + table.insert(code, table.concat({...})) + end + + write(([=[ ]=])) + write(([=[ -- **************************************** ]=])) + write(([=[ -- %s ]=]):format(t.name)) + write(([=[ -- **************************************** ]=])) + write(([=[ ]=])) + + write([=[ local test_env = require("spec.util.test_env") ]=]) + write([=[ local lfs = require("lfs") ]=]) + write([=[ local fs = require("lfs") ]=]) + write([=[ local dir_sep = package.config:sub(1, 1) ]=]) + write([=[ local coverage = " -e \"require('luacov.runner')([[" .. test_env.testing_paths.testrun_dir .. dir_sep .. "luacov.config]])\" " ]=]) + write([=[ local luarocks_cmd = test_env.execute_helper(test_env.Q(test_env.testing_paths.lua) .. coverage .. " " .. test_env.testing_paths.src_dir .. "/bin/luarocks", false, test_env.env_variables):sub(1, -5) ]=]) + write([=[ local luarocks_admin_cmd = test_env.execute_helper(test_env.Q(test_env.testing_paths.lua) .. coverage .. " " .. test_env.testing_paths.src_dir .. "/bin/luarocks-admin", false, test_env.env_variables):sub(1, -5) ]=]) + + write([=[ local function make_dir(dirname) ]=]) + write([=[ local bits = {} ]=]) + write([=[ if dirname:sub(1, 1) == dir_sep then bits[1] = "" end ]=]) + write([=[ local ok, err ]=]) + write([=[ for p in dirname:gmatch("[^" .. dir_sep .. "]+") do ]=]) + write([=[ table.insert(bits, p) ]=]) + write([=[ ok, err = lfs.mkdir(table.concat(bits, dir_sep)) ]=]) + write([=[ end ]=]) + write([=[ local exists = (lfs.attributes(dirname) or {}).mode == "directory" ]=]) + write([=[ return exists, (not exists) and err ]=]) + write([=[ end ]=]) + + write(([=[ local function error_message(line, msg, input) ]=])) + write(([=[ local out = {"\n\n", %q, ":", line, ": ", msg} ]=]):format(filename)) + write(([=[ if input then ]=])) + write(([=[ if input:match("\n") then ]=])) + write(([=[ table.insert(out, "\n") ]=])) + write(([=[ table.insert(out, ("-"):rep(40)) ]=])) + write(([=[ table.insert(out, "\n") ]=])) + write(([=[ table.insert(out, input) ]=])) + write(([=[ table.insert(out, ("-"):rep(40)) ]=])) + write(([=[ table.insert(out, "\n") ]=])) + write(([=[ else ]=])) + write(([=[ table.insert(out, ": ") ]=])) + write(([=[ table.insert(out, input) ]=])) + write(([=[ end ]=])) + write(([=[ end ]=])) + write(([=[ return table.concat(out) ]=])) + write(([=[ end ]=])) + + write([=[ return function() ]=]) + write([=[ test_env.run_in_tmp(function(tmpdir) ]=]) + write([=[ local function handle_tmpdir(s) ]=]) + write([=[ return (s:gsub("%%{url%(%%{tmpdir}%)}", (tmpdir:gsub("\\", "/"))) ]=]) + write([=[ :gsub("%%{tmpdir}", (tmpdir:gsub("[\\/]", dir_sep)))) ]=]) + write([=[ end ]=]) + write([=[ local ok, err ]=]) + for _, op in ipairs(t.ops) do + if op.name then + op.name = native_slash(op.name) + write(([=[ local name = handle_tmpdir(%q) ]=]):format(op.name)) + end + if op.op == "FILE" then + if op.name:match("[\\/]") then + write(([=[ make_dir(handle_tmpdir(%q)) ]=]):format(dir.dir_name(op.name))) + end + write([=[ test_env.write_file(name, handle_tmpdir([=====[ ]=]) + for _, line in ipairs(op.data) do + write(line) + end + write([=[ ]=====]), finally) ]=]) + elseif op.op == "EXISTS" then + write(([=[ ok, err = lfs.attributes(name) ]=])) + write(([=[ assert.truthy(ok, error_message(%d, "EXISTS failed: " .. name .. " - " .. (err or "") )) ]=]):format(op.line)) + elseif op.op == "NOT_EXISTS" then + write(([=[ assert.falsy(lfs.attributes(name), error_message(%d, "NOT_EXISTS failed: " .. name .. " exists" )) ]=]):format(op.line)) + elseif op.op == "MKDIR" then + write(([=[ ok, err = make_dir(name) ]=])) + write(([=[ assert.truthy((lfs.attributes(name) or {}).mode == "directory", error_message(%d, "MKDIR failed: " .. name .. " - " .. (err or "") )) ]=]):format(op.line)) + elseif op.op == "RMDIR" then + write(([=[ ok, err = test_env.remove_dir(name) ]=])) + write(([=[ assert.falsy((lfs.attributes(name) or {}).mode == "directory", error_message(%d, "MKDIR failed: " .. name .. " - " .. (err or "") )) ]=]):format(op.line)) + elseif op.op == "RM" then + write(([=[ ok, err = os.remove(name) ]=])) + write(([=[ assert.falsy((lfs.attributes(name) or {}).mode == "file", error_message(%d, "RM failed: " .. name .. " - " .. (err or "") )) ]=]):format(op.line)) + elseif op.op == "FILE_CONTENTS" then + write(([=[ do ]=])) + write(([=[ local fd_file = assert(io.open(name, "rb")) ]=])) + write(([=[ local file_data = fd_file:read("*a") ]=])) + write(([=[ fd_file:close() ]=])) + write([=[ local block_at = 1 ]=]) + write([=[ local s, e, line ]=]) + for i, line in ipairs(op.data) do + write(([=[ line = %q ]=]):format(line)) + write(([=[ s, e = string.find(file_data, line, 1, true) ]=])) + write(([=[ assert(s, error_message(%d, "FILE_CONTENTS " .. name .. " did not match: " .. line, file_data)) ]=]):format(op.start + i)) + write(([=[ block_at = e + 1 ]=]):format(i)) + end + write([=[ end ]=]) + elseif op.op == "RUN" then + local cmd_helper = cmd_helpers[op.program] or ("%q"):format(op.program) + local redirs = " 1>stdout.txt 2>stderr.txt " + write(([=[ local ok, _, code = os.execute(%s .. " " .. %q .. %q) ]=]):format(cmd_helper, op.args, redirs)) + write([=[ if type(ok) == "number" then code = (ok >= 256 and ok / 256 or ok) end ]=]) + + write([=[ local fd_stderr = assert(io.open("stderr.txt", "rb")) ]=]) + write([=[ local stderr_data = fd_stderr:read("*a") ]=]) + write([=[ fd_stderr:close() ]=]) + + write([=[ if stderr_data:match("please report") then ]=]) + write(([=[ assert(false, error_message(%d, "RUN crashed: ", stderr_data)) ]=]):format(op.line)) + write([=[ end ]=]) + + if op.stdout or op.not_stdout or op.verbose then + write([=[ local fd_stdout = assert(io.open("stdout.txt", "rb")) ]=]) + write([=[ local stdout_data = fd_stdout:read("*a") ]=]) + write([=[ fd_stdout:close() ]=]) + end + + if op.verbose then + write([=[ print() ]=]) + write([=[ print("STDOUT: --" .. ("-"):rep(70)) ]=]) + write([=[ print(stdout_data) ]=]) + write([=[ print("STDERR: --" .. ("-"):rep(70)) ]=]) + write([=[ print(stderr_data) ]=]) + write([=[ print(("-"):rep(80)) ]=]) + write([=[ print() ]=]) + end + + check_output(write, op.stdout, "STDOUT", "stdout_data") + check_output(write, op.stderr, "STDERR", "stderr_data") + + check_output(write, op.not_stdout, "NOT_STDOUT", "stdout_data") + check_output(write, op.not_stderr, "NOT_STDERR", "stderr_data") + + if op.exit then + write(([=[ assert.same(%d, code, error_message(%d, "EXIT did not match: " .. %d, stderr_data)) ]=]):format(op.exit, op.exit_line, op.exit)) + end + end + end + write([=[ end, finally) ]=]) + write([=[ end ]=]) + + local program = table.concat(code, "\n") + local chunk = assert(load(program, "@" .. filename .. ":[TEST " .. tn .. "]", "t", env or _ENV)) + if env and setfenv then + setfenv(chunk, env) + end + t.fn = chunk() + end + + return tests +end + +return quick diff --git a/spec/util/test_env.lua b/spec/util/test_env.lua new file mode 100644 index 0000000..92fb193 --- /dev/null +++ b/spec/util/test_env.lua @@ -0,0 +1,1187 @@ +local test_env = {} + +local lfs = require("lfs") +local versions = require("spec.util.versions") + +local help_message = [[ +LuaRocks test-suite + +INFORMATION + New test-suite for LuaRocks project, using unit testing framework Busted. +REQUIREMENTS + Be sure sshd is running on your system, or use '--exclude-tags=ssh', + to not execute tests which require sshd. +USAGE + busted [-Xhelper ] +ARGUMENTS + env= Set type of environment to use ("minimal" or "full", + default: "minimal"). + noreset Don't reset environment after each test + clean Remove existing testing environment. + ci Add if running on Unix CI. + appveyor Add if running on Appveyor. + os= Set OS ("linux", "osx", or "windows"). + lua_dir= Path of Lua installation (default "/usr/local") + lua= Name of the interpreter, may be full path (default "lua") +]] + +local function help() + print(help_message) + os.exit(1) +end + +local function title(str) + print() + print(("-"):rep(#str)) + print(str) + print(("-"):rep(#str)) +end + +local dir_sep = package.config:sub(1, 1) +local function P(p) + return (p:gsub("/", dir_sep)) +end + +local function dir_path(...) + return P((table.concat({ ... }, "/"):gsub("\\", "/"):gsub("/+", "/"))) +end + +local function C(...) + return table.concat({...}, " ") +end + +--- Quote argument for shell processing. Fixes paths on Windows. +-- Adds double quotes and escapes. Based on function in fs/win32.lua. +-- @param arg string: Unquoted argument. +-- @return string: Quoted argument. +local function Q(arg) + if test_env.TEST_TARGET_OS == "windows" then + local drive_letter = "[%.a-zA-Z]?:?[\\/]" + -- Quote DIR for Windows + if arg:match("^"..drive_letter) then + arg = P(arg) + end + + if arg == "\\" then + return '\\' -- CHDIR needs special handling for root dir + end + + return '"' .. arg .. '"' + else + return "'" .. arg:gsub("'", "'\\''") .. "'" + end +end + +local function V(str) + return (str:gsub("${([^}]-)}", function(name) + name = name:lower() + local prefix, suffix = name:match("^(.*)_(.)$") + if suffix then + name = prefix + local d = assert(versions[name]) + local v, r = d:match("^([^-]*)%-(%d*)$") + if suffix == "d" then + return d + elseif suffix == "v" then + return v + elseif suffix == "r" then + return r + else + print("Test error: invalid suffix " .. suffix .. " in variable " .. name) + os.exit(1) + end + else + if not versions[name] then + print("Test error: no version definition for " .. name) + os.exit(1) + end + return versions[name] + end + end)) +end + +local function tool(name) + if test_env.TEST_TARGET_OS == "windows" then + return Q(dir_path(test_env.testing_paths.win_tools, name .. ".exe")) + else + return name + end +end + +local os_remove = os.remove +os.remove = function(f) -- luacheck: ignore + return os_remove(V(f)) +end + +local os_rename = os.rename +os.rename = function(a, b) -- luacheck: ignore + return os_rename(V(a), V(b)) +end + +-- Monkeypatch incorrect tmpname's on some Lua distributions for Windows +local os_tmpname = os.tmpname +os.tmpname = function() -- luacheck:ignore + local name = os_tmpname() + if name:sub(1, 1) == '\\' then + name = os.getenv "TEMP"..name + end + return name +end + +local lfs_chdir = lfs.chdir +lfs.chdir = function(d) -- luacheck: ignore + return lfs_chdir(V(d)) +end + +local lfs_attributes = lfs.attributes +lfs.attributes = function(f, ...) -- luacheck: ignore + return lfs_attributes(V(f), ...) +end + +local function exists(path) + return lfs.attributes(path, "mode") ~= nil +end + +function test_env.file_if_exists(path) + return lfs.attributes(path, "mode") and path +end + +function test_env.quiet(command) + if not test_env.VERBOSE then + if test_env.TEST_TARGET_OS == "windows" then + return command .. " 1> NUL 2> NUL" + else + return command .. " 1> /dev/null 2> /dev/null" + end + else + return command + end +end + +function test_env.copy(source, destination) + source = V(source) + destination = V(destination) + + local r_source, r_destination, err + r_source, err = io.open(source, "r") + if err then + print(debug.traceback()) + os.exit(1) + end + + r_destination, err = io.open(destination, "w") + if err then + print(debug.traceback()) + os.exit(1) + end + + while true do + local block = r_source:read(8192) + if not block then break end + r_destination:write(block) + end + + r_source:close() + r_destination:close() +end + +function test_env.get_tmp_path() + local path = os.tmpname() + if test_env.TEST_TARGET_OS == "windows" and not path:find(":") then + path = dir_path(os.getenv("TEMP"), path) + end + os.remove(path) + return path +end + +--- Helper function that runs the given function inside +-- a temporary directory, isolating it +-- @param f function: the function to be run +function test_env.run_in_tmp(f, finally) + local olddir = lfs.currentdir() + local tmpdir = test_env.get_tmp_path() + lfs.mkdir(tmpdir) + lfs.chdir(tmpdir) + + if not finally then + error("run_in_tmp needs a finally argument") + end + + -- for unit tests, so that current dir known by luarocks.fs (when running with non-lfs) + -- is synchronized with actual lfs (system) current dir + local fs = require("luarocks.fs") + if not fs.change_dir then + local cfg = require("luarocks.core.cfg") + cfg.init() + fs.init() + end + fs.change_dir(tmpdir) + + local lr_config = test_env.env_variables.LUAROCKS_CONFIG + + test_env.copy(lr_config, lr_config .. ".bak") + + finally(function() + test_env.copy(lr_config .. ".bak", lr_config) + lfs.chdir(olddir) + lfs.rmdir(tmpdir) + fs.change_dir(olddir) + end) + + f(tmpdir) +end + +--- Helper function for execute_bool and execute_output +-- @param command string: command to execute +-- @param print_command boolean: print command if 'true' +-- @param env_variables table: table of environment variables to export {FOO="bar", BAR="foo"} +-- @return final_command string: concatenated command to execution +function test_env.execute_helper(command, print_command, env_variables) + local final_command = "" + + if print_command then + print("[EXECUTING]: " .. command) + end + + local unset_variables = { + "LUA_PATH", + "LUA_CPATH", + "LUA_PATH_5_2", + "LUA_CPATH_5_2", + "LUA_PATH_5_3", + "LUA_CPATH_5_3", + "LUAROCKS_SYSCONFDIR", + } + + if env_variables then + if test_env.TEST_TARGET_OS == "windows" then + for _, k in ipairs(unset_variables) do + final_command = final_command .. "set " .. k .. "=&" + end + for k,v in pairs(env_variables) do + final_command = final_command .. "set " .. k .. "=" .. v .. "&" + end + final_command = final_command:sub(1, -2) .. "&" + else + for _, k in ipairs(unset_variables) do + final_command = final_command .. "unset " .. k .. "; " + end + final_command = final_command .. "export " + for k,v in pairs(env_variables) do + final_command = final_command .. k .. "='" .. v .. "' " + end + -- remove last space and add ';' to separate exporting variables from command + final_command = final_command:sub(1, -2) .. "; " + end + end + + final_command = final_command .. command .. " 2>&1" + + return final_command +end + +function test_env.execute(cmd) + local ok = os.execute(cmd) + return (ok == true or ok == 0) -- normalize Lua 5.1 output to boolean +end + +--- Execute command and returns true/false +-- @return true/false boolean: status of the command execution +local function execute_bool(command, print_command, env_variables) + command = test_env.execute_helper(command, print_command, env_variables) + + local redirect_filename + local redirect = "" + if print_command ~= nil then + redirect_filename = dir_path(test_env.testing_paths.luarocks_tmp, "output.txt") + redirect = " > " .. redirect_filename + os.remove(redirect_filename) + end + local ok = test_env.execute(command .. redirect) + if redirect ~= "" then + if not ok or test_env.VERBOSE then + local fd = io.open(redirect_filename, "r") + if fd then + print(fd:read("*a")) + fd:close() + end + end + os.remove(redirect_filename) + end + return ok +end + +--- Execute command and returns output of command +-- @return output string: output the command execution +local function execute_output(command, print_command, env_variables) + command = test_env.execute_helper(command, print_command, env_variables) + + local file = assert(io.popen(command)) + local output = file:read('*all') + file:close() + return (output:gsub("\r\n", "\n"):gsub("\n$", "")) -- remove final newline +end + +--- Set test_env.LUA_V or test_env.LUAJIT_V based +-- on version of Lua used to run this script. +function test_env.set_lua_version() + if _G.jit then + test_env.LUAJIT_V = _G.jit.version:match("(2%.%d)%.%d") + test_env.lua_version = "5.1" + else + test_env.LUA_V = _VERSION:match("5%.%d") + test_env.lua_version = test_env.LUA_V + end +end + +--- Set all arguments from input into global variables +function test_env.set_args() + -- if at least Lua/LuaJIT version argument was found on input start to parse other arguments to env. variables + test_env.TYPE_TEST_ENV = "minimal" + test_env.RESET_ENV = true + + for _, argument in ipairs(arg) do + if argument:find("^env=") then + test_env.TYPE_TEST_ENV = argument:match("^env=(.*)$") + elseif argument == "noreset" then + test_env.RESET_ENV = false + elseif argument == "clean" then + test_env.TEST_ENV_CLEAN = true + elseif argument == "verbose" then + test_env.VERBOSE = true + elseif argument == "ci" then + test_env.CI = true + elseif argument == "appveyor" then + test_env.APPVEYOR = true + elseif argument:find("^os=") then + test_env.TEST_TARGET_OS = argument:match("^os=(.*)$") + elseif argument == "mingw" then + test_env.MINGW = true + elseif argument == "vs" then + test_env.MINGW = false + elseif argument:find("^lua_dir=") then + test_env.LUA_DIR = argument:match("^lua_dir=(.*)$") + elseif argument:find("^lua=") then + test_env.LUA = argument:match("^lua=(.*)$") + else + help() + end + end + + if not test_env.TEST_TARGET_OS then + title("OS CHECK") + + if dir_sep == "\\" then + test_env.TEST_TARGET_OS = "windows" + if test_env.APPVEYOR then + test_env.OPENSSL_INCDIR = "C:\\OpenSSL-v111-Win32\\include" + test_env.OPENSSL_LIBDIR = "C:\\OpenSSL-v111-Win32\\lib" + if test_env.MINGW then + test_env.OPENSSL_LIBDIR = "C:\\OpenSSL-v111-Win32\\bin" + end + end + else + local system = execute_output("uname -s") + if system == "Linux" then + test_env.TEST_TARGET_OS = "linux" + if test_env.CI then + test_env.OPENSSL_INCDIR = "/usr/include" + test_env.OPENSSL_LIBDIR = "/usr/lib/x86_64-linux-gnu" + end + elseif system == "Darwin" then + test_env.TEST_TARGET_OS = "osx" + if test_env.CI then + if exists("/opt/homebrew/opt/openssl@3/include") then + test_env.OPENSSL_INCDIR = "/opt/homebrew/opt/openssl@3/include" + test_env.OPENSSL_LIBDIR = "/opt/homebrew/opt/openssl@3/lib" + elseif exists("/opt/homebrew/opt/openssl@1.1/include") then + test_env.OPENSSL_INCDIR = "/opt/homebrew/opt/openssl@1.1/include" + test_env.OPENSSL_LIBDIR = "/opt/homebrew/opt/openssl@1.1/lib" + elseif exists("/opt/homebrew/opt/openssl/include") then + test_env.OPENSSL_INCDIR = "/opt/homebrew/opt/openssl/include" + test_env.OPENSSL_LIBDIR = "/opt/homebrew/opt/openssl/lib" + else + test_env.OPENSSL_INCDIR = "/usr/local/opt/openssl/include" + test_env.OPENSSL_LIBDIR = "/usr/local/opt/openssl/lib" + end + end + end + end + print(test_env.TEST_TARGET_OS) + end + + if test_env.TEST_TARGET_OS == "windows" then + test_env.lib_extension = "dll" + else + test_env.lib_extension = "so" + end + + test_env.openssl_dirs = "" + if test_env.OPENSSL_INCDIR then + test_env.openssl_dirs = C("OPENSSL_INCDIR=" .. test_env.OPENSSL_INCDIR, + "OPENSSL_LIBDIR=" .. test_env.OPENSSL_LIBDIR) + end + + return true +end + +function test_env.copy_dir(source_path, target_path) + source_path = V(source_path) + target_path = V(target_path) + + local flag = test_env.TEST_TARGET_OS == "windows" and "-R" or "-a" + os.execute(C(tool("cp"), flag, dir_path(source_path, "."), target_path)) +end + +--- Remove directory recursively +-- @param path string: directory path to delete +function test_env.remove_dir(path) + path = V(path) + + if exists(path) then + for file in lfs.dir(path) do + if file ~= "." and file ~= ".." then + local full_path = dir_path(path, file) + + if lfs.attributes(full_path, "mode") == "directory" then + test_env.remove_dir(full_path) + else + os.remove(full_path) + end + end + end + end + lfs.rmdir(path) +end + +--- Remove subdirectories of a directory that match a pattern +-- @param path string: path to directory +-- @param pattern string: pattern matching basenames of subdirectories to be removed +function test_env.remove_subdirs(path, pattern) + path = V(path) + + if exists(path) then + for file in lfs.dir(path) do + if file ~= "." and file ~= ".." then + local full_path = dir_path(path, file) + + if lfs.attributes(full_path, "mode") == "directory" and file:find(pattern) then + test_env.remove_dir(full_path) + end + end + end + end +end + +--- Remove files matching a pattern +-- @param path string: directory where to delete files +-- @param pattern string: pattern matching basenames of files to be deleted +-- @return result_check boolean: true if one or more files deleted +function test_env.remove_files(path, pattern) + path = V(path) + + local result_check = false + if exists(path) then + for file in lfs.dir(path) do + if file ~= "." and file ~= ".." then + if file:find(pattern) then + if os.remove(dir_path(path, file)) then + result_check = true + end + end + end + end + end + return result_check +end + + +--- Function for downloading rocks and rockspecs +-- @param urls table: array of full names of rocks/rockspecs to download +-- @param save_path string: path to directory, where to download rocks/rockspecs +-- @return make_manifest boolean: true if new rocks downloaded +local function download_rocks(urls, save_path) + local luarocks_repo = "https://luarocks.org/" + + local to_download = {} + local fixtures = {} + for _, url in ipairs(urls) do + url = V(url) + + if url:match("^spec/fixtures") then + table.insert(fixtures, P(url:gsub("^spec/fixtures", test_env.testing_paths.fixtures_dir))) + else + -- check if already downloaded + if not exists(dir_path(save_path, url)) then + table.insert(to_download, ((luarocks_repo .. url):gsub("org//", "org/"))) + end + end + end + + if #fixtures > 0 then + os.execute(C(tool("cp"), table.concat(fixtures, " "), save_path)) + end + + if #to_download > 0 then + local ok = execute_bool(C(tool("wget"), "--no-check-certificate -cP", save_path, table.concat(to_download, " "))) + if not ok then + os.exit(1) + end + end + + return (#fixtures > 0) or (#to_download > 0) +end + +--- Create a file containing a string. +-- @param pathname string: path to file. +-- @param str string: content of the file. +function test_env.write_file(pathname, str, finally) + pathname = V(pathname) + + local file = assert(io.open(pathname, "wb")) + file:write(str) + file:close() + if finally then + finally(function() + os.remove(pathname) + end) + end +end + +--- Create environment variables needed for tests +-- @param testing_paths table: table with paths to testing directory +-- @return env_variables table: table with created environment variables +local function create_env(testing_paths) + local lua_v = _VERSION:gsub("Lua ", "") + local testrun_dir = test_env.testing_paths.testrun_dir + local lrprefix = testing_paths.testing_lrprefix + local tree = testing_paths.testing_tree + local sys_tree = testing_paths.testing_sys_tree + local deps_tree = testing_paths.testing_deps_tree + + if test_env.LUAJIT_V then + lua_v="5.1" + end + + local env_variables = {} + env_variables.GNUPGHOME = testing_paths.gpg_dir + env_variables.LUA_VERSION = lua_v + env_variables.LUAROCKS_CONFIG = dir_path(testrun_dir, "testing_config.lua") + + local lua_path = {} + if test_env.TEST_TARGET_OS == "windows" then + table.insert(lua_path, dir_path(lrprefix, "lua", "?.lua")) + else + table.insert(lua_path, dir_path(lrprefix, "share", "lua", lua_v, "?.lua")) + end + table.insert(lua_path, dir_path(tree, "share", "lua", lua_v, "?.lua")) + table.insert(lua_path, dir_path(tree, "share", "lua", lua_v, "?", "init.lua")) + table.insert(lua_path, dir_path(sys_tree, "share", "lua", lua_v, "?.lua")) + table.insert(lua_path, dir_path(sys_tree, "share", "lua", lua_v, "?", "init.lua")) + table.insert(lua_path, dir_path(deps_tree, "share", "lua", lua_v, "?.lua")) + table.insert(lua_path, dir_path(deps_tree, "share", "lua", lua_v, "?", "init.lua")) + table.insert(lua_path, dir_path(testing_paths.src_dir, "?.lua")) + env_variables.LUA_PATH = table.concat(lua_path, ";") .. ";" + + local lua_cpath = {} + local lib_pattern = "?." .. test_env.lib_extension + table.insert(lua_cpath, dir_path(tree, "lib", "lua", lua_v, lib_pattern)) + table.insert(lua_cpath, dir_path(sys_tree, "lib", "lua", lua_v, lib_pattern)) + table.insert(lua_cpath, dir_path(deps_tree, "lib", "lua", lua_v, lib_pattern)) + env_variables.LUA_CPATH = table.concat(lua_cpath, ";") .. ";" + + local path = { os.getenv("PATH") } + table.insert(path, dir_path(tree, "bin")) + table.insert(path, dir_path(sys_tree, "bin")) + table.insert(path, dir_path(deps_tree, "bin")) + env_variables.PATH = table.concat(path, test_env.TARGET_OS == "windows" and ";" or ":") + + return env_variables +end + +local function make_run_function(cmd_name, exec_function, with_coverage, do_print) + local cmd_prefix = Q(test_env.testing_paths.lua) + local testrun_dir = test_env.testing_paths.testrun_dir + + if with_coverage then + cmd_prefix = C(cmd_prefix, "-e", "\"require('luacov.runner')([[" .. testrun_dir .. "/luacov.config]])\"") + end + + if cmd_name then + cmd_prefix = C(cmd_prefix, dir_path(test_env.testing_paths.src_dir, "bin", cmd_name)) + end + + cmd_prefix = P(cmd_prefix) + + return function(cmd, new_vars) + cmd = V(cmd) + local temp_vars = {} + for k, v in pairs(test_env.env_variables) do + temp_vars[k] = v + end + if new_vars then + for k, v in pairs(new_vars) do + temp_vars[k] = v + end + end + return exec_function(C(cmd_prefix, cmd), do_print, temp_vars) + end +end + +local function make_run_functions() + local fns = {} + + local cmds = { + ["lua"] = nil, + ["luarocks"] = "luarocks", + ["luarocks_admin"] = "luarocks-admin", + } + + for _, name in ipairs({"lua", "luarocks", "luarocks_admin"}) do + fns[name] = make_run_function(cmds[name], execute_output, true, true) + fns[name .. "_bool"] = make_run_function(cmds[name], execute_bool, true, true) + fns[name .. "_nocov"] = make_run_function(cmds[name], execute_bool, false, true) + fns[name .. "_noprint_nocov"] = make_run_function(cmds[name], execute_bool, false, false) + end + + return fns +end + +local function move_file(src, dst) + local ok = execute_bool(C(tool("mv"), P(src), P(dst))) + if not ok then + print(debug.traceback()) + os.exit(1) + end +end + +--- Rebuild environment. +-- Remove old installed rocks and install new ones, +-- updating manifests and tree copies. +local function build_environment(rocks, env_variables) + title("BUILDING ENVIRONMENT") + local testing_paths = test_env.testing_paths + test_env.remove_dir(testing_paths.testing_tree) + test_env.remove_dir(testing_paths.testing_sys_tree) + + lfs.mkdir(testing_paths.testing_tree) + lfs.mkdir(testing_paths.testing_sys_tree) + lfs.mkdir(testing_paths.testing_deps_tree) + + test_env.run.luarocks_admin_nocov(C("make_manifest", Q(testing_paths.testing_server))) + test_env.run.luarocks_admin_nocov(C("make_manifest", Q(testing_paths.testing_cache))) + + for _, rock in ipairs(rocks) do + local only_server = "--only-server=" .. testing_paths.testing_cache + local tree = "--tree=" .. testing_paths.testing_deps_tree + if not test_env.run.luarocks_nocov(test_env.quiet(C("install", only_server, tree, Q(rock)), env_variables)) then + assert(test_env.run.luarocks_nocov(C("build", tree, Q(rock)), env_variables)) + assert(test_env.run.luarocks_nocov(C("pack", tree, Q(rock)), env_variables)) + move_file(rock .. "-*.rock", testing_paths.testing_cache) + end + end +end + +local function find_lua() + -- (1) LUA is a full path + if test_env.LUA and test_env.LUA:match("[/\\]") then + + local lua_bindir = test_env.LUA:match("^(.-)[/\\][^/\\]*$") + local luadir = test_env.LUA_DIR or lua_bindir:gsub("[/\\]bin$") + local lua = test_env.LUA + + return lua_bindir, luadir, lua + end + + -- (2) LUA is just the interpreter name + local lua_exe = test_env.LUA + or ((test_env.TEST_TARGET_OS == "windows") and "lua.exe") + or "lua" + + -- (2.1) LUA_DIR was given + if test_env.LUA_DIR then + + local luadir = test_env.LUA_DIR + local lua_bindir = exists(dir_path(luadir, "bin")) + and dir_path(luadir, "bin") + or luadir + local lua = dir_path(lua_bindir, lua_exe) + + return lua_bindir, luadir, lua + end + + -- (2.2) LUA_DIR was not given, try some default paths + local try_dirs = (test_env.TEST_TARGET_OS == "windows") + and { os.getenv("ProgramFiles(x86)").."\\LuaRocks" } + or { "/usr/local", "/usr" } + + for _, luadir in ipairs(try_dirs) do + for _, lua_bindir in ipairs({ luadir, dir_path(luadir, "bin") }) do + local lua = dir_path(lua_bindir, lua_exe) + if exists(lua) then + return lua_bindir, luadir, lua + end + end + end +end + +local function create_testing_paths(suffix) + local paths = {} + + paths.lua_bindir, paths.luadir, paths.lua = find_lua() + if (not paths.lua) or (not exists(paths.lua)) then + error("Lua interpreter not found! Run `busted -Xhelper help` for options") + end + + local base_dir = lfs.currentdir() + paths.src_dir = dir_path(base_dir, "src") + paths.spec_dir = dir_path(base_dir, "spec") + paths.util_dir = dir_path(base_dir, "spec", "util") + paths.fixtures_dir = dir_path(base_dir, "spec", "fixtures") + paths.fixtures_repo_dir = dir_path(base_dir, "spec", "fixtures", "a_repo") + paths.gpg_dir = dir_path(base_dir, "spec", "fixtures", "gpg") + + local testrun_dir = dir_path(base_dir, "testrun") + paths.testrun_dir = testrun_dir + paths.testing_lrprefix = dir_path(testrun_dir, "testing_lrprefix-" .. suffix) + paths.testing_tree = dir_path(testrun_dir, "testing-" .. suffix) + paths.testing_sys_tree = dir_path(testrun_dir, "testing_sys-" .. suffix) + paths.testing_deps_tree = dir_path(testrun_dir, "testing_deps-" .. suffix) + paths.testing_cache = dir_path(testrun_dir, "testing_cache-" .. suffix) + paths.testing_server = dir_path(testrun_dir, "testing_server-" .. suffix) + + local rocks_v = "rocks-" .. test_env.lua_version + paths.testing_rocks = dir_path(paths.testing_tree, "lib", "luarocks", rocks_v) + paths.testing_sys_rocks = dir_path(paths.testing_sys_tree, "lib", "luarocks", rocks_v) + paths.testing_deps_rocks = dir_path(paths.testing_deps_tree, "lib", "luarocks", rocks_v) + + if test_env.TEST_TARGET_OS == "windows" then + paths.luarocks_tmp = os.getenv("TEMP") + else + paths.luarocks_tmp = "/tmp/luarocks_testing" + end + + if test_env.TEST_TARGET_OS == "windows" then + paths.win_tools = dir_path(base_dir, "win32", "tools") + end + + return paths +end + +--- Helper function to unload luarocks modules from global table package.loaded +-- Needed to load our local (testing) version of LuaRocks +function test_env.unload_luarocks() + for modname, _ in pairs(package.loaded) do + if modname:match("^luarocks%.") then + package.loaded[modname] = nil + end + end + local src_pattern = dir_path(test_env.testing_paths.src_dir, "?.lua") + if not package.path:find(src_pattern, 1, true) then + package.path = src_pattern .. ";" .. package.path + end +end + +local function get_luarocks_platform(variables) + local print_arch_script = "\"" .. + "cfg = require('luarocks.core.cfg');" .. + "cfg.init();" .. + "print(cfg.arch)" .. + "\"" + local cmd = C(test_env.testing_paths.lua, "-e", print_arch_script) + return execute_output(cmd, false, variables) +end + +--- Test if required rock is installed and if not, install it. +-- Return `true` if the rock is already installed or has been installed successfully, +-- `false` if installation failed. +function test_env.need_rock(rock) + rock = V(rock) + + print("Check if " .. rock .. " is installed") + if test_env.run.luarocks_noprint_nocov(test_env.quiet("show " .. rock)) then + return true + else + local ok = test_env.run.luarocks_noprint_nocov(test_env.quiet("install " .. rock)) + if not ok then + print("WARNING: failed installing " .. rock) + end + return ok + end +end + +--- For each key-value pair in replacements table +-- replace %{key} in given string with value. +local function substitute(str, replacements) + return (str:gsub("%%%b{}", function(marker) + local r = replacements[marker:sub(3, -2)] + if r then + r = r:gsub("\\", "\\\\") + end + return r + end)) +end + + +--- Create configs for luacov and several versions of Luarocks +-- configs needed for some tests. +local function create_configs() + local testrun_dir = test_env.testing_paths.testrun_dir + + -- testing_config.lua + -- testing_config_show_downloads.lua + -- testing_config_no_downloader.lua + local config_content = substitute([[ + rocks_trees = { + { name = "user", root = "%{testing_tree}" }, + { name = "deps", root = "%{testing_deps_tree}" }, + { name = "system", root = "%{testing_sys_tree}" }, + } + rocks_servers = { + "%{testing_server}" + } + local_cache = "%{testing_cache}" + upload_server = "testing" + upload_user = "%{user}" + upload_servers = { + testing = { + rsync = "localhost/tmp/luarocks_testing", + }, + } + ]], { + user = "testuser", + testing_sys_tree = test_env.testing_paths.testing_sys_tree, + testing_deps_tree = test_env.testing_paths.testing_deps_tree, + testing_tree = test_env.testing_paths.testing_tree, + testing_server = test_env.testing_paths.testing_server, + testing_cache = test_env.testing_paths.testing_cache + }) + + test_env.write_file(dir_path(testrun_dir, "testing_config.lua"), config_content .. " \nweb_browser = \"true\"") + test_env.write_file(dir_path(testrun_dir, "testing_config_show_downloads.lua"), config_content + .. "show_downloads = true \n rocks_servers={\"http://luarocks.org/repositories/rocks\"}") + test_env.write_file(dir_path(testrun_dir, "testing_config_no_downloader.lua"), config_content + .. "variables = { WGET = 'invalid', CURL = 'invalid' }") + + -- testing_config_sftp.lua + config_content = substitute([[ + rocks_trees = { + "%{testing_tree}", + "%{testing_deps_tree}", + "%{testing_sys_tree}", + } + local_cache = "%{testing_cache}" + upload_server = "testing" + upload_user = "%{user}" + upload_servers = { + testing = { + sftp = "localhost/tmp/luarocks_testing", + }, + } + ]], { + user = "testuser", + testing_sys_tree = test_env.testing_paths.testing_sys_tree, + testing_deps_tree = test_env.testing_paths.testing_deps_tree, + testing_tree = test_env.testing_paths.testing_tree, + testing_cache = test_env.testing_paths.testing_cache + }) + + test_env.write_file(dir_path(testrun_dir, "testing_config_sftp.lua"), config_content) + + -- luacov.config + config_content = substitute([[ + return { + statsfile = "%{statsfile}", + reportfile = "%{reportfile}", + exclude = { + "src%/luarocks%/vendor.+$", + }, + modules = { + ["luarocks"] = "%{luarocks_path}", + ["luarocks-admin"] = "%{luarocks_admin_path}", + ["luarocks.*"] = "src", + ["luarocks.*.*"] = "src", + ["luarocks.*.*.*"] = "src" + } + } + ]], { + statsfile = dir_path(testrun_dir, "luacov.stats.out"), + reportfile = dir_path(testrun_dir, "luacov.report.out"), + luarocks_path = dir_path("src", "bin", "luarocks"), + luarocks_admin_path = dir_path("src", "bin", "luarocks-admin"), + }) + + test_env.write_file(dir_path(testrun_dir, "luacov.config"), config_content) + + config_content = [[ + -- Config file of mock LuaRocks.org site for tests + upload = { + server = "http://localhost:8080", + tool_version = "1.0.0", + api_version = "1", + } + ]] + test_env.write_file(dir_path(testrun_dir, "luarocks_site.lua"), config_content) +end + +--- Remove testing directories. +local function clean() + local testrun_dir = test_env.testing_paths.testrun_dir + + print("Cleaning testing directory...") + test_env.remove_dir(test_env.testing_paths.luarocks_tmp) + test_env.remove_subdirs(testrun_dir, "testing[_%-]") + test_env.remove_files(testrun_dir, "testing_") + test_env.remove_files(testrun_dir, "luacov") + test_env.remove_files(testrun_dir, "upload_config") + test_env.remove_files(testrun_dir, "luarocks_site") + print("Cleaning done!") +end + +--- Setup current checkout of luarocks to work with testing prefix. +local function setup_luarocks() + local testing_paths = test_env.testing_paths + title("Setting up LuaRocks") + + local lines = { + "return {", + ("SYSCONFDIR = %q,"):format(dir_path(testing_paths.testing_lrprefix, "etc/luarocks")), + ("LUA_DIR = %q,"):format(testing_paths.luadir), + ("LUA_BINDIR = %q,"):format(testing_paths.lua_bindir), + ("LUA = %q,"):format(testing_paths.lua), + } + + if test_env.TEST_TARGET_OS == "windows" then + if test_env.MINGW then + table.insert(lines, [[SYSTEM = "mingw",]]) + else + table.insert(lines, [[SYSTEM = "windows",]]) + end + table.insert(lines, ("WIN_TOOLS = %q,"):format(testing_paths.win_tools)) + end + + table.insert(lines, "}") + + test_env.write_file("src/luarocks/core/hardcoded.lua", table.concat(lines, "\n") .. "\n") + + print("LuaRocks set up correctly!") +end + +local function mock_api_call(path) + return test_env.execute(C(tool("wget"), "--timeout=0.1 --quiet --tries=10 http://localhost:8080" .. path)) +end + +function test_env.mock_server_init() + if not test_env.mock_prepared then + error("need to setup_specs with with_mock set to true") + end + + local testing_paths = test_env.testing_paths + assert(test_env.need_rock("restserver-xavante")) + + local lua = Q(testing_paths.lua) + local mock_server = Q(dir_path(testing_paths.util_dir, "mock-server.lua")) + local fixtures_dir = Q(testing_paths.fixtures_dir) + + local cmd = C(lua, mock_server, fixtures_dir) + + local bg_cmd = test_env.TEST_TARGET_OS == "windows" + and C("start", "/b", "\"\"", cmd) + or C(cmd, "&") + + os.execute(test_env.execute_helper(bg_cmd, true, test_env.env_variables)) + + for _ = 1, 100 do + if mock_api_call("/api/tool_version") then + break + end + os.execute(test_env.TEST_TARGET_OS == "windows" + and "ping 192.0.2.0 -n 1 -w 250 > NUL" + or "sleep 0.1") + end + +end + +function test_env.mock_server_done() + mock_api_call("/shutdown") +end + +local function find_binary_rock(src_rock, dirname) + local patt = src_rock:gsub("([.-])", "%%%1"):gsub("src", ".*[^s][^r][^c]") + for name in lfs.dir(dirname) do + if name:match(patt) then + return true + end + end + return false +end + +local function prepare_mock_server_binary_rocks() + if test_env.mock_prepared then + return + end + + local testing_paths = test_env.testing_paths + + local rocks = { + -- rocks needed for mock-server + "luasocket-${LUASOCKET}.src.rock", + "coxpcall-1.16.0-1.src.rock", + "binaryheap-${BINARYHEAP}.src.rock", + "timerwheel-${TIMERWHEEL}.src.rock", + "copas-${COPAS}.src.rock", + "luafilesystem-${LUAFILESYSTEM}.src.rock", + "xavante-2.4.0-1.src.rock", + "wsapi-1.6.1-1.src.rock", + "rings-1.3.0-1.src.rock", + "wsapi-xavante-1.6.1-1.src.rock", + "dkjson-${DKJSON}.src.rock", + "restserver-0.1-1.src.rock", + "restserver-xavante-0.2-1.src.rock", + } + local make_manifest = download_rocks(rocks, testing_paths.testing_server) + for _, rock in ipairs(rocks) do + rock = V(rock) + local rockname = rock:gsub("%-[^-]+%-%d+%.[%a.]+$", "") + if not find_binary_rock(rock, testing_paths.testing_server) then + local rockpath = dir_path(testing_paths.testing_server, rock) + local tree = "--tree=" .. testing_paths.testing_cache + + test_env.run.luarocks_nocov(C("build", Q(rockpath), tree)) + test_env.run.luarocks_nocov(C("pack", rockname, tree)) + + move_file(rockname .. "-*.rock", testing_paths.testing_server) + make_manifest = true + end + end + if make_manifest then + test_env.run.luarocks_admin_nocov(C("make_manifest", Q(testing_paths.testing_server))) + end + + test_env.mock_prepared = true +end + +--- +-- Main function to create config files and testing environment +function test_env.main() + local testing_paths = test_env.testing_paths + local testrun_dir = test_env.testing_paths.testrun_dir + + if test_env.TEST_ENV_CLEAN then + clean() + end + + lfs.mkdir(testrun_dir) + test_env.write_file(dir_path(testrun_dir, ".luarocks-no-project"), "") + lfs.mkdir(testing_paths.testing_cache) + lfs.mkdir(testing_paths.luarocks_tmp) + + create_configs() + + setup_luarocks() + + -- Preparation of rocks for building environment + local rocks = {} -- names of rocks, required for building environment + local urls = {} -- names of rock and rockspec files to be downloaded + + local env_vars = { + LUAROCKS_CONFIG = dir_path(testrun_dir, "testing_config.lua") + } + + if test_env.TYPE_TEST_ENV == "full" then + table.insert(urls, "/luafilesystem-${LUAFILESYSTEM}.src.rock") + table.insert(urls, "/luasocket-${LUASOCKET}.src.rock") + table.insert(urls, "/luasec-${LUASEC}.src.rock") + table.insert(urls, "/md5-1.2-1.src.rock") + table.insert(urls, "/manifests/hisham/lua-zlib-1.2-0.src.rock") + table.insert(urls, "/manifests/hisham/lua-bz2-0.2.1.1-1.src.rock") + rocks = {"luafilesystem", "luasocket", "luasec", "md5", "lua-zlib", "lua-bz2"} + if test_env.TEST_TARGET_OS ~= "windows" then + if test_env.lua_version == "5.1" then + table.insert(urls, "/bit32-${BIT32}.src.rock") + table.insert(rocks, "bit32") + end + table.insert(urls, "/luaposix-${LUAPOSIX}.src.rock") + table.insert(rocks, "luaposix") + end + assert(test_env.run.luarocks_nocov(C("config", "variables.OPENSSL_INCDIR", Q(test_env.OPENSSL_INCDIR)), env_vars)) + assert(test_env.run.luarocks_nocov(C("config", "variables.OPENSSL_LIBDIR", Q(test_env.OPENSSL_LIBDIR)), env_vars)) + end + + -- luacov is needed for both minimal or full environment + table.insert(urls, "/luacov-${LUACOV}.src.rock") + table.insert(urls, "/cluacov-${CLUACOV}.src.rock") + table.insert(rocks, "luacov") + table.insert(rocks, "cluacov") + + -- Download rocks needed for LuaRocks testing environment + lfs.mkdir(testing_paths.testing_server) + download_rocks(urls, testing_paths.testing_server) + + build_environment(rocks, env_vars) +end + +--- Function for initial setup of environment and variables +function test_env.setup_specs(extra_rocks, use_mock) + test_env.unload_luarocks() + + local testrun_dir = test_env.testing_paths.testrun_dir + local variables = test_env.env_variables + + -- if global variable about successful creation of testing environment doesn't exist, build environment + if not test_env.setup_done then + if test_env.CI then + if not exists(os.getenv("HOME"), ".ssh/id_rsa.pub") then + execute_bool("ssh-keygen -t rsa -P \"\" -f ~/.ssh/id_rsa") + execute_bool("cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys") + execute_bool("chmod og-wx ~/.ssh/authorized_keys") + execute_bool("ssh-keyscan localhost >> ~/.ssh/known_hosts") + end + end + + test_env.main() + + -- preload before meddling with package.path + require("spec.util.git_repo") + require("spec.util.quick") + + package.path = variables.LUA_PATH + package.cpath = variables.LUA_CPATH + + test_env.platform = get_luarocks_platform(test_env.env_variables) + test_env.wrapper_extension = test_env.TEST_TARGET_OS == "windows" and ".bat" or "" + test_env.setup_done = true + title("RUNNING TESTS") + end + + if use_mock == "mock" then + prepare_mock_server_binary_rocks() + end + + if extra_rocks then + local make_manifest = download_rocks(extra_rocks, test_env.testing_paths.testing_server) + if make_manifest then + test_env.run.luarocks_admin_nocov("make_manifest " .. test_env.testing_paths.testing_server) + end + end + + if test_env.RESET_ENV then + test_env.remove_dir(test_env.testing_paths.testing_tree) + test_env.remove_dir(test_env.testing_paths.testing_sys_tree) + end + + lfs.chdir(testrun_dir) +end + +test_env.set_lua_version() +test_env.set_args() +test_env.testing_paths = create_testing_paths(test_env.LUA_V or test_env.LUAJIT_V) +test_env.env_variables = create_env(test_env.testing_paths) +test_env.run = make_run_functions() +test_env.exists = exists +test_env.V = V +test_env.Q = Q +test_env.P = P +test_env.platform = get_luarocks_platform(test_env.env_variables) + +return test_env diff --git a/spec/util/versions.lua b/spec/util/versions.lua new file mode 100644 index 0000000..77c1129 --- /dev/null +++ b/spec/util/versions.lua @@ -0,0 +1,17 @@ +return { + binaryheap = "0.4-1", -- dependency for copas + bit32 = "5.3.5.1-1", -- dependency for luaposix on Lua 5.1 + cluacov = "0.1.3-1", + copas = "3.0.0-2", + cprint = "0.2-1", + dkjson = "2.6-1", + lpeg = "1.0.0-1", + luacov = "0.15.0-1", + luafilesystem = "1.8.0-1", + luafilesystem_old = "1.6.3-2", + luaposix = "35.1-1", + luasocket = "3.0.0-1", + luasec = "1.3.2-1", + lxsh = "0.8.6-2", + timerwheel = "0.2.0-2", -- dependency for copas +} diff --git a/spec/util_spec.lua b/spec/util_spec.lua new file mode 100644 index 0000000..0f199c9 --- /dev/null +++ b/spec/util_spec.lua @@ -0,0 +1,55 @@ +local test_env = require("spec.util.test_env") +local lfs = require("lfs") +local run = test_env.run + +describe("Basic tests #integration", function() + + before_each(function() + test_env.setup_specs() + end) + + it("--version", function() + assert.is_true(run.luarocks_bool("--version")) + end) + + it("unknown command", function() + assert.is_false(run.luarocks_bool("unknown_command")) + end) + + it("arguments fail", function() + assert.is_false(run.luarocks_bool("--porcelain=invalid")) + assert.is_false(run.luarocks_bool("--invalid-flag")) + assert.is_false(run.luarocks_bool("--server")) + assert.is_false(run.luarocks_bool("--server --porcelain")) + assert.is_false(run.luarocks_bool("--invalid-flag=abc")) + assert.is_false(run.luarocks_bool("invalid=5")) + end) + + it("executing from not existing directory #unix", function() + local main_path = lfs.currentdir() + assert.is_true(lfs.mkdir("idontexist")) + assert.is_true(lfs.chdir("idontexist")) + local delete_path = lfs.currentdir() + assert.is_true(os.remove(delete_path)) + + local output = run.luarocks("") + assert.is.falsy(output:find("the Lua package manager")) + assert.is_true(lfs.chdir(main_path)) + + output = run.luarocks("") + assert.is.truthy(output:find("the Lua package manager")) + end) + + it("--timeout", function() + assert.is.truthy(run.luarocks("--timeout=10")) + end) + + it("--timeout invalid", function() + assert.is_false(run.luarocks_bool("--timeout=abc")) + end) + + it("--only-server", function() + assert.is.truthy(run.luarocks("--only-server=testing")) + end) + +end) diff --git a/spec/which_spec.lua b/spec/which_spec.lua new file mode 100644 index 0000000..5712511 --- /dev/null +++ b/spec/which_spec.lua @@ -0,0 +1,39 @@ +local test_env = require("spec.util.test_env") +local run = test_env.run +local P = test_env.P + +local extra_rocks = { + "/say-1.2-1.src.rock", +} + +describe("luarocks which #integration", function() + + lazy_setup(function() + test_env.setup_specs(extra_rocks) + end) + + it("fails on missing arguments", function() + local output = run.luarocks("which") + assert.match("missing argument 'modname'", output, 1, true) + end) + + it("finds modules found in package.path", function() + assert.is_true(run.luarocks_bool("install say 1.2")) + local output = run.luarocks("which say") + assert.match(P"say/init.lua", output, 1, true) + assert.match("provided by say 1.2-1", output, 1, true) + end) + + it("finds modules found in package.path", function() + run.luarocks("install ") + local output = run.luarocks("which luarocks.loader") + assert.match("luarocks/loader.lua", output, 1, true) + assert.match("not installed as a rock", output, 1, true) + end) + + it("report modules not found", function() + local output = run.luarocks("which asdfgaoeui") + assert.match("Module 'asdfgaoeui' not found", output, 1, true) + end) + +end) diff --git a/spec/write_rockspec_spec.lua b/spec/write_rockspec_spec.lua new file mode 100644 index 0000000..abd5c80 --- /dev/null +++ b/spec/write_rockspec_spec.lua @@ -0,0 +1,104 @@ +local test_env = require("spec.util.test_env") +local git_repo = require("spec.util.git_repo") +local lfs = require("lfs") +local run = test_env.run + +describe("luarocks write_rockspec tests #integration", function() + + lazy_setup(function() + test_env.setup_specs() + end) + + it("fails with invalid argument", function() + assert.is_false(run.luarocks_bool("write_rockspec invalid")) + end) + + it("fails with invalid zip", function() + assert.is_false(run.luarocks_bool("write_rockspec http://example.com/invalid.zip")) + end) + + describe("from #git #unix", function() + local git + + lazy_setup(function() + git = git_repo.start() + end) + + teardown(function() + git:stop() + end) + + it("runs with no flags/arguments", function() + local d = lfs.currentdir() + finally(function() + os.remove("testrock-dev-1.rockspec") + lfs.chdir(d) + test_env.remove_dir("testrock") + end) + os.execute("git clone git://localhost/testrock") + lfs.chdir("testrock") + assert.is_true(run.luarocks_bool("write_rockspec")) + assert.is.truthy(lfs.attributes("testrock-dev-1.rockspec")) + end) + + it("runs", function() + finally(function() os.remove("testrock-dev-1.rockspec") end) + assert.is_true(run.luarocks_bool("write_rockspec git://localhost/testrock")) + assert.is.truthy(lfs.attributes("testrock-dev-1.rockspec")) + end) + + it("runs with --tag", function() + finally(function() os.remove("testrock-2.3.0-1.rockspec") end) + assert.is_true(run.luarocks_bool("write_rockspec git://localhost/testrock --tag=v2.3.0")) + assert.is.truthy(lfs.attributes("testrock-2.3.0-1.rockspec")) + -- TODO check contents + end) + + it("runs with format flag", function() + finally(function() os.remove("testrock-dev-1.rockspec") end) + assert.is_true(run.luarocks_bool("write_rockspec git://localhost/testrock --rockspec-format=1.1 --lua-versions=5.1,5.2")) + assert.is.truthy(lfs.attributes("testrock-dev-1.rockspec")) + -- TODO check contents + end) + + it("runs with full flags", function() + finally(function() os.remove("testrock-dev-1.rockspec") end) + assert.is_true(run.luarocks_bool("write_rockspec git://localhost/testrock --lua-versions=5.1,5.2 --license=\"MIT/X11\" " + .. " --homepage=\"http://www.luarocks.org\" --summary=\"A package manager for Lua modules\" ")) + assert.is.truthy(lfs.attributes("testrock-dev-1.rockspec")) + -- TODO check contents + end) + + it("with various flags", function() + finally(function() os.remove("testrock-dev-1.rockspec") end) + assert.is_true(run.luarocks_bool("write_rockspec git://localhost/testrock --lib=fcgi --license=\"3-clause BSD\" " .. "--lua-versions=5.1,5.2")) + assert.is.truthy(lfs.attributes("testrock-dev-1.rockspec")) + -- TODO check contents + end) + end) + + describe("from tarball #mock", function() + + lazy_setup(function() + test_env.setup_specs(nil, "mock") + test_env.mock_server_init() + end) + lazy_teardown(function() + test_env.mock_server_done() + end) + + it("via http", function() + finally(function() os.remove("an_upstream_tarball-0.1-1.rockspec") end) + assert.is_true(run.luarocks_bool("write_rockspec http://localhost:8080/file/an_upstream_tarball-0.1.tar.gz --lua-versions=5.1")) + assert.is.truthy(lfs.attributes("an_upstream_tarball-0.1-1.rockspec")) + -- TODO check contents + end) + + it("with a different basedir", function() + finally(function() os.remove("renamed_upstream_tarball-0.1-1.rockspec") end) + assert.is_true(run.luarocks_bool("write_rockspec http://localhost:8080/file/renamed_upstream_tarball-0.1.tar.gz --lua-versions=5.1")) + assert.is.truthy(lfs.attributes("renamed_upstream_tarball-0.1-1.rockspec")) + -- TODO check contents + end) + end) +end) diff --git a/src/bin/luarocks b/src/bin/luarocks new file mode 100755 index 0000000..56caaa6 --- /dev/null +++ b/src/bin/luarocks @@ -0,0 +1,35 @@ +#!/usr/bin/env lua + +-- Load cfg first so that the loader knows it is running inside LuaRocks +local cfg = require("luarocks.core.cfg") + +local loader = require("luarocks.loader") +local cmd = require("luarocks.cmd") + +local description = "LuaRocks main command-line interface" + +local commands = { + init = "luarocks.cmd.init", + pack = "luarocks.cmd.pack", + unpack = "luarocks.cmd.unpack", + build = "luarocks.cmd.build", + install = "luarocks.cmd.install", + search = "luarocks.cmd.search", + list = "luarocks.cmd.list", + remove = "luarocks.cmd.remove", + make = "luarocks.cmd.make", + download = "luarocks.cmd.download", + path = "luarocks.cmd.path", + show = "luarocks.cmd.show", + new_version = "luarocks.cmd.new_version", + lint = "luarocks.cmd.lint", + write_rockspec = "luarocks.cmd.write_rockspec", + purge = "luarocks.cmd.purge", + doc = "luarocks.cmd.doc", + upload = "luarocks.cmd.upload", + config = "luarocks.cmd.config", + which = "luarocks.cmd.which", + test = "luarocks.cmd.test", +} + +cmd.run_command(description, commands, "luarocks.cmd.external", ...) diff --git a/src/bin/luarocks-admin b/src/bin/luarocks-admin new file mode 100755 index 0000000..4a85e45 --- /dev/null +++ b/src/bin/luarocks-admin @@ -0,0 +1,18 @@ +#!/usr/bin/env lua + +-- Load cfg first so that luarocks.loader knows it is running inside LuaRocks +local cfg = require("luarocks.core.cfg") + +local loader = require("luarocks.loader") +local cmd = require("luarocks.cmd") + +local description = "LuaRocks repository administration interface" + +local commands = { + make_manifest = "luarocks.admin.cmd.make_manifest", + add = "luarocks.admin.cmd.add", + remove = "luarocks.admin.cmd.remove", + refresh_cache = "luarocks.admin.cmd.refresh_cache", +} + +cmd.run_command(description, commands, "luarocks.admin.cmd.external", ...) diff --git a/src/luarocks/admin/cache.lua b/src/luarocks/admin/cache.lua new file mode 100644 index 0000000..7a4e4af --- /dev/null +++ b/src/luarocks/admin/cache.lua @@ -0,0 +1,88 @@ + +--- Module handling the LuaRocks local cache. +-- Adds a rock or rockspec to a rocks server. +local cache = {} + +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +function cache.get_upload_server(server) + if not server then server = cfg.upload_server end + if not server then + return nil, "No server specified and no default configured with upload_server." + end + return server, cfg.upload_servers and cfg.upload_servers[server] +end + +function cache.get_server_urls(server, upload_server) + local download_url = server + local login_url = nil + if upload_server then + if upload_server.rsync then download_url = "rsync://"..upload_server.rsync + elseif upload_server.http then download_url = "http://"..upload_server.http + elseif upload_server.ftp then download_url = "ftp://"..upload_server.ftp + end + + if upload_server.ftp then login_url = "ftp://"..upload_server.ftp + elseif upload_server.sftp then login_url = "sftp://"..upload_server.sftp + end + end + return download_url, login_url +end + +function cache.split_server_url(url, user, password) + local protocol, server_path = dir.split_url(url) + if protocol == "file" then + server_path = fs.absolute_name(server_path) + elseif server_path:match("@") then + local credentials + credentials, server_path = server_path:match("([^@]*)@(.*)") + if credentials:match(":") then + user, password = credentials:match("([^:]*):(.*)") + else + user = credentials + end + end + local local_cache = dir.path(cfg.local_cache, (server_path:gsub("[\\/]", "_"))) + return local_cache, protocol, server_path, user, password +end + +local function download_cache(protocol, server_path, user, password) + os.remove("index.html") + -- TODO abstract away explicit 'wget' call + if protocol == "rsync" then + local srv, path = server_path:match("([^/]+)(/.+)") + return fs.execute(cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." -e ssh "..user.."@"..srv..":"..path.."/ ./") + elseif protocol == "file" then + return fs.copy_contents(server_path, ".") + else + local login_info = "" + if user then login_info = " --user="..user end + if password then login_info = login_info .. " --password="..password end + return fs.execute(cfg.variables.WGET.." --no-cache -q -m -np -nd "..protocol.."://"..server_path..login_info) + end +end + +function cache.refresh_local_cache(url, given_user, given_password) + local local_cache, protocol, server_path, user, password = cache.split_server_url(url, given_user, given_password) + + local ok, err = fs.make_dir(local_cache) + if not ok then + return nil, "Failed creating local cache dir: "..err + end + + fs.change_dir(local_cache) + + util.printout("Refreshing cache "..local_cache.."...") + + ok = download_cache(protocol, server_path, user, password) + if not ok then + return nil, "Failed downloading cache." + end + + return local_cache, protocol, server_path, user, password +end + +return cache diff --git a/src/luarocks/admin/cmd/add.lua b/src/luarocks/admin/cmd/add.lua new file mode 100644 index 0000000..aa444c5 --- /dev/null +++ b/src/luarocks/admin/cmd/add.lua @@ -0,0 +1,134 @@ + +--- Module implementing the luarocks-admin "add" command. +-- Adds a rock or rockspec to a rocks server. +local add = {} + +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local writer = require("luarocks.manif.writer") +local fs = require("luarocks.fs") +local cache = require("luarocks.admin.cache") +local index = require("luarocks.admin.index") + +function add.add_to_parser(parser) + local cmd = parser:command("add", "Add a rock or rockspec to a rocks server.", util.see_also()) + + cmd:argument("rock", "A local rockspec or rock file.") + :args("+") + + cmd:option("--server", "The server to use. If not given, the default server ".. + "set in the upload_server variable from the configuration file is used instead.") + :target("add_server") + cmd:flag("--no-refresh", "Do not refresh the local cache prior to ".. + "generation of the updated manifest.") + cmd:flag("--index", "Produce an index.html file for the manifest. This ".. + "flag is automatically set if an index.html file already exists.") +end + +local function zip_manifests() + for ver in util.lua_versions() do + local file = "manifest-"..ver + local zip = file..".zip" + fs.delete(dir.path(fs.current_dir(), zip)) + fs.zip(zip, file) + end +end + +local function add_files_to_server(refresh, rockfiles, server, upload_server, do_index) + assert(type(refresh) == "boolean" or not refresh) + assert(type(rockfiles) == "table") + assert(type(server) == "string") + assert(type(upload_server) == "table" or not upload_server) + + local download_url, login_url = cache.get_server_urls(server, upload_server) + local at = fs.current_dir() + local refresh_fn = refresh and cache.refresh_local_cache or cache.split_server_url + + local local_cache, protocol, server_path, user, password = refresh_fn(download_url, cfg.upload_user, cfg.upload_password) + if not local_cache then + return nil, protocol + end + + if not login_url then + login_url = protocol.."://"..server_path + end + + local ok, err = fs.change_dir(at) + if not ok then return nil, err end + + local files = {} + for _, rockfile in ipairs(rockfiles) do + if fs.exists(rockfile) then + util.printout("Copying file "..rockfile.." to "..local_cache.."...") + local absolute = fs.absolute_name(rockfile) + fs.copy(absolute, local_cache, "read") + table.insert(files, dir.base_name(absolute)) + else + util.printerr("File "..rockfile.." not found") + end + end + if #files == 0 then + return nil, "No files found" + end + + local ok, err = fs.change_dir(local_cache) + if not ok then return nil, err end + + util.printout("Updating manifest...") + writer.make_manifest(local_cache, "one", true) + + zip_manifests() + + if fs.exists("index.html") then + do_index = true + end + + if do_index then + util.printout("Updating index.html...") + index.make_index(local_cache) + end + + local login_info = "" + if user then login_info = " -u "..user end + if password then login_info = login_info..":"..password end + if not login_url:match("/$") then + login_url = login_url .. "/" + end + + if do_index then + table.insert(files, "index.html") + end + table.insert(files, "manifest") + for ver in util.lua_versions() do + table.insert(files, "manifest-"..ver) + table.insert(files, "manifest-"..ver..".zip") + end + + -- TODO abstract away explicit 'curl' call + + local cmd + if protocol == "rsync" then + local srv, path = server_path:match("([^/]+)(/.+)") + cmd = cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." -e ssh "..local_cache.."/ "..user.."@"..srv..":"..path.."/" + elseif protocol == "file" then + return fs.copy_contents(local_cache, server_path) + elseif upload_server and upload_server.sftp then + local part1, part2 = upload_server.sftp:match("^([^/]*)/(.*)$") + cmd = cfg.variables.SCP.." "..table.concat(files, " ").." "..user.."@"..part1..":/"..part2 + else + cmd = cfg.variables.CURL.." "..login_info.." -T '{"..table.concat(files, ",").."}' "..login_url + end + + util.printout(cmd) + return fs.execute(cmd) +end + +function add.command(args) + local server, server_table = cache.get_upload_server(args.add_server or args.server) + if not server then return nil, server_table end + return add_files_to_server(not args.no_refresh, args.rock, server, server_table, args.index) +end + + +return add diff --git a/src/luarocks/admin/cmd/make_manifest.lua b/src/luarocks/admin/cmd/make_manifest.lua new file mode 100644 index 0000000..18f74b5 --- /dev/null +++ b/src/luarocks/admin/cmd/make_manifest.lua @@ -0,0 +1,50 @@ + +--- Module implementing the luarocks-admin "make_manifest" command. +-- Compile a manifest file for a repository. +local make_manifest = {} + +local writer = require("luarocks.manif.writer") +local index = require("luarocks.admin.index") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local deps = require("luarocks.deps") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") + +function make_manifest.add_to_parser(parser) + local cmd = parser:command("make_manifest", "Compile a manifest file for a repository.", util.see_also()) + + cmd:argument("repository", "Local repository pathname.") + :args("?") + + cmd:flag("--local-tree", "If given, do not write versioned versions of the manifest file.\n".. + "Use this when rebuilding the manifest of a local rocks tree.") + util.deps_mode_option(cmd) +end + +--- Driver function for "make_manifest" command. +-- @return boolean or (nil, string): True if manifest was generated, +-- or nil and an error message. +function make_manifest.command(args) + local repo = args.repository or cfg.rocks_dir + + util.printout("Making manifest for "..repo) + + if repo:match("/lib/luarocks") and not args.local_tree then + util.warning("This looks like a local rocks tree, but you did not pass --local-tree.") + end + + local ok, err = writer.make_manifest(repo, deps.get_deps_mode(args), not args.local_tree) + if ok and not args.local_tree then + util.printout("Generating index.html for "..repo) + index.make_index(repo) + end + if args.local_tree then + for luaver in util.lua_versions() do + fs.delete(dir.path(repo, "manifest-"..luaver)) + end + end + return ok, err +end + +return make_manifest diff --git a/src/luarocks/admin/cmd/refresh_cache.lua b/src/luarocks/admin/cmd/refresh_cache.lua new file mode 100644 index 0000000..f8d5189 --- /dev/null +++ b/src/luarocks/admin/cmd/refresh_cache.lua @@ -0,0 +1,31 @@ + +--- Module implementing the luarocks-admin "refresh_cache" command. +local refresh_cache = {} + +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local cache = require("luarocks.admin.cache") + +function refresh_cache.add_to_parser(parser) + local cmd = parser:command("refresh_cache", "Refresh local cache of a remote rocks server.", util.see_also()) + + cmd:option("--from", "The server to use. If not given, the default server ".. + "set in the upload_server variable from the configuration file is used instead.") + :argname("") +end + +function refresh_cache.command(args) + local server, upload_server = cache.get_upload_server(args.server) + if not server then return nil, upload_server end + local download_url = cache.get_server_urls(server, upload_server) + + local ok, err = cache.refresh_local_cache(download_url, cfg.upload_user, cfg.upload_password) + if not ok then + return nil, err + else + return true + end +end + + +return refresh_cache diff --git a/src/luarocks/admin/cmd/remove.lua b/src/luarocks/admin/cmd/remove.lua new file mode 100644 index 0000000..ed7644e --- /dev/null +++ b/src/luarocks/admin/cmd/remove.lua @@ -0,0 +1,95 @@ + +--- Module implementing the luarocks-admin "remove" command. +-- Removes a rock or rockspec from a rocks server. +local admin_remove = {} + +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local writer = require("luarocks.manif.writer") +local fs = require("luarocks.fs") +local cache = require("luarocks.admin.cache") +local index = require("luarocks.admin.index") + +function admin_remove.add_to_parser(parser) + local cmd = parser:command("remove", "Remove a rock or rockspec from a rocks server.", util.see_also()) + + cmd:argument("rock", "A local rockspec or rock file.") + :args("+") + + cmd:option("--server", "The server to use. If not given, the default server ".. + "set in the upload_server variable from the configuration file is used instead.") + cmd:flag("--no-refresh", "Do not refresh the local cache prior to ".. + "generation of the updated manifest.") +end + +local function remove_files_from_server(refresh, rockfiles, server, upload_server) + assert(type(refresh) == "boolean" or not refresh) + assert(type(rockfiles) == "table") + assert(type(server) == "string") + assert(type(upload_server) == "table" or not upload_server) + + local download_url, login_url = cache.get_server_urls(server, upload_server) + local at = fs.current_dir() + local refresh_fn = refresh and cache.refresh_local_cache or cache.split_server_url + + local local_cache, protocol, server_path, user, password = refresh_fn(download_url, cfg.upload_user, cfg.upload_password) + if not local_cache then + return nil, protocol + end + + local ok, err = fs.change_dir(at) + if not ok then return nil, err end + + local nr_files = 0 + for _, rockfile in ipairs(rockfiles) do + local basename = dir.base_name(rockfile) + local file = dir.path(local_cache, basename) + util.printout("Removing file "..file.."...") + fs.delete(file) + if not fs.exists(file) then + nr_files = nr_files + 1 + else + util.printerr("Failed removing "..file) + end + end + if nr_files == 0 then + return nil, "No files removed." + end + + local ok, err = fs.change_dir(local_cache) + if not ok then return nil, err end + + util.printout("Updating manifest...") + writer.make_manifest(local_cache, "one", true) + util.printout("Updating index.html...") + index.make_index(local_cache) + + if protocol == "file" then + local cmd = cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." --delete "..local_cache.."/ ".. server_path.."/" + util.printout(cmd) + fs.execute(cmd) + return true + end + + if protocol ~= "rsync" then + return nil, "This command requires 'rsync', check your configuration." + end + + local srv, path = server_path:match("([^/]+)(/.+)") + local cmd = cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." --delete -e ssh "..local_cache.."/ "..user.."@"..srv..":"..path.."/" + + util.printout(cmd) + fs.execute(cmd) + + return true +end + +function admin_remove.command(args) + local server, server_table = cache.get_upload_server(args.server) + if not server then return nil, server_table end + return remove_files_from_server(not args.no_refresh, args.rock, server, server_table) +end + + +return admin_remove diff --git a/src/luarocks/admin/index.lua b/src/luarocks/admin/index.lua new file mode 100644 index 0000000..64c8c1e --- /dev/null +++ b/src/luarocks/admin/index.lua @@ -0,0 +1,185 @@ + +--- Module which builds the index.html page to be used in rocks servers. +local index = {} + +local util = require("luarocks.util") +local fs = require("luarocks.fs") +local vers = require("luarocks.core.vers") +local persist = require("luarocks.persist") +local dir = require("luarocks.dir") +local manif = require("luarocks.manif") + +local ext_url_target = ' target="_blank"' + +local index_header = [[ + + + +Available rocks + + + + +

Available rocks

+

+Lua modules available from this location for use with LuaRocks: +

+ +]] + +local index_package_begin = [[ + + + +]] + +local index_footer_begin = [[ +
+

$package - $summary
+

$detailed
+$externaldependencies +latest sources $homepage | License: $license

+
+]] + +local index_package_end = [[ +
+

+manifest file +]] +local index_manifest_ver = [[ +• Lua $VER manifest file (zip) +]] +local index_footer_end = [[ +

+ + +]] + +function index.format_external_dependencies(rockspec) + if rockspec.external_dependencies then + local deplist = {} + local listed_set = {} + local plats = nil + for name, desc in util.sortedpairs(rockspec.external_dependencies) do + if name ~= "platforms" then + table.insert(deplist, name:lower()) + listed_set[name] = true + else + plats = desc + end + end + if plats then + for plat, entries in util.sortedpairs(plats) do + for name, desc in util.sortedpairs(entries) do + if not listed_set[name] then + table.insert(deplist, name:lower() .. " (on "..plat..")") + end + end + end + end + return '

External dependencies: ' .. table.concat(deplist, ', ').. '

' + else + return "" + end +end + +function index.make_index(repo) + if not fs.is_dir(repo) then + return nil, "Cannot access repository at "..repo + end + local manifest = manif.load_manifest(repo) + local out = io.open(dir.path(repo, "index.html"), "w") + + out:write(index_header) + for package, version_list in util.sortedpairs(manifest.repository) do + local latest_rockspec = nil + local output = index_package_begin + for version, data in util.sortedpairs(version_list, vers.compare_versions) do + local versions = {} + output = output..version..': ' + table.sort(data, function(a,b) return a.arch < b.arch end) + for _, item in ipairs(data) do + local file + if item.arch == 'rockspec' then + file = ("%s-%s.rockspec"):format(package, version) + if not latest_rockspec then latest_rockspec = file end + else + file = ("%s-%s.%s.rock"):format(package, version, item.arch) + end + table.insert(versions, ''..item.arch..'') + end + output = output .. table.concat(versions, ', ') .. '
' + end + output = output .. index_package_end + if latest_rockspec then + local rockspec = persist.load_into_table(dir.path(repo, latest_rockspec)) + local descript = rockspec.description or {} + local vars = { + anchor = package, + package = rockspec.package, + original = rockspec.source.url, + summary = descript.summary or "", + detailed = descript.detailed or "", + license = descript.license or "N/A", + homepage = descript.homepage and ('| project homepage') or "", + externaldependencies = index.format_external_dependencies(rockspec) + } + vars.detailed = vars.detailed:gsub("\n\n", "

"):gsub("%s+", " ") + vars.detailed = vars.detailed:gsub("(https?://[a-zA-Z0-9%.%%-_%+%[%]=%?&/$@;:]+)", '%1') + output = output:gsub("$(%w+)", vars) + else + output = output:gsub("$anchor", package) + output = output:gsub("$package", package) + output = output:gsub("$(%w+)", "") + end + out:write(output) + end + out:write(index_footer_begin) + for ver in util.lua_versions() do + out:write((index_manifest_ver:gsub("$VER", ver))) + end + out:write(index_footer_end) + out:close() +end + +return index diff --git a/src/luarocks/build.lua b/src/luarocks/build.lua new file mode 100644 index 0000000..c6b3388 --- /dev/null +++ b/src/luarocks/build.lua @@ -0,0 +1,495 @@ + +local build = {} + +local path = require("luarocks.path") +local util = require("luarocks.util") +local fun = require("luarocks.fun") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local deps = require("luarocks.deps") +local cfg = require("luarocks.core.cfg") +local vers = require("luarocks.core.vers") +local repos = require("luarocks.repos") +local writer = require("luarocks.manif.writer") +local deplocks = require("luarocks.deplocks") + +build.opts = util.opts_table("build.opts", { + need_to_fetch = "boolean", + minimal_mode = "boolean", + deps_mode = "string", + build_only_deps = "boolean", + namespace = "string?", + branch = "string?", + verify = "boolean", + check_lua_versions = "boolean", + pin = "boolean", + rebuild = "boolean", + no_install = "boolean" +}) + +do + --- Write to the current directory the contents of a table, + -- where each key is a file name and its value is the file content. + -- @param files table: The table of files to be written. + local function extract_from_rockspec(files) + for name, content in pairs(files) do + local fd = io.open(dir.path(fs.current_dir(), name), "w+") + fd:write(content) + fd:close() + end + end + + --- Applies patches inlined in the build.patches section + -- and extracts files inlined in the build.extra_files section + -- of a rockspec. + -- @param rockspec table: A rockspec table. + -- @return boolean or (nil, string): True if succeeded or + -- nil and an error message. + function build.apply_patches(rockspec) + assert(rockspec:type() == "rockspec") + + if not (rockspec.build.extra_files or rockspec.build.patches) then + return true + end + + local fd = io.open(fs.absolute_name(".luarocks.patches.applied"), "r") + if fd then + fd:close() + return true + end + + if rockspec.build.extra_files then + extract_from_rockspec(rockspec.build.extra_files) + end + if rockspec.build.patches then + extract_from_rockspec(rockspec.build.patches) + for patch, patchdata in util.sortedpairs(rockspec.build.patches) do + util.printout("Applying patch "..patch.."...") + local create_delete = rockspec:format_is_at_least("3.0") + local ok, err = fs.apply_patch(tostring(patch), patchdata, create_delete) + if not ok then + return nil, "Failed applying patch "..patch + end + end + end + + fd = io.open(fs.absolute_name(".luarocks.patches.applied"), "w") + if fd then + fd:close() + end + return true + end +end + +local function check_macosx_deployment_target(rockspec) + local target = rockspec.build.macosx_deployment_target + local function patch_variable(var) + if rockspec.variables[var]:match("MACOSX_DEPLOYMENT_TARGET") then + rockspec.variables[var] = (rockspec.variables[var]):gsub("MACOSX_DEPLOYMENT_TARGET=[^ ]*", "MACOSX_DEPLOYMENT_TARGET="..target) + else + rockspec.variables[var] = "env MACOSX_DEPLOYMENT_TARGET="..target.." "..rockspec.variables[var] + end + end + if cfg.is_platform("macosx") and rockspec:format_is_at_least("3.0") and target then + local version = util.popen_read("sw_vers -productVersion") + if version:match("^%d+%.%d+%.%d+$") or version:match("^%d+%.%d+$") then + if vers.compare_versions(target, version) then + return nil, ("This rock requires Mac OSX %s, and you are running %s."):format(target, version) + end + end + patch_variable("CC") + patch_variable("LD") + end + return true +end + +local function process_dependencies(rockspec, opts) + if not opts.build_only_deps then + local ok, err, errcode = deps.check_external_deps(rockspec, "build") + if err then + return nil, err, errcode + end + end + + if opts.deps_mode == "none" then + return true + end + + if not opts.build_only_deps then + if next(rockspec.build_dependencies) then + + local user_lua_version = cfg.lua_version + local running_lua_version = _VERSION:sub(5) + + if running_lua_version ~= user_lua_version then + -- Temporarily flip the user-selected Lua version, + -- so that we install build dependencies for the + -- Lua version on which the LuaRocks program is running. + + -- HACK: we have to do this by flipping a bunch of + -- global config settings, and this list may not be complete. + cfg.lua_version = running_lua_version + cfg.lua_modules_path = cfg.lua_modules_path:gsub(user_lua_version:gsub("%.", "%%."), running_lua_version) + cfg.lib_modules_path = cfg.lib_modules_path:gsub(user_lua_version:gsub("%.", "%%."), running_lua_version) + cfg.rocks_subdir = cfg.rocks_subdir:gsub(user_lua_version:gsub("%.", "%%."), running_lua_version) + path.use_tree(cfg.root_dir) + end + + local ok, err, errcode = deps.fulfill_dependencies(rockspec, "build_dependencies", "all", opts.verify) + + path.add_to_package_paths(cfg.root_dir) + + if running_lua_version ~= user_lua_version then + -- flip the settings back + cfg.lua_version = user_lua_version + cfg.lua_modules_path = cfg.lua_modules_path:gsub(running_lua_version:gsub("%.", "%%."), user_lua_version) + cfg.lib_modules_path = cfg.lib_modules_path:gsub(running_lua_version:gsub("%.", "%%."), user_lua_version) + cfg.rocks_subdir = cfg.rocks_subdir:gsub(running_lua_version:gsub("%.", "%%."), user_lua_version) + path.use_tree(cfg.root_dir) + end + + if err then + return nil, err, errcode + end + end + end + + return deps.fulfill_dependencies(rockspec, "dependencies", opts.deps_mode, opts.verify) +end + +local function fetch_and_change_to_source_dir(rockspec, opts) + if opts.minimal_mode or opts.build_only_deps then + return true + end + if opts.need_to_fetch then + if opts.branch then + rockspec.source.branch = opts.branch + end + local ok, source_dir, errcode = fetch.fetch_sources(rockspec, true) + if not ok then + return nil, source_dir, errcode + end + local err + ok, err = fs.change_dir(source_dir) + if not ok then + return nil, err + end + else + if rockspec.source.file then + local ok, err = fs.unpack_archive(rockspec.source.file) + if not ok then + return nil, err + end + end + local ok, err = fetch.find_rockspec_source_dir(rockspec, ".") + if not ok then + return nil, err + end + end + fs.change_dir(rockspec.source.dir) + return true +end + +local function prepare_install_dirs(name, version) + local dirs = { + lua = { name = path.lua_dir(name, version), is_module_path = true, perms = "read" }, + lib = { name = path.lib_dir(name, version), is_module_path = true, perms = "exec" }, + bin = { name = path.bin_dir(name, version), is_module_path = false, perms = "exec" }, + conf = { name = path.conf_dir(name, version), is_module_path = false, perms = "read" }, + } + + for _, d in pairs(dirs) do + local ok, err = fs.make_dir(d.name) + if not ok then + return nil, err + end + end + + return dirs +end + +local function run_build_driver(rockspec, no_install) + local btype = rockspec.build.type + if btype == "none" then + return true + end + -- Temporary compatibility + if btype == "module" then + util.printout("Do not use 'module' as a build type. Use 'builtin' instead.") + btype = "builtin" + rockspec.build.type = btype + end + if cfg.accepted_build_types and not fun.contains(cfg.accepted_build_types, btype) then + return nil, "This rockspec uses the '"..btype.."' build type, which is blocked by the 'accepted_build_types' setting in your LuaRocks configuration." + end + local pok, driver = pcall(require, "luarocks.build." .. btype) + if not pok or type(driver) ~= "table" then + return nil, "Failed initializing build back-end for build type '"..btype.."': "..driver + end + + if not driver.skip_lua_inc_lib_check then + local ok, err, errcode = deps.check_lua_incdir(rockspec.variables) + if not ok then + return nil, err, errcode + end + + if cfg.link_lua_explicitly then + local ok, err, errcode = deps.check_lua_libdir(rockspec.variables) + if not ok then + return nil, err, errcode + end + end + end + + local ok, err = driver.run(rockspec, no_install) + if not ok then + return nil, "Build error: " .. err + end + return true +end + +local install_files +do + --- Install files to a given location. + -- Takes a table where the array part is a list of filenames to be copied. + -- In the hash part, other keys, if is_module_path is set, are identifiers + -- in Lua module format, to indicate which subdirectory the file should be + -- copied to. For example, install_files({["foo.bar"] = "src/bar.lua"}, "boo") + -- will copy src/bar.lua to boo/foo. + -- @param files table or nil: A table containing a list of files to copy in + -- the format described above. If nil is passed, this function is a no-op. + -- Directories should be delimited by forward slashes as in internet URLs. + -- @param location string: The base directory files should be copied to. + -- @param is_module_path boolean: True if string keys in files should be + -- interpreted as dotted module paths. + -- @param perms string ("read" or "exec"): Permissions of the newly created + -- files installed. + -- Directories are always created with the default permissions. + -- @return boolean or (nil, string): True if succeeded or + -- nil and an error message. + local function install_to(files, location, is_module_path, perms) + assert(type(files) == "table" or not files) + assert(type(location) == "string") + if not files then + return true + end + for k, file in pairs(files) do + local dest = location + local filename = dir.base_name(file) + if type(k) == "string" then + local modname = k + if is_module_path then + dest = dir.path(location, path.module_to_path(modname)) + local ok, err = fs.make_dir(dest) + if not ok then return nil, err end + if filename:match("%.lua$") then + local basename = modname:match("([^.]+)$") + filename = basename..".lua" + end + else + dest = dir.path(location, dir.dir_name(modname)) + local ok, err = fs.make_dir(dest) + if not ok then return nil, err end + filename = dir.base_name(modname) + end + else + local ok, err = fs.make_dir(dest) + if not ok then return nil, err end + end + local ok = fs.copy(file, dir.path(dest, filename), perms) + if not ok then + return nil, "Failed copying "..file + end + end + return true + end + + local function install_default_docs(name, version) + local patterns = { "readme", "license", "copying", ".*%.md" } + local dest = dir.path(path.install_dir(name, version), "doc") + local has_dir = false + for file in fs.dir() do + for _, pattern in ipairs(patterns) do + if file:lower():match("^"..pattern) then + if not has_dir then + fs.make_dir(dest) + has_dir = true + end + fs.copy(file, dest, "read") + break + end + end + end + end + + install_files = function(rockspec, dirs) + local name, version = rockspec.name, rockspec.version + + if rockspec.build.install then + for k, d in pairs(dirs) do + local ok, err = install_to(rockspec.build.install[k], d.name, d.is_module_path, d.perms) + if not ok then return nil, err end + end + end + + local copy_directories = rockspec.build.copy_directories + local copying_default = false + if not copy_directories then + copy_directories = {"doc"} + copying_default = true + end + + local any_docs = false + for _, copy_dir in pairs(copy_directories) do + if fs.is_dir(copy_dir) then + local dest = dir.path(path.install_dir(name, version), copy_dir) + fs.make_dir(dest) + fs.copy_contents(copy_dir, dest) + any_docs = true + else + if not copying_default then + return nil, "Directory '"..copy_dir.."' not found" + end + end + end + if not any_docs then + install_default_docs(name, version) + end + + return true + end +end + +local function write_rock_dir_files(rockspec, opts) + local name, version = rockspec.name, rockspec.version + + fs.copy(rockspec.local_abs_filename, path.rockspec_file(name, version), "read") + + local deplock_file = deplocks.get_abs_filename(rockspec.name) + if deplock_file then + fs.copy(deplock_file, dir.path(path.install_dir(name, version), "luarocks.lock"), "read") + end + + local ok, err = writer.make_rock_manifest(name, version) + if not ok then return nil, err end + + ok, err = writer.make_namespace_file(name, version, opts.namespace) + if not ok then return nil, err end + + return true +end + +--- Build and install a rock given a rockspec. +-- @param opts table: build options table +-- @return (string, string) or (nil, string, [string]): Name and version of +-- installed rock if succeeded or nil and an error message followed by an error code. +function build.build_rockspec(rockspec, opts) + assert(rockspec:type() == "rockspec") + assert(opts:type() == "build.opts") + + if not rockspec.build then + if rockspec:format_is_at_least("3.0") then + rockspec.build = { + type = "builtin" + } + else + return nil, "Rockspec error: build table not specified" + end + end + + if not rockspec.build.type then + if rockspec:format_is_at_least("3.0") then + rockspec.build.type = "builtin" + else + return nil, "Rockspec error: build type not specified" + end + end + + local ok, err = fetch_and_change_to_source_dir(rockspec, opts) + if not ok then return nil, err end + + if opts.pin then + deplocks.init(rockspec.name, ".") + end + + ok, err = process_dependencies(rockspec, opts) + if not ok then return nil, err end + + local name, version = rockspec.name, rockspec.version + if opts.build_only_deps then + if opts.pin then + deplocks.write_file() + end + return name, version + end + + local dirs, err + local rollback + if not opts.no_install then + if repos.is_installed(name, version) then + repos.delete_version(name, version, opts.deps_mode) + end + + dirs, err = prepare_install_dirs(name, version) + if not dirs then return nil, err end + + rollback = util.schedule_function(function() + fs.delete(path.install_dir(name, version)) + fs.remove_dir_if_empty(path.versions_dir(name)) + end) + end + + ok, err = build.apply_patches(rockspec) + if not ok then return nil, err end + + ok, err = check_macosx_deployment_target(rockspec) + if not ok then return nil, err end + + ok, err = run_build_driver(rockspec, opts.no_install) + if not ok then return nil, err end + + if opts.no_install then + fs.pop_dir() + if opts.need_to_fetch then + fs.pop_dir() + end + return name, version + end + + ok, err = install_files(rockspec, dirs) + if not ok then return nil, err end + + for _, d in pairs(dirs) do + fs.remove_dir_if_empty(d.name) + end + + fs.pop_dir() + if opts.need_to_fetch then + fs.pop_dir() + end + + if opts.pin then + deplocks.write_file() + end + + ok, err = write_rock_dir_files(rockspec, opts) + if not ok then return nil, err end + + ok, err = repos.deploy_files(name, version, repos.should_wrap_bin_scripts(rockspec), opts.deps_mode) + if not ok then return nil, err end + + util.remove_scheduled_function(rollback) + rollback = util.schedule_function(function() + repos.delete_version(name, version, opts.deps_mode) + end) + + ok, err = repos.run_hook(rockspec, "post_install") + if not ok then return nil, err end + + util.announce_install(rockspec) + util.remove_scheduled_function(rollback) + return name, version +end + +return build diff --git a/src/luarocks/build/builtin.lua b/src/luarocks/build/builtin.lua new file mode 100644 index 0000000..4c15d2b --- /dev/null +++ b/src/luarocks/build/builtin.lua @@ -0,0 +1,395 @@ + +--- A builtin build system: back-end to provide a portable way of building C-based Lua modules. +local builtin = {} + +-- This build driver checks LUA_INCDIR and LUA_LIBDIR on demand, +-- so that pure-Lua rocks don't need to have development headers +-- installed. +builtin.skip_lua_inc_lib_check = true + +local unpack = unpack or table.unpack +local dir_sep = package.config:sub(1, 1) + +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local deps = require("luarocks.deps") + +local function autoextract_libs(external_dependencies, variables) + if not external_dependencies then + return nil, nil, nil + end + local libs = {} + local incdirs = {} + local libdirs = {} + for name, data in pairs(external_dependencies) do + if data.library then + table.insert(libs, data.library) + table.insert(incdirs, variables[name .. "_INCDIR"]) + table.insert(libdirs, variables[name .. "_LIBDIR"]) + end + end + return libs, incdirs, libdirs +end + +do + local function get_cmod_name(file) + local fd = io.open(dir.path(fs.current_dir(), file), "r") + if not fd then return nil end + local data = fd:read("*a") + fd:close() + return (data:match("int%s+luaopen_([a-zA-Z0-9_]+)")) + end + + local skiplist = { + ["spec"] = true, + [".luarocks"] = true, + ["lua_modules"] = true, + ["test.lua"] = true, + ["tests.lua"] = true, + } + + function builtin.autodetect_modules(libs, incdirs, libdirs) + local modules = {} + local install + local copy_directories + + local prefix = "" + for _, parent in ipairs({"src", "lua", "lib"}) do + if fs.is_dir(parent) then + fs.change_dir(parent) + prefix = parent .. dir_sep + break + end + end + + for _, file in ipairs(fs.find()) do + local base = file:match("^([^\\/]*)") + if not skiplist[base] then + local luamod = file:match("(.*)%.lua$") + if luamod then + modules[path.path_to_module(file)] = prefix .. file + else + local cmod = file:match("(.*)%.c$") + if cmod then + local modname = get_cmod_name(file) or path.path_to_module(file:gsub("%.c$", ".lua")) + modules[modname] = { + sources = prefix..file, + libraries = libs, + incdirs = incdirs, + libdirs = libdirs, + } + end + end + end + end + + if prefix ~= "" then + fs.pop_dir() + end + + local bindir = (fs.is_dir(dir.path("src", "bin")) and dir.path("src", "bin")) + or (fs.is_dir("bin") and "bin") + if bindir then + install = { bin = {} } + for _, file in ipairs(fs.list_dir(bindir)) do + table.insert(install.bin, dir.path(bindir, file)) + end + end + + for _, directory in ipairs({ "doc", "docs", "samples", "tests" }) do + if fs.is_dir(directory) then + if not copy_directories then + copy_directories = {} + end + table.insert(copy_directories, directory) + end + end + + return modules, install, copy_directories + end +end + +--- Run a command displaying its execution on standard output. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +local function execute(...) + io.stdout:write(table.concat({...}, " ").."\n") + return fs.execute(...) +end + +--- Driver function for the builtin build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors occurred, +-- nil and an error message otherwise. +function builtin.run(rockspec, no_install) + assert(rockspec:type() == "rockspec") + local compile_object, compile_library, compile_static_library + + local build = rockspec.build + local variables = rockspec.variables + local checked_lua_h = false + + for _, var in ipairs{ "CC", "CFLAGS", "LDFLAGS" } do + variables[var] = variables[var] or os.getenv(var) or "" + end + + local function add_flags(extras, flag, flags) + if flags then + if type(flags) ~= "table" then + flags = { tostring(flags) } + end + util.variable_substitutions(flags, variables) + for _, v in ipairs(flags) do + table.insert(extras, flag:format(v)) + end + end + end + + if cfg.is_platform("mingw32") then + compile_object = function(object, source, defines, incdirs) + local extras = {} + add_flags(extras, "-D%s", defines) + add_flags(extras, "-I%s", incdirs) + return execute(variables.CC.." "..variables.CFLAGS, "-c", "-o", object, "-I"..variables.LUA_INCDIR, source, unpack(extras)) + end + compile_library = function(library, objects, libraries, libdirs, name) + local extras = { unpack(objects) } + add_flags(extras, "-L%s", libdirs) + add_flags(extras, "-l%s", libraries) + extras[#extras+1] = dir.path(variables.LUA_LIBDIR, variables.LUALIB) + + if variables.CC == "clang" or variables.CC == "clang-cl" then + local exported_name = name:gsub("%.", "_") + exported_name = exported_name:match('^[^%-]+%-(.+)$') or exported_name + extras[#extras+1] = string.format("-Wl,-export:luaopen_%s", exported_name) + else + extras[#extras+1] = "-l" .. (variables.MSVCRT or "m") + end + + local ok = execute(variables.LD.." "..variables.LDFLAGS.." "..variables.LIBFLAG, "-o", library, unpack(extras)) + return ok + end + --[[ TODO disable static libs until we fix the conflict in the manifest, which will take extending the manifest format. + compile_static_library = function(library, objects, libraries, libdirs, name) + local ok = execute(variables.AR, "rc", library, unpack(objects)) + if ok then + ok = execute(variables.RANLIB, library) + end + return ok + end + ]] + elseif cfg.is_platform("win32") then + compile_object = function(object, source, defines, incdirs) + local extras = {} + add_flags(extras, "-D%s", defines) + add_flags(extras, "-I%s", incdirs) + return execute(variables.CC.." "..variables.CFLAGS, "-c", "-Fo"..object, "-I"..variables.LUA_INCDIR, source, unpack(extras)) + end + compile_library = function(library, objects, libraries, libdirs, name) + local extras = { unpack(objects) } + add_flags(extras, "-libpath:%s", libdirs) + add_flags(extras, "%s.lib", libraries) + local basename = dir.base_name(library):gsub(".[^.]*$", "") + local deffile = basename .. ".def" + local def = io.open(dir.path(fs.current_dir(), deffile), "w+") + local exported_name = name:gsub("%.", "_") + exported_name = exported_name:match('^[^%-]+%-(.+)$') or exported_name + def:write("EXPORTS\n") + def:write("luaopen_"..exported_name.."\n") + def:close() + local ok = execute(variables.LD, "-dll", "-def:"..deffile, "-out:"..library, dir.path(variables.LUA_LIBDIR, variables.LUALIB), unpack(extras)) + local basedir = "" + if name:find("%.") ~= nil then + basedir = name:gsub("%.%w+$", "\\") + basedir = basedir:gsub("%.", "\\") + end + local manifestfile = basedir .. basename..".dll.manifest" + + if ok and fs.exists(manifestfile) then + ok = execute(variables.MT, "-manifest", manifestfile, "-outputresource:"..basedir..basename..".dll;2") + end + return ok + end + --[[ TODO disable static libs until we fix the conflict in the manifest, which will take extending the manifest format. + compile_static_library = function(library, objects, libraries, libdirs, name) + local ok = execute(variables.AR, "-out:"..library, unpack(objects)) + return ok + end + ]] + else + compile_object = function(object, source, defines, incdirs) + local extras = {} + add_flags(extras, "-D%s", defines) + add_flags(extras, "-I%s", incdirs) + return execute(variables.CC.." "..variables.CFLAGS, "-I"..variables.LUA_INCDIR, "-c", source, "-o", object, unpack(extras)) + end + compile_library = function (library, objects, libraries, libdirs) + local extras = { unpack(objects) } + add_flags(extras, "-L%s", libdirs) + if cfg.gcc_rpath then + add_flags(extras, "-Wl,-rpath,%s", libdirs) + end + add_flags(extras, "-l%s", libraries) + if cfg.link_lua_explicitly then + extras[#extras+1] = "-L"..variables.LUA_LIBDIR + extras[#extras+1] = "-llua" + end + return execute(variables.LD.." "..variables.LDFLAGS.." "..variables.LIBFLAG, "-o", library, unpack(extras)) + end + compile_static_library = function(library, objects, libraries, libdirs, name) -- luacheck: ignore 211 + local ok = execute(variables.AR, "rc", library, unpack(objects)) + if ok then + ok = execute(variables.RANLIB, library) + end + return ok + end + end + + local ok, err + local lua_modules = {} + local lib_modules = {} + local luadir = path.lua_dir(rockspec.name, rockspec.version) + local libdir = path.lib_dir(rockspec.name, rockspec.version) + + local autolibs, autoincdirs, autolibdirs = autoextract_libs(rockspec.external_dependencies, rockspec.variables) + + if not build.modules then + if rockspec:format_is_at_least("3.0") then + local install, copy_directories + build.modules, install, copy_directories = builtin.autodetect_modules(autolibs, autoincdirs, autolibdirs) + build.install = build.install or install + build.copy_directories = build.copy_directories or copy_directories + else + return nil, "Missing build.modules table" + end + end + + local compile_temp_dir + + local mkdir_cache = {} + local function cached_make_dir(name) + if name == "" or mkdir_cache[name] then + return true + end + mkdir_cache[name] = true + return fs.make_dir(name) + end + + for name, info in pairs(build.modules) do + local moddir = path.module_to_path(name) + if type(info) == "string" then + local ext = info:match("%.([^.]+)$") + if ext == "lua" then + local filename = dir.base_name(info) + if filename == "init.lua" and not name:match("%.init$") then + moddir = path.module_to_path(name..".init") + else + local basename = name:match("([^.]+)$") + filename = basename..".lua" + end + local dest = dir.path(luadir, moddir, filename) + lua_modules[info] = dest + else + info = {info} + end + end + if type(info) == "table" then + if not checked_lua_h then + local ok, err, errcode = deps.check_lua_incdir(rockspec.variables) + if not ok then + return nil, err, errcode + end + + if cfg.link_lua_explicitly then + local ok, err, errcode = deps.check_lua_libdir(rockspec.variables) + if not ok then + return nil, err, errcode + end + end + checked_lua_h = true + end + local objects = {} + local sources = info.sources + if info[1] then sources = info end + if type(sources) == "string" then sources = {sources} end + if type(sources) ~= "table" then + return nil, "error in rockspec: module '" .. name .. "' entry has no 'sources' list" + end + for _, source in ipairs(sources) do + if type(source) ~= "string" then + return nil, "error in rockspec: module '" .. name .. "' does not specify source correctly." + end + local object = source:gsub("%.[^.]*$", "."..cfg.obj_extension) + if not object then + object = source.."."..cfg.obj_extension + end + ok = compile_object(object, source, info.defines, info.incdirs or autoincdirs) + if not ok then + return nil, "Failed compiling object "..object + end + table.insert(objects, object) + end + + if not compile_temp_dir then + compile_temp_dir = fs.make_temp_dir("build-" .. rockspec.package .. "-" .. rockspec.version) + util.schedule_function(fs.delete, compile_temp_dir) + end + + local module_name = name:match("([^.]*)$").."."..util.matchquote(cfg.lib_extension) + if moddir ~= "" then + module_name = dir.path(moddir, module_name) + end + + local build_name = dir.path(compile_temp_dir, module_name) + local build_dir = dir.dir_name(build_name) + cached_make_dir(build_dir) + + lib_modules[build_name] = dir.path(libdir, module_name) + ok = compile_library(build_name, objects, info.libraries, info.libdirs or autolibdirs, name) + if not ok then + return nil, "Failed compiling module "..module_name + end + + -- for backwards compatibility, try keeping a copy of the module + -- in the old location (luasec-1.3.2-1 rockspec breaks otherwise) + if cached_make_dir(dir.dir_name(module_name)) then + fs.copy(build_name, module_name) + end + + --[[ TODO disable static libs until we fix the conflict in the manifest, which will take extending the manifest format. + module_name = name:match("([^.]*)$").."."..util.matchquote(cfg.static_lib_extension) + if moddir ~= "" then + module_name = dir.path(moddir, module_name) + end + lib_modules[module_name] = dir.path(libdir, module_name) + ok = compile_static_library(module_name, objects, info.libraries, info.libdirs, name) + if not ok then + return nil, "Failed compiling static library "..module_name + end + ]] + end + end + if not no_install then + for _, mods in ipairs({{ tbl = lua_modules, perms = "read" }, { tbl = lib_modules, perms = "exec" }}) do + for name, dest in pairs(mods.tbl) do + cached_make_dir(dir.dir_name(dest)) + ok, err = fs.copy(name, dest, mods.perms) + if not ok then + return nil, "Failed installing "..name.." in "..dest..": "..err + end + end + end + if fs.is_dir("lua") then + ok, err = fs.copy_contents("lua", luadir) + if not ok then + return nil, "Failed copying contents of 'lua' directory: "..err + end + end + end + return true +end + +return builtin diff --git a/src/luarocks/build/cmake.lua b/src/luarocks/build/cmake.lua new file mode 100644 index 0000000..b7a4786 --- /dev/null +++ b/src/luarocks/build/cmake.lua @@ -0,0 +1,78 @@ + +--- Build back-end for CMake-based modules. +local cmake = {} + +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + +--- Driver function for the "cmake" build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors occurred, +-- nil and an error message otherwise. +function cmake.run(rockspec, no_install) + assert(rockspec:type() == "rockspec") + local build = rockspec.build + local variables = build.variables or {} + + -- Pass Env variables + variables.CMAKE_MODULE_PATH=os.getenv("CMAKE_MODULE_PATH") + variables.CMAKE_LIBRARY_PATH=os.getenv("CMAKE_LIBRARY_PATH") + variables.CMAKE_INCLUDE_PATH=os.getenv("CMAKE_INCLUDE_PATH") + + util.variable_substitutions(variables, rockspec.variables) + + local ok, err_msg = fs.is_tool_available(rockspec.variables.CMAKE, "CMake") + if not ok then + return nil, err_msg + end + + -- If inline cmake is present create CMakeLists.txt from it. + if type(build.cmake) == "string" then + local cmake_handler = assert(io.open(fs.current_dir().."/CMakeLists.txt", "w")) + cmake_handler:write(build.cmake) + cmake_handler:close() + end + + -- Execute cmake with variables. + local args = "" + + -- Try to pick the best generator. With msvc and x64, CMake does not select it by default so we need to be explicit. + if cfg.cmake_generator then + args = args .. ' -G"'..cfg.cmake_generator.. '"' + elseif cfg.is_platform("windows") and cfg.target_cpu:match("x86_64$") then + args = args .. " -DCMAKE_GENERATOR_PLATFORM=x64" + end + + for k,v in pairs(variables) do + args = args .. ' -D' ..k.. '="' ..tostring(v).. '"' + end + + if not fs.execute_string(rockspec.variables.CMAKE.." -H. -Bbuild.luarocks "..args) then + return nil, "Failed cmake." + end + + local do_build, do_install + if rockspec:format_is_at_least("3.0") then + do_build = (build.build_pass == nil) and true or build.build_pass + do_install = (build.install_pass == nil) and true or build.install_pass + else + do_build = true + do_install = true + end + + if do_build then + if not fs.execute_string(rockspec.variables.CMAKE.." --build build.luarocks --config Release") then + return nil, "Failed building." + end + end + if do_install and not no_install then + if not fs.execute_string(rockspec.variables.CMAKE.." --build build.luarocks --target install --config Release") then + return nil, "Failed installing." + end + end + + return true +end + +return cmake diff --git a/src/luarocks/build/command.lua b/src/luarocks/build/command.lua new file mode 100644 index 0000000..b0c4aa7 --- /dev/null +++ b/src/luarocks/build/command.lua @@ -0,0 +1,41 @@ + +--- Build back-end for raw listing of commands in rockspec files. +local command = {} + +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + +--- Driver function for the "command" build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors occurred, +-- nil and an error message otherwise. +function command.run(rockspec, not_install) + assert(rockspec:type() == "rockspec") + + local build = rockspec.build + + util.variable_substitutions(build, rockspec.variables) + + local env = { + CC = cfg.variables.CC, + --LD = cfg.variables.LD, + --CFLAGS = cfg.variables.CFLAGS, + } + + if build.build_command then + util.printout(build.build_command) + if not fs.execute_env(env, build.build_command) then + return nil, "Failed building." + end + end + if build.install_command and not not_install then + util.printout(build.install_command) + if not fs.execute_env(env, build.install_command) then + return nil, "Failed installing." + end + end + return true +end + +return command diff --git a/src/luarocks/build/make.lua b/src/luarocks/build/make.lua new file mode 100644 index 0000000..4345ddf --- /dev/null +++ b/src/luarocks/build/make.lua @@ -0,0 +1,98 @@ + +--- Build back-end for using Makefile-based packages. +local make = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + +--- Call "make" with given target and variables +-- @param make_cmd string: the make command to be used (typically +-- configured through variables.MAKE in the config files, or +-- the appropriate platform-specific default). +-- @param pass boolean: If true, run make; if false, do nothing. +-- @param target string: The make target; an empty string indicates +-- the default target. +-- @param variables table: A table containing string-string key-value +-- pairs representing variable assignments to be passed to make. +-- @return boolean: false if any errors occurred, true otherwise. +local function make_pass(make_cmd, pass, target, variables) + assert(type(pass) == "boolean") + assert(type(target) == "string") + assert(type(variables) == "table") + + local assignments = {} + for k,v in pairs(variables) do + table.insert(assignments, k.."="..v) + end + if pass then + return fs.execute(make_cmd.." "..target, unpack(assignments)) + else + return true + end +end + +--- Driver function for the "make" build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors occurred, +-- nil and an error message otherwise. +function make.run(rockspec, not_install) + assert(rockspec:type() == "rockspec") + + local build = rockspec.build + + if build.build_pass == nil then build.build_pass = true end + if build.install_pass == nil then build.install_pass = true end + build.build_variables = build.build_variables or {} + build.install_variables = build.install_variables or {} + build.build_target = build.build_target or "" + build.install_target = build.install_target or "install" + local makefile = build.makefile or cfg.makefile + if makefile then + -- Assumes all make's accept -f. True for POSIX make, GNU make and Microsoft nmake. + build.build_target = "-f "..makefile.." "..build.build_target + build.install_target = "-f "..makefile.." "..build.install_target + end + + if build.variables then + for var, val in pairs(build.variables) do + build.build_variables[var] = val + build.install_variables[var] = val + end + end + + util.warn_if_not_used(build.build_variables, { CFLAGS=true }, "variable %s was not passed in build_variables") + + util.variable_substitutions(build.build_variables, rockspec.variables) + util.variable_substitutions(build.install_variables, rockspec.variables) + + local auto_variables = { "CC" } + + for _, variable in pairs(auto_variables) do + if not build.build_variables[variable] then + build.build_variables[variable] = rockspec.variables[variable] + end + if not build.install_variables[variable] then + build.install_variables[variable] = rockspec.variables[variable] + end + end + + -- backwards compatibility + local make_cmd = cfg.make or rockspec.variables.MAKE + + local ok = make_pass(make_cmd, build.build_pass, build.build_target, build.build_variables) + if not ok then + return nil, "Failed building." + end + if not not_install then + ok = make_pass(make_cmd, build.install_pass, build.install_target, build.install_variables) + if not ok then + return nil, "Failed installing." + end + end + return true +end + +return make diff --git a/src/luarocks/cmd.lua b/src/luarocks/cmd.lua new file mode 100644 index 0000000..7e0abe5 --- /dev/null +++ b/src/luarocks/cmd.lua @@ -0,0 +1,781 @@ + +--- Functions for command-line scripts. +local cmd = {} + +local manif = require("luarocks.manif") +local config = require("luarocks.config") +local util = require("luarocks.util") +local path = require("luarocks.path") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local fun = require("luarocks.fun") +local fs = require("luarocks.fs") +local argparse = require("luarocks.vendor.argparse") + +local unpack = table.unpack or unpack +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +local hc_ok, hardcoded = pcall(require, "luarocks.core.hardcoded") +if not hc_ok then + hardcoded = {} +end + +local program = util.this_program("luarocks") + +cmd.errorcodes = { + OK = 0, + UNSPECIFIED = 1, + PERMISSIONDENIED = 2, + CONFIGFILE = 3, + LOCK = 4, + CRASH = 99 +} + +local function check_popen() + local popen_ok, popen_result = pcall(io.popen, "") + if popen_ok then + if popen_result then + popen_result:close() + end + else + io.stderr:write("Your version of Lua does not support io.popen,\n") + io.stderr:write("which is required by LuaRocks. Please check your Lua installation.\n") + os.exit(cmd.errorcodes.UNSPECIFIED) + end +end + +local process_tree_args +do + local function replace_tree(args, root, tree) + root = dir.normalize(root) + args.tree = root + path.use_tree(tree or root) + end + + local function strip_trailing_slashes() + if type(cfg.root_dir) == "string" then + cfg.root_dir = cfg.root_dir:gsub("/+$", "") + else + cfg.root_dir.root = cfg.root_dir.root:gsub("/+$", "") + end + cfg.rocks_dir = cfg.rocks_dir:gsub("/+$", "") + cfg.deploy_bin_dir = cfg.deploy_bin_dir:gsub("/+$", "") + cfg.deploy_lua_dir = cfg.deploy_lua_dir:gsub("/+$", "") + cfg.deploy_lib_dir = cfg.deploy_lib_dir:gsub("/+$", "") + end + + local function set_named_tree(args, name) + for _, tree in ipairs(cfg.rocks_trees) do + if type(tree) == "table" and name == tree.name then + if not tree.root then + return nil, "Configuration error: tree '"..tree.name.."' has no 'root' field." + end + replace_tree(args, tree.root, tree) + return true + end + end + return false + end + + process_tree_args = function(args, project_dir) + + if args.global then + local ok, err = set_named_tree(args, "system") + if not ok then + return nil, err + end + elseif args.tree then + local named = set_named_tree(args, args.tree) + if not named then + local root_dir = fs.absolute_name(args.tree) + replace_tree(args, root_dir) + if (args.deps_mode or cfg.deps_mode) ~= "order" then + table.insert(cfg.rocks_trees, 1, { name = "arg", root = root_dir } ) + end + end + elseif args["local"] then + if fs.is_superuser() then + return nil, "The --local flag is meant for operating in a user's home directory.\n".. + "You are running as a superuser, which is intended for system-wide operation.\n".. + "To force using the superuser's home, use --tree explicitly." + else + local ok, err = set_named_tree(args, "user") + if not ok then + return nil, err + end + end + elseif args.project_tree then + local tree = args.project_tree + table.insert(cfg.rocks_trees, 1, { name = "project", root = tree } ) + manif.load_rocks_tree_manifests() + path.use_tree(tree) + elseif cfg.local_by_default then + local ok, err = set_named_tree(args, "user") + if not ok then + return nil, err + end + elseif project_dir then + local project_tree = project_dir .. "/lua_modules" + table.insert(cfg.rocks_trees, 1, { name = "project", root = project_tree } ) + manif.load_rocks_tree_manifests() + path.use_tree(project_tree) + else + local trees = cfg.rocks_trees + path.use_tree(trees[#trees]) + end + + strip_trailing_slashes() + + cfg.variables.ROCKS_TREE = cfg.rocks_dir + cfg.variables.SCRIPTS_DIR = cfg.deploy_bin_dir + + return true + end +end + +local function process_server_args(args) + if args.server then + local protocol, pathname = dir.split_url(args.server) + table.insert(cfg.rocks_servers, 1, protocol.."://"..pathname) + end + + if args.dev then + local append_dev = function(s) return dir.path(s, "dev") end + local dev_servers = fun.traverse(cfg.rocks_servers, append_dev) + cfg.rocks_servers = fun.concat(dev_servers, cfg.rocks_servers) + end + + if args.only_server then + if args.dev then + return nil, "--only-server cannot be used with --dev" + end + if args.server then + return nil, "--only-server cannot be used with --server" + end + cfg.rocks_servers = { args.only_server } + end + + return true +end + +local function error_handler(err) + if not debug then + return err + end + local mode = "Arch.: " .. (cfg and cfg.arch or "unknown") + if package.config:sub(1, 1) == "\\" then + if cfg and cfg.fs_use_modules then + mode = mode .. " (fs_use_modules = true)" + end + end + if cfg and cfg.is_binary then + mode = mode .. " (binary)" + end + return debug.traceback("LuaRocks "..cfg.program_version.. + " bug (please report at https://github.com/luarocks/luarocks/issues).\n".. + mode.."\n"..err, 2) +end + +--- Display an error message and exit. +-- @param message string: The error message. +-- @param exitcode number: the exitcode to use +local function die(message, exitcode) + assert(type(message) == "string", "bad error, expected string, got: " .. type(message)) + assert(exitcode == nil or type(exitcode) == "number", "bad error, expected number, got: " .. type(exitcode) .. " - " .. tostring(exitcode)) + util.printerr("\nError: "..message) + + local ok, err = xpcall(util.run_scheduled_functions, error_handler) + if not ok then + util.printerr("\nError: "..err) + exitcode = cmd.errorcodes.CRASH + end + + os.exit(exitcode or cmd.errorcodes.UNSPECIFIED) +end + +local function search_lua(lua_version, verbose, search_at) + if search_at then + return util.find_lua(search_at, lua_version, verbose) + end + + local path_sep = (package.config:sub(1, 1) == "\\" and ";" or ":") + local all_tried = {} + for bindir in (os.getenv("PATH") or ""):gmatch("[^"..path_sep.."]+") do + local searchdir = (bindir:gsub("[\\/]+bin[\\/]?$", "")) + local detected, tried = util.find_lua(searchdir, lua_version) + if detected then + return detected + else + table.insert(all_tried, tried) + end + end + return nil, "Could not find " .. + (lua_version and "Lua " .. lua_version or "Lua") .. + " in PATH." .. + (verbose and " Tried:\n" .. table.concat(all_tried, "\n") or "") +end + +local init_config +do + local detect_config_via_args + do + local function find_project_dir(project_tree) + if project_tree then + return project_tree:gsub("[/\\][^/\\]+$", ""), true + else + local try = "." + for _ = 1, 10 do -- FIXME detect when root dir was hit instead + if util.exists(try .. "/.luarocks") and util.exists(try .. "/lua_modules") then + return dir.normalize(try), false + elseif util.exists(try .. "/.luarocks-no-project") then + break + end + try = try .. "/.." + end + end + return nil + end + + local function find_default_lua_version(args, project_dir) + if hardcoded.FORCE_CONFIG then + return nil + end + + local dirs = {} + if project_dir then + table.insert(dirs, dir.path(project_dir, ".luarocks")) + end + if cfg.homeconfdir then + table.insert(dirs, cfg.homeconfdir) + end + table.insert(dirs, cfg.sysconfdir) + for _, d in ipairs(dirs) do + local f = dir.path(d, "default-lua-version.lua") + local mod, err = loadfile(f, "t") + if mod then + local pok, ver = pcall(mod) + if pok and type(ver) == "string" and ver:match("%d+.%d+") then + if args.verbose then + util.printout("Defaulting to Lua " .. ver .. " based on " .. f .. " ...") + end + return ver + end + end + end + return nil + end + + local function find_version_from_config(dirname) + return fun.find(util.lua_versions("descending"), function(v) + if util.exists(dir.path(dirname, ".luarocks", "config-"..v..".lua")) then + return v + end + end) + end + + local function detect_lua_via_args(args, project_dir) + local lua_version = args.lua_version + or find_default_lua_version(args, project_dir) + or (project_dir and find_version_from_config(project_dir)) + + if args.lua_dir then + local detected, err = util.find_lua(args.lua_dir, lua_version) + if not detected then + local suggestion = (not args.lua_version) + and "\nYou may want to specify a different Lua version with --lua-version\n" + or "" + die(err .. suggestion) + end + return detected + end + + if lua_version then + local detected = search_lua(lua_version) + if detected then + return detected + end + return { + lua_version = lua_version, + } + end + + return {} + end + + detect_config_via_args = function(args) + local project_dir, given + if not args.no_project then + project_dir, given = find_project_dir(args.project_tree) + end + + local detected = detect_lua_via_args(args, project_dir) + if args.lua_version then + detected.given_lua_version = args.lua_version + end + if args.lua_dir then + detected.given_lua_dir = args.lua_dir + end + if given then + detected.given_project_dir = project_dir + end + detected.project_dir = project_dir + return detected + end + end + + init_config = function(args) + local detected = detect_config_via_args(args) + + local ok, err = cfg.init(detected, util.warning) + if not ok then + return nil, err + end + + return (detected.lua_dir ~= nil) + end +end + +local variables_help = [[ +Variables: + Variables from the "variables" table of the configuration file can be + overridden with VAR=VALUE assignments. + +]] + +local lua_example = package.config:sub(1, 1) == "\\" + and "" + or "" + +local function show_status(file, status, err) + return (file and file .. " " or "") .. (status and "(ok)" or ("(" .. (err or "not found") ..")")) +end + +local function use_to_fix_location(key, what) + local buf = " ****************************************\n" + buf = buf .. " Use the command\n\n" + buf = buf .. " luarocks config " .. key .. " " .. (what or "

") .. "\n\n" + buf = buf .. " to fix the location\n" + buf = buf .. " ****************************************\n" + return buf +end + +local function get_config_text(cfg) -- luacheck: ignore 431 + local deps = require("luarocks.deps") + + local libdir_ok = deps.check_lua_libdir(cfg.variables) + local incdir_ok = deps.check_lua_incdir(cfg.variables) + local lua_ok = cfg.variables.LUA and fs.exists(cfg.variables.LUA) + + local buf = "Configuration:\n" + buf = buf.." Lua:\n" + buf = buf.." Version : "..cfg.lua_version.."\n" + if cfg.luajit_version then + buf = buf.." LuaJIT : "..cfg.luajit_version.."\n" + end + buf = buf.." LUA : "..show_status(cfg.variables.LUA, lua_ok, "interpreter not found").."\n" + if not lua_ok then + buf = buf .. use_to_fix_location("variables.LUA", lua_example) + end + buf = buf.." LUA_INCDIR : "..show_status(cfg.variables.LUA_INCDIR, incdir_ok, "lua.h not found").."\n" + if lua_ok and not incdir_ok then + buf = buf .. use_to_fix_location("variables.LUA_INCDIR") + end + buf = buf.." LUA_LIBDIR : "..show_status(cfg.variables.LUA_LIBDIR, libdir_ok, "Lua library itself not found").."\n" + if lua_ok and not libdir_ok then + buf = buf .. use_to_fix_location("variables.LUA_LIBDIR") + end + + buf = buf.."\n Configuration files:\n" + local conf = cfg.config_files + buf = buf.." System : "..show_status(fs.absolute_name(conf.system.file), conf.system.found).."\n" + if conf.user.file then + buf = buf.." User : "..show_status(fs.absolute_name(conf.user.file), conf.user.found).."\n" + else + buf = buf.." User : disabled in this LuaRocks installation.\n" + end + if conf.project then + buf = buf.." Project : "..show_status(fs.absolute_name(conf.project.file), conf.project.found).."\n" + end + buf = buf.."\n Rocks trees in use: \n" + for _, tree in ipairs(cfg.rocks_trees) do + if type(tree) == "string" then + buf = buf.." "..fs.absolute_name(tree) + else + local name = tree.name and " (\""..tree.name.."\")" or "" + buf = buf.." "..fs.absolute_name(tree.root)..name + end + buf = buf .. "\n" + end + + return buf +end + +local function get_parser(description, cmd_modules) + local basename = dir.base_name(program) + local parser = argparse( + basename, "LuaRocks "..cfg.program_version..", the Lua package manager\n\n".. + program.." - "..description, variables_help.."Run '"..basename.. + "' without any arguments to see the configuration.") + :help_max_width(80) + :add_help_command() + :add_complete_command({ + help_max_width = 100, + summary = "Output a shell completion script.", + description = [[ +Output a shell completion script. + +Enabling completions for Bash: + + Add the following line to your ~/.bashrc: + source <(]]..basename..[[ completion bash) + or save the completion script to the local completion directory: + ]]..basename..[[ completion bash > ~/.local/share/bash-completion/completions/]]..basename..[[ + + +Enabling completions for Zsh: + + Save the completion script to a file in your $fpath. + You can add a new directory to your $fpath by adding e.g. + fpath=(~/.zfunc $fpath) + to your ~/.zshrc. + Then run: + ]]..basename..[[ completion zsh > ~/.zfunc/_]]..basename..[[ + + +Enabling completion for Fish: + + Add the following line to your ~/.config/fish/config.fish: + ]]..basename..[[ completion fish | source + or save the completion script to the local completion directory: + ]]..basename..[[ completion fish > ~/.config/fish/completions/]]..basename..[[.fish +]]}) + :command_target("command") + :require_command(false) + + parser:flag("--version", "Show version info and exit.") + :action(function() + util.printout(program.." "..cfg.program_version) + util.printout(description) + util.printout() + os.exit(cmd.errorcodes.OK) + end) + parser:flag("--dev", "Enable the sub-repositories in rocks servers for ".. + "rockspecs of in-development versions.") + parser:option("--server", "Fetch rocks/rockspecs from this server ".. + "(takes priority over config file).") + :hidden_name("--from") + parser:option("--only-server", "Fetch rocks/rockspecs from this server only ".. + "(overrides any entries in the config file).") + :argname("") + :hidden_name("--only-from") + parser:option("--only-sources", "Restrict downloads to paths matching the given URL.") + :argname("") + :hidden_name("--only-sources-from") + parser:option("--namespace", "Specify the rocks server namespace to use.") + :convert(string.lower) + parser:option("--lua-dir", "Which Lua installation to use.") + :argname("") + parser:option("--lua-version", "Which Lua version to use.") + :argname("") + :convert(function(s) return (s:match("^%d+%.%d+$")) end) + parser:option("--tree", "Which tree to operate on.") + :hidden_name("--to") + parser:flag("--local", "Use the tree in the user's home directory.\n".. + "To enable it, see '"..program.." help path'.") + parser:flag("--global", "Use the system tree when `local_by_default` is `true`.") + parser:flag("--no-project", "Do not use project tree even if running from a project folder.") + parser:flag("--force-lock", "Attempt to overwrite the lock for commands " .. + "that require exclusive access, such as 'install'") + parser:flag("--verbose", "Display verbose output of commands executed.") + parser:option("--timeout", "Timeout on network operations, in seconds.\n".. + "0 means no timeout (wait forever). Default is ".. + tostring(cfg.connection_timeout)..".") + :argname("") + :convert(tonumber) + + -- Used internally to force the use of a particular project tree + parser:option("--project-tree"):hidden(true) + + for _, module in util.sortedpairs(cmd_modules) do + module.add_to_parser(parser) + end + + return parser +end + +local function get_first_arg() + if not arg then + return + end + local first_arg = arg[0] + local i = -1 + while arg[i] do + first_arg = arg[i] + i = i -1 + end + return first_arg +end + +--- Main command-line processor. +-- Parses input arguments and calls the appropriate driver function +-- to execute the action requested on the command-line, forwarding +-- to it any additional arguments passed by the user. +-- @param description string: Short summary description of the program. +-- @param commands table: contains the loaded modules representing commands. +-- @param external_namespace string: where to look for external commands. +-- @param ... string: Arguments given on the command-line. +function cmd.run_command(description, commands, external_namespace, ...) + + check_popen() + + -- Preliminary initialization + cfg.init() + + fs.init() + + for _, module_name in ipairs(fs.modules(external_namespace)) do + if not commands[module_name] then + commands[module_name] = external_namespace.."."..module_name + end + end + + local cmd_modules = {} + for name, module in pairs(commands) do + local pok, mod = pcall(require, module) + if pok and type(mod) == "table" then + local original_command = mod.command + if original_command then + if not mod.add_to_parser then + mod.add_to_parser = function(parser) + parser:command(name, mod.help, util.see_also()) + :summary(mod.help_summary) + :handle_options(false) + :argument("input") + :args("*") + end + + mod.command = function(args) + return original_command(args, unpack(args.input)) + end + end + cmd_modules[name] = mod + else + util.warning("command module " .. module .. " does not implement command(), skipping") + end + else + util.warning("failed to load command module " .. module .. ": " .. mod) + end + end + + local function process_cmdline_vars(...) + local args = pack(...) + local cmdline_vars = {} + local last = args.n + for i = 1, args.n do + if args[i] == "--" then + last = i - 1 + break + end + end + for i = last, 1, -1 do + local arg = args[i] + if arg:match("^[^-][^=]*=") then + local var, val = arg:match("^([A-Z_][A-Z0-9_]*)=(.*)") + if val then + cmdline_vars[var] = val + table.remove(args, i) + else + die("Invalid assignment: "..arg) + end + end + end + + return args, cmdline_vars + end + + local args, cmdline_vars = process_cmdline_vars(...) + local parser = get_parser(description, cmd_modules) + args = parser:parse(args) + + -- Compatibility for old flag + if args.nodeps then + args.deps_mode = "none" + end + + if args.timeout then -- setting it in the config file will kick-in earlier in the process + cfg.connection_timeout = args.timeout + end + + if args.command == "config" then + if args.key == "lua_version" and args.value then + args.lua_version = args.value + elseif args.key == "lua_dir" and args.value then + args.lua_dir = args.value + end + end + + ----------------------------------------------------------------------------- + local lua_found, err = init_config(args) + if err then + die(err) + end + ----------------------------------------------------------------------------- + + -- Now that the config is fully loaded, reinitialize fs using the full + -- feature set. + fs.init() + + -- if the Lua interpreter wasn't explicitly found before cfg.init, + -- try again now. + local tried + if not lua_found then + local detected + detected, tried = search_lua(cfg.lua_version, args.verbose, cfg.variables.LUA_DIR) + if detected then + lua_found = true + cfg.variables.LUA = detected.lua + cfg.variables.LUA_DIR = detected.lua_dir + cfg.variables.LUA_BINDIR = detected.lua_bindir + if args.lua_dir then + cfg.variables.LUA_INCDIR = nil + cfg.variables.LUA_LIBDIR = nil + end + else + cfg.variables.LUA = nil + cfg.variables.LUA_DIR = nil + cfg.variables.LUA_BINDIR = nil + cfg.variables.LUA_INCDIR = nil + cfg.variables.LUA_LIBDIR = nil + end + end + + if lua_found then + assert(cfg.variables.LUA) + else + -- Fallback producing _some_ Lua configuration based on the running interpreter. + -- Most likely won't produce correct results when running from the standalone binary, + -- so eventually we need to drop this and outright fail if Lua is not found + -- or explictly configured + if not cfg.variables.LUA then + local first_arg = get_first_arg() + local bin_dir = dir.dir_name(fs.absolute_name(first_arg)) + local exe = dir.base_name(first_arg) + exe = exe:match("rocks") and ("lua" .. (cfg.arch:match("win") and ".exe" or "")) or exe + local full_path = dir.path(bin_dir, exe) + if util.check_lua_version(full_path, cfg.lua_version) then + cfg.variables.LUA = dir.path(bin_dir, exe) + cfg.variables.LUA_DIR = bin_dir:gsub("[/\\]bin[/\\]?$", "") + cfg.variables.LUA_BINDIR = bin_dir + cfg.variables.LUA_INCDIR = nil + cfg.variables.LUA_LIBDIR = nil + end + end + end + + cfg.lua_found = lua_found + + if cfg.project_dir then + cfg.project_dir = fs.absolute_name(cfg.project_dir) + end + + if args.verbose then + cfg.verbose = true + print(("-"):rep(79)) + print("Current configuration:") + print(("-"):rep(79)) + print(config.to_string(cfg)) + print(("-"):rep(79)) + fs.verbose() + end + + if (not fs.current_dir()) or fs.current_dir() == "" then + die("Current directory does not exist. Please run LuaRocks from an existing directory.") + end + + local ok, err = process_tree_args(args, cfg.project_dir) + if not ok then + die(err) + end + + ok, err = process_server_args(args) + if not ok then + die(err) + end + + if args.only_sources then + cfg.only_sources_from = args.only_sources + end + + for k, v in pairs(cmdline_vars) do + cfg.variables[k] = v + end + + -- if running as superuser, use system cache dir + if fs.is_superuser() then + cfg.local_cache = dir.path(fs.system_cache_dir(), "luarocks") + end + + if args.no_manifest then + cfg.no_manifest = true + end + + if not args.command then + parser:epilog(variables_help..get_config_text(cfg)) + util.printout() + util.printout(parser:get_help()) + util.printout() + os.exit(cmd.errorcodes.OK) + end + + if not cfg.variables["LUA"] and args.command ~= "config" and args.command ~= "help" then + local flag = (not cfg.project_tree) + and "--local " + or "" + if args.lua_version then + flag = "--lua-version=" .. args.lua_version .. " " .. flag + end + die((tried or "Lua interpreter not found.") .. + "\nPlease set your Lua interpreter with:\n\n" .. + " luarocks " .. flag.. "config variables.LUA " .. lua_example .. "\n") + end + + local cmd_mod = cmd_modules[args.command] + + local lock + if cmd_mod.needs_lock and cmd_mod.needs_lock(args) then + local ok, err = fs.check_command_permissions(args) + if not ok then + die(err, cmd.errorcodes.PERMISSIONDENIED) + end + + lock, err = fs.lock_access(path.root_dir(cfg.root_dir), args.force_lock) + if not lock then + err = args.force_lock + and ("failed to force the lock" .. (err and ": " .. err or "")) + or (err and err ~= "File exists") + and err + or "try --force-lock to overwrite the lock" + + die("command '" .. args.command .. "' " .. + "requires exclusive write access to " .. path.root_dir(cfg.root_dir) .. " - " .. + err, cmd.errorcodes.LOCK) + end + end + + local call_ok, ok, err, exitcode = xpcall(function() + return cmd_mod.command(args) + end, error_handler) + + if lock then + fs.unlock_access(lock) + end + + if not call_ok then + die(ok, cmd.errorcodes.CRASH) + elseif not ok then + die(err, exitcode) + end + util.run_scheduled_functions() +end + +return cmd diff --git a/src/luarocks/cmd/build.lua b/src/luarocks/cmd/build.lua new file mode 100644 index 0000000..3268041 --- /dev/null +++ b/src/luarocks/cmd/build.lua @@ -0,0 +1,198 @@ + +--- Module implementing the LuaRocks "build" command. +-- Builds a rock, compiling its C parts if any. +local cmd_build = {} + +local pack = require("luarocks.pack") +local path = require("luarocks.path") +local util = require("luarocks.util") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local deps = require("luarocks.deps") +local remove = require("luarocks.remove") +local cfg = require("luarocks.core.cfg") +local build = require("luarocks.build") +local writer = require("luarocks.manif.writer") +local search = require("luarocks.search") +local make = require("luarocks.cmd.make") +local repos = require("luarocks.repos") + +function cmd_build.add_to_parser(parser) + local cmd = parser:command("build", "Build and install a rock, compiling its C parts if any.\n".. -- luacheck: ignore 431 + "If the sources contain a luarocks.lock file, uses it as an authoritative source for ".. + "exact version of dependencies.\n".. + "If no arguments are given, behaves as luarocks make.", util.see_also()) + :summary("Build/compile a rock.") + + cmd:argument("rock", "A rockspec file, a source rock file, or the name of ".. + "a rock to be fetched from a repository.") + :args("?") + :action(util.namespaced_name_action) + cmd:argument("version", "Rock version.") + :args("?") + + cmd:flag("--only-deps --deps-only", "Install only the dependencies of the rock.") + cmd:option("--branch", "Override the `source.branch` field in the loaded ".. + "rockspec. Allows to specify a different branch to fetch. Particularly ".. + 'for "dev" rocks.') + :argname("") + cmd:flag("--pin", "Create a luarocks.lock file listing the exact ".. + "versions of each dependency found for this rock (recursively), ".. + "and store it in the rock's directory. ".. + "Ignores any existing luarocks.lock file in the rock's sources.") + make.cmd_options(cmd) +end + +--- Build and install a rock. +-- @param rock_filename string: local or remote filename of a rock. +-- @param opts table: build options +-- @return boolean or (nil, string, [string]): True if build was successful, +-- or false and an error message and an optional error code. +local function build_rock(rock_filename, opts) + assert(type(rock_filename) == "string") + assert(opts:type() == "build.opts") + + local ok, err, errcode + + local unpack_dir + unpack_dir, err, errcode = fetch.fetch_and_unpack_rock(rock_filename, nil, opts.verify) + if not unpack_dir then + return nil, err, errcode + end + + local rockspec_filename = path.rockspec_name_from_rock(rock_filename) + + ok, err = fs.change_dir(unpack_dir) + if not ok then return nil, err end + + local rockspec + rockspec, err, errcode = fetch.load_rockspec(rockspec_filename) + if not rockspec then + return nil, err, errcode + end + + ok, err, errcode = build.build_rockspec(rockspec, opts) + + fs.pop_dir() + return ok, err, errcode +end + +local function do_build(name, namespace, version, opts) + assert(type(name) == "string") + assert(type(namespace) == "string" or not namespace) + assert(version == nil or type(version) == "string") + assert(opts:type() == "build.opts") + + local url, err + if name:match("%.rockspec$") or name:match("%.rock$") then + url = name + else + url, err = search.find_src_or_rockspec(name, namespace, version, opts.check_lua_versions) + if not url then + return nil, err + end + end + + name, version = path.parse_name(url) + if name and repos.is_installed(name, version) then + if not opts.rebuild then + util.printout(name .. " " .. version .. " is already installed in " .. path.root_dir(cfg.root_dir)) + util.printout("Use --force to reinstall.") + return name, version, "skip" + end + end + + if url:match("%.rockspec$") then + local rockspec, err = fetch.load_rockspec(url, nil, opts.verify) + if not rockspec then + return nil, err + end + return build.build_rockspec(rockspec, opts) + end + + if url:match("%.src%.rock$") then + opts.need_to_fetch = false + end + + return build_rock(url, opts) +end + +--- Driver function for "build" command. +-- If a package name is given, forwards the request to "search" and, +-- if returned a result, installs the matching rock. +-- When passing a package name, a version number may also be given. +-- @return boolean or (nil, string, exitcode): True if build was successful; nil and an +-- error message otherwise. exitcode is optionally returned. +function cmd_build.command(args) + if not args.rock then + return make.command(args) + end + + local opts = build.opts({ + need_to_fetch = true, + minimal_mode = false, + deps_mode = deps.get_deps_mode(args), + build_only_deps = not not (args.only_deps and not args.pack_binary_rock), + namespace = args.namespace, + branch = args.branch, + verify = not not args.verify, + check_lua_versions = not not args.check_lua_versions, + pin = not not args.pin, + rebuild = not not (args.force or args.force_fast), + no_install = false + }) + + if args.sign and not args.pack_binary_rock then + return nil, "In the build command, --sign is meant to be used only with --pack-binary-rock" + end + + if args.pack_binary_rock then + return pack.pack_binary_rock(args.rock, args.namespace, args.version, args.sign, function() + local name, version = do_build(args.rock, args.namespace, args.version, opts) + if name and args.no_doc then + util.remove_doc_dir(name, version) + end + return name, version + end) + end + + local name, version, skip = do_build(args.rock, args.namespace, args.version, opts) + if not name then + return nil, version + end + if skip == "skip" then + return name, version + end + + if args.no_doc then + util.remove_doc_dir(name, version) + end + + if opts.build_only_deps then + util.printout("Stopping after installing dependencies for " ..name.." "..version) + util.printout() + else + if (not args.keep) and not cfg.keep_other_versions then + local ok, err, warn = remove.remove_other_versions(name, version, args.force, args.force_fast) + if not ok then + return nil, err + elseif warn then + util.printerr(err) + end + end + end + + if opts.deps_mode ~= "none" then + writer.check_dependencies(nil, deps.get_deps_mode(args)) + end + return name, version +end + +cmd_build.needs_lock = function(args) + if args.pack_binary_rock then + return false + end + return true +end + +return cmd_build diff --git a/src/luarocks/cmd/config.lua b/src/luarocks/cmd/config.lua new file mode 100644 index 0000000..d67711a --- /dev/null +++ b/src/luarocks/cmd/config.lua @@ -0,0 +1,392 @@ +--- Module implementing the LuaRocks "config" command. +-- Queries information about the LuaRocks configuration. +local config_cmd = {} + +local persist = require("luarocks.persist") +local config = require("luarocks.config") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local deps = require("luarocks.deps") +local dir = require("luarocks.dir") +local fs = require("luarocks.fs") +local json = require("luarocks.vendor.dkjson") + +function config_cmd.add_to_parser(parser) + local cmd = parser:command("config", [[ +Query information about the LuaRocks configuration. + +* When given a configuration key, it prints the value of that key according to + the currently active configuration (taking into account all config files and + any command-line flags passed) + + Examples: + luarocks config variables.LUA_INCDIR + luarocks config lua_version + +* When given a configuration key and a value, it overwrites the config file (see + the --scope option below to determine which) and replaces the value of the + given key with the given value. + + * `lua_dir` is a special key as it checks for a valid Lua installation + (equivalent to --lua-dir) and sets several keys at once. + * `lua_version` is a special key as it changes the default Lua version + used by LuaRocks commands (equivalent to passing --lua-version). + + Examples: + luarocks config variables.OPENSSL_DIR /usr/local/openssl + luarocks config lua_dir /usr/local + luarocks config lua_version 5.3 + +* When given a configuration key and --unset, it overwrites the config file (see + the --scope option below to determine which) and deletes that key from the + file. + + Example: luarocks config variables.OPENSSL_DIR --unset + +* When given no arguments, it prints the entire currently active configuration, + resulting from reading the config files from all scopes. + + Example: luarocks config]], util.see_also([[ + https://github.com/luarocks/luarocks/wiki/Config-file-format + for detailed information on the LuaRocks config file format. +]])) + :summary("Query information about the LuaRocks configuration.") + + cmd:argument("key", "The configuration key.") + :args("?") + cmd:argument("value", "The configuration value.") + :args("?") + + cmd:option("--scope", "The scope indicates which config file should be rewritten.\n".. + '* Using a wrapper created with `luarocks init`, the default is "project".\n'.. + '* Using --local (or when `local_by_default` is `true`), the default is "user".\n'.. + '* Otherwise, the default is "system".') + :choices({"system", "user", "project"}) + cmd:flag("--unset", "Delete the key from the configuration file.") + cmd:flag("--json", "Output as JSON.") + + -- Deprecated flags + cmd:flag("--lua-incdir"):hidden(true) + cmd:flag("--lua-libdir"):hidden(true) + cmd:flag("--lua-ver"):hidden(true) + cmd:flag("--system-config"):hidden(true) + cmd:flag("--user-config"):hidden(true) + cmd:flag("--rock-trees"):hidden(true) +end + +local function config_file(conf) + print(dir.normalize(conf.file)) + if conf.found then + return true + else + return nil, "file not found" + end +end + +local function traverse_varstring(var, tbl, fn, missing_parent) + local k, r = var:match("^%[([0-9]+)%]%.(.*)$") + if k then + k = tonumber(k) + else + k, r = var:match("^([^.[]+)%.(.*)$") + if not k then + k, r = var:match("^([^[]+)(%[.*)$") + end + end + + if k then + if not tbl[k] and missing_parent then + missing_parent(tbl, k) + end + + if tbl[k] then + return traverse_varstring(r, tbl[k], fn, missing_parent) + else + return nil, "Unknown entry " .. k + end + end + + local i = var:match("^%[([0-9]+)%]$") + if i then + var = tonumber(i) + end + + return fn(tbl, var) +end + +local function print_json(value) + print(json.encode(value)) + return true +end + +local function print_entry(var, tbl, is_json) + return traverse_varstring(var, tbl, function(t, k) + if not t[k] then + return nil, "Unknown entry " .. k + end + local val = t[k] + + if not config.should_skip(var, val) then + if is_json then + return print_json(val) + elseif type(val) == "string" then + print(val) + else + persist.write_value(io.stdout, val) + end + end + return true + end) +end + +local function infer_type(var) + local typ + traverse_varstring(var, cfg, function(t, k) + if t[k] ~= nil then + typ = type(t[k]) + end + end) + return typ +end + +local function write_entries(keys, scope, do_unset) + if scope == "project" and not cfg.config_files.project then + return nil, "Current directory is not part of a project. You may want to run `luarocks init`." + end + + local file_name = cfg.config_files[scope].file + + local tbl, err = persist.load_config_file_if_basic(file_name, cfg) + if not tbl then + return nil, err + end + + for var, val in util.sortedpairs(keys) do + traverse_varstring(var, tbl, function(t, k) + if do_unset then + t[k] = nil + else + local typ = infer_type(var) + local v + if typ == "number" and tonumber(val) then + v = tonumber(val) + elseif typ == "boolean" and val == "true" then + v = true + elseif typ == "boolean" and val == "false" then + v = false + else + v = val + end + t[k] = v + keys[var] = v + end + return true + end, function(p, k) + p[k] = {} + end) + end + + local ok, err = fs.make_dir(dir.dir_name(file_name)) + if not ok then + return nil, err + end + + ok, err = persist.save_from_table(file_name, tbl) + if ok then + print(do_unset and "Removed" or "Wrote") + for var, val in util.sortedpairs(keys) do + if do_unset then + print(("\t%s"):format(var)) + else + if type(val) == "string" then + print(("\t%s = %q"):format(var, val)) + else + print(("\t%s = %s"):format(var, tostring(val))) + end + end + end + print(do_unset and "from" or "to") + print("\t" .. file_name) + return true + else + return nil, err + end +end + +local function get_scope(args) + return args.scope + or (args["local"] and "user") + or (args.project_tree and "project") + or (cfg.local_by_default and "user") + or (fs.is_writable(cfg.config_files["system"].file) and "system") + or "user" +end + +local function report_on_lua_incdir_config(value, lua_version) + local variables = { + ["LUA_DIR"] = cfg.variables.LUA_DIR, + ["LUA_BINDIR"] = cfg.variables.LUA_BINDIR, + ["LUA_INCDIR"] = value, + ["LUA_LIBDIR"] = cfg.variables.LUA_LIBDIR, + ["LUA"] = cfg.variables.LUA, + } + + local ok, err = deps.check_lua_incdir(variables, lua_version) + if not ok then + util.printerr() + util.warning((err:gsub(" You can use.*", ""))) + end + return ok +end + +local function report_on_lua_libdir_config(value, lua_version) + local variables = { + ["LUA_DIR"] = cfg.variables.LUA_DIR, + ["LUA_BINDIR"] = cfg.variables.LUA_BINDIR, + ["LUA_INCDIR"] = cfg.variables.LUA_INCDIR, + ["LUA_LIBDIR"] = value, + ["LUA"] = cfg.variables.LUA, + } + + local ok, err, _, err_files = deps.check_lua_libdir(variables, lua_version) + if not ok then + util.printerr() + util.warning((err:gsub(" You can use.*", ""))) + util.printerr("Tried:") + for _, l in pairs(err_files or {}) do + for _, d in ipairs(l) do + util.printerr("\t" .. d) + end + end + end + return ok +end + +local function warn_bad_c_config() + util.printerr() + util.printerr("LuaRocks may not work correctly when building C modules using this configuration.") + util.printerr() +end + +--- Driver function for "config" command. +-- @return boolean: True if succeeded, nil on errors. +function config_cmd.command(args) + local lua_version = args.lua_version or cfg.lua_version + + deps.check_lua_incdir(cfg.variables, lua_version) + deps.check_lua_libdir(cfg.variables, lua_version) + + -- deprecated flags + if args.lua_incdir then + print(cfg.variables.LUA_INCDIR) + return true + end + if args.lua_libdir then + print(cfg.variables.LUA_LIBDIR) + return true + end + if args.lua_ver then + print(cfg.lua_version) + return true + end + if args.system_config then + return config_file(cfg.config_files.system) + end + if args.user_config then + return config_file(cfg.config_files.user) + end + if args.rock_trees then + for _, tree in ipairs(cfg.rocks_trees) do + if type(tree) == "string" then + util.printout(dir.normalize(tree)) + else + local name = tree.name and "\t"..tree.name or "" + util.printout(dir.normalize(tree.root)..name) + end + end + return true + end + + if args.key == "lua_version" and args.value then + local scope = get_scope(args) + if scope == "project" and not cfg.config_files.project then + return nil, "Current directory is not part of a project. You may want to run `luarocks init`." + end + + local location = cfg.config_files[scope] + if (not location) or (not location.file) then + return nil, "could not get config file location for " .. tostring(scope) .. " scope" + end + + local prefix = dir.dir_name(location.file) + local ok, err = persist.save_default_lua_version(prefix, args.value) + if not ok then + return nil, "could not set default Lua version: " .. err + end + print("Lua version will default to " .. args.value .. " in " .. prefix) + end + + if args.key == "lua_dir" and args.value then + local scope = get_scope(args) + local keys = { + ["variables.LUA_DIR"] = cfg.variables.LUA_DIR, + ["variables.LUA_BINDIR"] = cfg.variables.LUA_BINDIR, + ["variables.LUA_INCDIR"] = cfg.variables.LUA_INCDIR, + ["variables.LUA_LIBDIR"] = cfg.variables.LUA_LIBDIR, + ["variables.LUA"] = cfg.variables.LUA, + } + if args.lua_version then + local prefix = dir.dir_name(cfg.config_files[scope].file) + persist.save_default_lua_version(prefix, args.lua_version) + end + local ok, err = write_entries(keys, scope, args.unset) + if ok then + local inc_ok = report_on_lua_incdir_config(cfg.variables.LUA_INCDIR, lua_version) + local lib_ok = ok and report_on_lua_libdir_config(cfg.variables.LUA_LIBDIR, lua_version) + if not (inc_ok and lib_ok) then + warn_bad_c_config() + end + end + + return ok, err + end + + if args.key then + if args.key:match("^[A-Z]") then + args.key = "variables." .. args.key + end + + if args.value or args.unset then + local scope = get_scope(args) + + local ok, err = write_entries({ [args.key] = args.value or args.unset }, scope, args.unset) + + if ok then + if args.key == "variables.LUA_INCDIR" then + local ok = report_on_lua_incdir_config(args.value, lua_version) + if not ok then + warn_bad_c_config() + end + elseif args.key == "variables.LUA_LIBDIR" then + local ok = report_on_lua_libdir_config(args.value, lua_version) + if not ok then + warn_bad_c_config() + end + end + end + + return ok, err + else + return print_entry(args.key, cfg, args.json) + end + end + + if args.json then + return print_json(config.get_config_for_display(cfg)) + else + print(config.to_string(cfg)) + return true + end +end + +return config_cmd diff --git a/src/luarocks/cmd/doc.lua b/src/luarocks/cmd/doc.lua new file mode 100644 index 0000000..a311700 --- /dev/null +++ b/src/luarocks/cmd/doc.lua @@ -0,0 +1,153 @@ + +--- Module implementing the LuaRocks "doc" command. +-- Shows documentation for an installed rock. +local doc = {} + +local util = require("luarocks.util") +local queries = require("luarocks.queries") +local search = require("luarocks.search") +local path = require("luarocks.path") +local dir = require("luarocks.dir") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local download = require("luarocks.download") + +function doc.add_to_parser(parser) + local cmd = parser:command("doc", "Show documentation for an installed rock.\n\n".. + "Without any flags, tries to load the documentation using a series of heuristics.\n".. + "With flags, return only the desired information.", util.see_also([[ + For more information about a rock, see the 'show' command. +]])) + :summary("Show documentation for an installed rock.") + + cmd:argument("rock", "Name of the rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "Version of the rock.") + :args("?") + + cmd:flag("--home", "Open the home page of project.") + cmd:flag("--list", "List documentation files only.") + cmd:flag("--porcelain", "Produce machine-friendly output.") +end + +local function show_homepage(homepage, name, namespace, version) + if not homepage then + return nil, "No 'homepage' field in rockspec for "..util.format_rock_name(name, namespace, version) + end + util.printout("Opening "..homepage.." ...") + fs.browser(homepage) + return true +end + +local function try_to_open_homepage(name, namespace, version) + local temp_dir, err = fs.make_temp_dir("doc-"..name.."-"..(version or "")) + if not temp_dir then + return nil, "Failed creating temporary directory: "..err + end + util.schedule_function(fs.delete, temp_dir) + local ok, err = fs.change_dir(temp_dir) + if not ok then return nil, err end + local filename, err = download.download("rockspec", name, namespace, version) + if not filename then return nil, err end + local rockspec, err = fetch.load_local_rockspec(filename) + if not rockspec then return nil, err end + fs.pop_dir() + local descript = rockspec.description or {} + return show_homepage(descript.homepage, name, namespace, version) +end + +--- Driver function for "doc" command. +-- @return boolean: True if succeeded, nil on errors. +function doc.command(args) + local query = queries.new(args.rock, args.namespace, args.version) + local iname, iversion, repo = search.pick_installed_rock(query, args.tree) + if not iname then + local rock = util.format_rock_name(args.rock, args.namespace, args.version) + util.printout(rock.." is not installed. Looking for it in the rocks servers...") + return try_to_open_homepage(args.rock, args.namespace, args.version) + end + local name, version = iname, iversion + + local rockspec, err = fetch.load_local_rockspec(path.rockspec_file(name, version, repo)) + if not rockspec then return nil,err end + local descript = rockspec.description or {} + + if args.home then + return show_homepage(descript.homepage, name, args.namespace, version) + end + + local directory = path.install_dir(name, version, repo) + + local docdir + local directories = { "doc", "docs" } + for _, d in ipairs(directories) do + local dirname = dir.path(directory, d) + if fs.is_dir(dirname) then + docdir = dirname + break + end + end + if not docdir then + if descript.homepage and not args.list then + util.printout("Local documentation directory not found -- opening "..descript.homepage.." ...") + fs.browser(descript.homepage) + return true + end + return nil, "Documentation directory not found for "..name.." "..version + end + + docdir = dir.normalize(docdir) + local files = fs.find(docdir) + local htmlpatt = "%.html?$" + local extensions = { htmlpatt, "%.md$", "%.txt$", "%.textile$", "" } + local basenames = { "index", "readme", "manual" } + + local porcelain = args.porcelain + if #files > 0 then + util.title("Documentation files for "..name.." "..version, porcelain) + if porcelain then + for _, file in ipairs(files) do + util.printout(docdir.."/"..file) + end + else + util.printout(docdir.."/") + for _, file in ipairs(files) do + util.printout("\t"..file) + end + end + end + + if args.list then + return true + end + + for _, extension in ipairs(extensions) do + for _, basename in ipairs(basenames) do + local filename = basename..extension + local found + for _, file in ipairs(files) do + if file:lower():match(filename) and ((not found) or #file < #found) then + found = file + end + end + if found then + local pathname = dir.path(docdir, found) + util.printout() + util.printout("Opening "..pathname.." ...") + util.printout() + local ok = fs.browser(pathname) + if not ok and not pathname:match(htmlpatt) then + local fd = io.open(pathname, "r") + util.printout(fd:read("*a")) + fd:close() + end + return true + end + end + end + + return true +end + + +return doc diff --git a/src/luarocks/cmd/download.lua b/src/luarocks/cmd/download.lua new file mode 100644 index 0000000..eae8243 --- /dev/null +++ b/src/luarocks/cmd/download.lua @@ -0,0 +1,51 @@ + +--- Module implementing the luarocks "download" command. +-- Download a rock from the repository. +local cmd_download = {} + +local util = require("luarocks.util") +local download = require("luarocks.download") + +function cmd_download.add_to_parser(parser) + local cmd = parser:command("download", "Download a specific rock file from a rocks server.", util.see_also()) + + cmd:argument("name", "Name of the rock.") + :args("?") + :action(util.namespaced_name_action) + cmd:argument("version", "Version of the rock.") + :args("?") + + cmd:flag("--all", "Download all files if there are multiple matches.") + cmd:mutex( + cmd:flag("--source", "Download .src.rock if available."), + cmd:flag("--rockspec", "Download .rockspec if available."), + cmd:option("--arch", "Download rock for a specific architecture.")) + cmd:flag("--check-lua-versions", "If the rock can't be found, check repository ".. + "and report if it is available for another Lua version.") +end + +--- Driver function for the "download" command. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +function cmd_download.command(args) + if not args.name and not args.all then + return nil, "Argument missing. "..util.see_help("download") + end + + args.name = args.name or "" + + local arch + + if args.source then + arch = "src" + elseif args.rockspec then + arch = "rockspec" + elseif args.arch then + arch = args.arch + end + + local dl, err = download.download(arch, args.name, args.namespace, args.version, args.all, args.check_lua_versions) + return dl and true, err +end + +return cmd_download diff --git a/src/luarocks/cmd/init.lua b/src/luarocks/cmd/init.lua new file mode 100644 index 0000000..b5359c9 --- /dev/null +++ b/src/luarocks/cmd/init.lua @@ -0,0 +1,219 @@ + +local init = {} + +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local deps = require("luarocks.deps") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local persist = require("luarocks.persist") +local write_rockspec = require("luarocks.cmd.write_rockspec") + +function init.add_to_parser(parser) + local cmd = parser:command("init", "Initialize a directory for a Lua project using LuaRocks.", util.see_also()) + + cmd:argument("name", "The project name.") + :args("?") + cmd:argument("version", "An optional project version.") + :args("?") + cmd:option("--wrapper-dir", "Location where the 'lua' and 'luarocks' wrapper scripts " .. + "should be generated; if not given, the current directory is used as a default.") + cmd:flag("--reset", "Delete any .luarocks/config-5.x.lua and ./lua and generate new ones.") + cmd:flag("--no-wrapper-scripts", "Do not generate wrapper ./lua and ./luarocks launcher scripts.") + cmd:flag("--no-gitignore", "Do not generate a .gitignore file.") + + cmd:group("Options for specifying rockspec data", write_rockspec.cmd_options(cmd)) +end + +local function gitignore_path(pwd, wrapper_dir, filename) + local norm_cur = fs.absolute_name(pwd) + local norm_file = fs.absolute_name(dir.path(wrapper_dir, filename)) + if norm_file:sub(1, #norm_cur) == norm_cur then + return norm_file:sub(#norm_cur + 2) + else + return filename + end +end + +local function write_gitignore(entries) + local gitignore = "" + local fd = io.open(".gitignore", "r") + if fd then + gitignore = fd:read("*a") + fd:close() + gitignore = "\n" .. gitignore .. "\n" + end + + fd = io.open(".gitignore", gitignore and "a" or "w") + if fd then + for _, entry in ipairs(entries) do + entry = "/" .. entry + if not gitignore:find("\n"..entry.."\n", 1, true) then + fd:write(entry.."\n") + end + end + fd:close() + end +end + +local function inject_tree(tree) + path.use_tree(tree) + local tree_set = false + for _, t in ipairs(cfg.rocks_trees) do + if type(t) == "table" then + if t.name == "project" then + t.root = tree + tree_set = true + end + end + end + if not tree_set then + table.insert(cfg.rocks_trees, 1, { name = "project", root = tree }) + end +end + +local function write_wrapper_scripts(wrapper_dir, luarocks_wrapper, lua_wrapper) + local tree = dir.path(fs.current_dir(), "lua_modules") + + fs.make_dir(wrapper_dir) + + luarocks_wrapper = dir.path(wrapper_dir, luarocks_wrapper) + if not fs.exists(luarocks_wrapper) then + util.printout("Preparing " .. luarocks_wrapper .. " ...") + fs.wrap_script(arg[0], luarocks_wrapper, "none", nil, nil, "--project-tree", tree) + else + util.printout(luarocks_wrapper .. " already exists. Not overwriting it!") + end + + lua_wrapper = dir.path(wrapper_dir, lua_wrapper) + local write_lua_wrapper = true + if fs.exists(lua_wrapper) then + if not util.lua_is_wrapper(lua_wrapper) then + util.printout(lua_wrapper .. " already exists and does not look like a wrapper script. Not overwriting.") + write_lua_wrapper = false + end + end + + if write_lua_wrapper then + if util.check_lua_version(cfg.variables.LUA, cfg.lua_version) then + util.printout("Preparing " .. lua_wrapper .. " for version " .. cfg.lua_version .. "...") + + -- Inject tree so it shows up as a lookup path in the wrappers + inject_tree(tree) + + fs.wrap_script(nil, lua_wrapper, "all") + else + util.warning("No Lua interpreter detected for version " .. cfg.lua_version .. ". Not creating " .. lua_wrapper) + end + end +end + +--- Driver function for "init" command. +-- @return boolean: True if succeeded, nil on errors. +function init.command(args) + local do_gitignore = not args.no_gitignore + local do_wrapper_scripts = not args.no_wrapper_scripts + local wrapper_dir = args.wrapper_dir or "." + + local pwd = fs.current_dir() + + if not args.name then + args.name = dir.base_name(pwd) + if args.name == "/" then + return nil, "When running from the root directory, please specify the argument" + end + end + + util.title("Initializing project '" .. args.name .. "' for Lua " .. cfg.lua_version .. " ...") + + local ok, err = deps.check_lua_incdir(cfg.variables) + if not ok then + return nil, err + end + + local has_rockspec = false + for file in fs.dir() do + if file:match("%.rockspec$") then + has_rockspec = true + break + end + end + + if not has_rockspec then + args.version = args.version or "dev" + args.location = pwd + local ok, err = write_rockspec.command(args) + if not ok then + util.printerr(err) + end + end + + local ext = cfg.wrapper_suffix + local luarocks_wrapper = "luarocks" .. ext + local lua_wrapper = "lua" .. ext + + if do_gitignore then + util.printout("Adding entries to .gitignore ...") + local ignores = { "lua_modules", ".luarocks" } + if do_wrapper_scripts then + table.insert(ignores, 1, gitignore_path(pwd, wrapper_dir, luarocks_wrapper)) + table.insert(ignores, 2, gitignore_path(pwd, wrapper_dir, lua_wrapper)) + end + write_gitignore(ignores) + end + + util.printout("Preparing ./.luarocks/ ...") + fs.make_dir(".luarocks") + local config_file = ".luarocks/config-" .. cfg.lua_version .. ".lua" + + if args.reset then + if do_wrapper_scripts then + fs.delete(fs.absolute_name(dir.path(wrapper_dir, lua_wrapper))) + end + fs.delete(fs.absolute_name(config_file)) + end + + local config_tbl, err = persist.load_config_file_if_basic(config_file, cfg) + if config_tbl then + local varnames = { + "LUA_DIR", + "LUA_INCDIR", + "LUA_LIBDIR", + "LUA_BINDIR", + "LUA", + } + for _, varname in ipairs(varnames) do + if cfg.variables[varname] then + config_tbl.variables = config_tbl.variables or {} + config_tbl.variables[varname] = cfg.variables[varname] + end + end + local ok, err = persist.save_from_table(config_file, config_tbl) + if ok then + util.printout("Wrote " .. config_file) + else + util.printout("Failed writing " .. config_file .. ": " .. err) + end + else + util.printout("Will not attempt to overwrite " .. config_file) + end + + ok, err = persist.save_default_lua_version(".luarocks", cfg.lua_version) + if not ok then + util.printout("Failed setting default Lua version: " .. err) + end + + util.printout("Preparing ./lua_modules/ ...") + fs.make_dir("lua_modules/lib/luarocks/rocks-" .. cfg.lua_version) + + if do_wrapper_scripts then + write_wrapper_scripts(wrapper_dir, luarocks_wrapper, lua_wrapper) + end + + return true +end + +init.needs_lock = function() return true end + +return init diff --git a/src/luarocks/cmd/install.lua b/src/luarocks/cmd/install.lua new file mode 100644 index 0000000..05e31fe --- /dev/null +++ b/src/luarocks/cmd/install.lua @@ -0,0 +1,271 @@ +--- Module implementing the LuaRocks "install" command. +-- Installs binary rocks. +local install = {} + +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local repos = require("luarocks.repos") +local fetch = require("luarocks.fetch") +local util = require("luarocks.util") +local fs = require("luarocks.fs") +local deps = require("luarocks.deps") +local writer = require("luarocks.manif.writer") +local remove = require("luarocks.remove") +local search = require("luarocks.search") +local queries = require("luarocks.queries") +local cfg = require("luarocks.core.cfg") + +function install.add_to_parser(parser) + local cmd = parser:command("install", "Install a rock.", util.see_also()) -- luacheck: ignore 431 + + cmd:argument("rock", "The name of a rock to be fetched from a repository ".. + "or a filename of a locally available rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "Version of the rock.") + :args("?") + + cmd:flag("--keep", "Do not remove previously installed versions of the ".. + "rock after building a new one. This behavior can be made permanent by ".. + "setting keep_other_versions=true in the configuration file.") + cmd:flag("--force", "If --keep is not specified, force removal of ".. + "previously installed versions if it would break dependencies. ".. + "If rock is already installed, reinstall it anyway.") + cmd:flag("--force-fast", "Like --force, but performs a forced removal ".. + "without reporting dependency issues.") + cmd:flag("--only-deps --deps-only", "Install only the dependencies of the rock.") + cmd:flag("--no-doc", "Install the rock without its documentation.") + cmd:flag("--verify", "Verify signature of the rockspec or src.rock being ".. + "built. If the rockspec or src.rock is being downloaded, LuaRocks will ".. + "attempt to download the signature as well. Otherwise, the signature ".. + "file should be already available locally in the same directory.\n".. + "You need the signer’s public key in your local keyring for this ".. + "option to work properly.") + cmd:flag("--check-lua-versions", "If the rock can't be found, check repository ".. + "and report if it is available for another Lua version.") + util.deps_mode_option(cmd) + cmd:flag("--no-manifest", "Skip creating/updating the manifest") + cmd:flag("--pin", "If the installed rock is a Lua module, create a ".. + "luarocks.lock file listing the exact versions of each dependency found for ".. + "this rock (recursively), and store it in the rock's directory. ".. + "Ignores any existing luarocks.lock file in the rock's sources.") + -- luarocks build options + parser:flag("--pack-binary-rock"):hidden(true) + parser:option("--branch"):hidden(true) + parser:flag("--sign"):hidden(true) +end + +install.opts = util.opts_table("install.opts", { + namespace = "string?", + keep = "boolean", + force = "boolean", + force_fast = "boolean", + no_doc = "boolean", + deps_mode = "string", + verify = "boolean", +}) + +--- Install a binary rock. +-- @param rock_file string: local or remote filename of a rock. +-- @param opts table: installation options +-- @return (string, string) or (nil, string, [string]): Name and version of +-- installed rock if succeeded or nil and an error message followed by an error code. +function install.install_binary_rock(rock_file, opts) + assert(type(rock_file) == "string") + assert(opts:type() == "install.opts") + + local namespace = opts.namespace + local deps_mode = opts.deps_mode + + local name, version, arch = path.parse_name(rock_file) + if not name then + return nil, "Filename "..rock_file.." does not match format 'name-version-revision.arch.rock'." + end + + if arch ~= "all" and arch ~= cfg.arch then + return nil, "Incompatible architecture "..arch, "arch" + end + if repos.is_installed(name, version) then + if not (opts.force or opts.force_fast) then + util.printout(name .. " " .. version .. " is already installed in " .. path.root_dir(cfg.root_dir)) + util.printout("Use --force to reinstall.") + return name, version + end + repos.delete_version(name, version, opts.deps_mode) + end + + local install_dir = path.install_dir(name, version) + + local rollback = util.schedule_function(function() + fs.delete(install_dir) + fs.remove_dir_if_empty(path.versions_dir(name)) + end) + + local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, install_dir, opts.verify) + if not ok then return nil, err, errcode end + + local rockspec, err = fetch.load_rockspec(path.rockspec_file(name, version)) + if err then + return nil, "Failed loading rockspec for installed package: "..err, errcode + end + + if opts.deps_mode ~= "none" then + ok, err, errcode = deps.check_external_deps(rockspec, "install") + if err then return nil, err, errcode end + end + + -- For compatibility with .rock files built with LuaRocks 1 + if not fs.exists(path.rock_manifest_file(name, version)) then + ok, err = writer.make_rock_manifest(name, version) + if err then return nil, err end + end + + if namespace then + ok, err = writer.make_namespace_file(name, version, namespace) + if err then return nil, err end + end + + if deps_mode ~= "none" then + local deplock_dir = fs.exists(dir.path(".", "luarocks.lock")) + and "." + or install_dir + ok, err, errcode = deps.fulfill_dependencies(rockspec, "dependencies", deps_mode, opts.verify, deplock_dir) + if err then return nil, err, errcode end + end + + ok, err = repos.deploy_files(name, version, repos.should_wrap_bin_scripts(rockspec), deps_mode) + if err then return nil, err end + + util.remove_scheduled_function(rollback) + rollback = util.schedule_function(function() + repos.delete_version(name, version, deps_mode) + end) + + ok, err = repos.run_hook(rockspec, "post_install") + if err then return nil, err end + + util.announce_install(rockspec) + util.remove_scheduled_function(rollback) + return name, version +end + +--- Installs the dependencies of a binary rock. +-- @param rock_file string: local or remote filename of a rock. +-- @param opts table: installation options +-- @return (string, string) or (nil, string, [string]): Name and version of +-- the rock whose dependencies were installed if succeeded or nil and an error message +-- followed by an error code. +function install.install_binary_rock_deps(rock_file, opts) + assert(type(rock_file) == "string") + assert(opts:type() == "install.opts") + + local name, version, arch = path.parse_name(rock_file) + if not name then + return nil, "Filename "..rock_file.." does not match format 'name-version-revision.arch.rock'." + end + + if arch ~= "all" and arch ~= cfg.arch then + return nil, "Incompatible architecture "..arch, "arch" + end + + local install_dir = path.install_dir(name, version) + + local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, install_dir, opts.verify) + if not ok then return nil, err, errcode end + + local rockspec, err = fetch.load_rockspec(path.rockspec_file(name, version)) + if err then + return nil, "Failed loading rockspec for installed package: "..err, errcode + end + + ok, err, errcode = deps.fulfill_dependencies(rockspec, "dependencies", opts.deps_mode, opts.verify, install_dir) + if err then return nil, err, errcode end + + util.printout() + util.printout("Successfully installed dependencies for " ..name.." "..version) + + return name, version +end + +local function install_rock_file_deps(filename, opts) + assert(opts:type() == "install.opts") + + local name, version = install.install_binary_rock_deps(filename, opts) + if not name then return nil, version end + + writer.check_dependencies(nil, opts.deps_mode) + return name, version +end + +local function install_rock_file(filename, opts) + assert(type(filename) == "string") + assert(opts:type() == "install.opts") + + local name, version = install.install_binary_rock(filename, opts) + if not name then return nil, version end + + if opts.no_doc then + util.remove_doc_dir(name, version) + end + + if (not opts.keep) and not cfg.keep_other_versions then + local ok, err, warn = remove.remove_other_versions(name, version, opts.force, opts.force_fast) + if not ok then + return nil, err + elseif warn then + util.printerr(err) + end + end + + writer.check_dependencies(nil, opts.deps_mode) + return name, version +end + +--- Driver function for the "install" command. +-- If an URL or pathname to a binary rock is given, fetches and installs it. +-- If a rockspec or a source rock is given, forwards the request to the "build" +-- command. +-- If a package name is given, forwards the request to "search" and, +-- if returned a result, installs the matching rock. +-- @return boolean or (nil, string, exitcode): True if installation was +-- successful, nil and an error message otherwise. exitcode is optionally returned. +function install.command(args) + if args.rock:match("%.rockspec$") or args.rock:match("%.src%.rock$") then + local build = require("luarocks.cmd.build") + return build.command(args) + elseif args.rock:match("%.rock$") then + local deps_mode = deps.get_deps_mode(args) + local opts = install.opts({ + namespace = args.namespace, + keep = not not args.keep, + force = not not args.force, + force_fast = not not args.force_fast, + no_doc = not not args.no_doc, + deps_mode = deps_mode, + verify = not not args.verify, + }) + if args.only_deps then + return install_rock_file_deps(args.rock, opts) + else + return install_rock_file(args.rock, opts) + end + else + local url, err = search.find_rock_checking_lua_versions( + queries.new(args.rock, args.namespace, args.version), + args.check_lua_versions) + if not url then + return nil, err + end + util.printout("Installing "..url) + args.rock = url + return install.command(args) + end +end + +install.needs_lock = function(args) + if args.pack_binary_rock then + return false + end + return true +end + +return install diff --git a/src/luarocks/cmd/lint.lua b/src/luarocks/cmd/lint.lua new file mode 100644 index 0000000..738503c --- /dev/null +++ b/src/luarocks/cmd/lint.lua @@ -0,0 +1,50 @@ + +--- Module implementing the LuaRocks "lint" command. +-- Utility function that checks syntax of the rockspec. +local lint = {} + +local util = require("luarocks.util") +local download = require("luarocks.download") +local fetch = require("luarocks.fetch") + +function lint.add_to_parser(parser) + local cmd = parser:command("lint", "Check syntax of a rockspec.\n\n".. + "Returns success if the text of the rockspec is syntactically correct, else failure.", + util.see_also()) + :summary("Check syntax of a rockspec.") + + cmd:argument("rockspec", "The rockspec to check.") +end + +function lint.command(args) + + local filename = args.rockspec + if not filename:match(".rockspec$") then + local err + filename, err = download.download("rockspec", filename:lower()) + if not filename then + return nil, err + end + end + + local rs, err = fetch.load_local_rockspec(filename) + if not rs then + return nil, "Failed loading rockspec: "..err + end + + local ok = true + + -- This should have been done in the type checker, + -- but it would break compatibility of other commands. + -- Making 'lint' alone be stricter shouldn't be a problem, + -- because extra-strict checks is what lint-type commands + -- are all about. + if not rs.description or not rs.description.license then + util.printerr("Rockspec has no description.license field.") + ok = false + end + + return ok, ok or filename.." failed consistency checks." +end + +return lint diff --git a/src/luarocks/cmd/list.lua b/src/luarocks/cmd/list.lua new file mode 100644 index 0000000..7b2682f --- /dev/null +++ b/src/luarocks/cmd/list.lua @@ -0,0 +1,96 @@ + +--- Module implementing the LuaRocks "list" command. +-- Lists currently installed rocks. +local list = {} + +local search = require("luarocks.search") +local queries = require("luarocks.queries") +local vers = require("luarocks.core.vers") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local path = require("luarocks.path") + +function list.add_to_parser(parser) + local cmd = parser:command("list", "List currently installed rocks.", util.see_also()) + + cmd:argument("filter", "A substring of a rock name to filter by.") + :args("?") + cmd:argument("version", "Rock version to filter by.") + :args("?") + + cmd:flag("--outdated", "List only rocks for which there is a higher ".. + "version available in the rocks server.") + cmd:flag("--porcelain", "Produce machine-friendly output.") +end + +local function check_outdated(trees, query) + local results_installed = {} + for _, tree in ipairs(trees) do + search.local_manifest_search(results_installed, path.rocks_dir(tree), query) + end + local outdated = {} + for name, versions in util.sortedpairs(results_installed) do + versions = util.keys(versions) + table.sort(versions, vers.compare_versions) + local latest_installed = versions[1] + + local query_available = queries.new(name:lower()) + local results_available, err = search.search_repos(query_available) + + if results_available[name] then + local available_versions = util.keys(results_available[name]) + table.sort(available_versions, vers.compare_versions) + local latest_available = available_versions[1] + local latest_available_repo = results_available[name][latest_available][1].repo + + if vers.compare_versions(latest_available, latest_installed) then + table.insert(outdated, { name = name, installed = latest_installed, available = latest_available, repo = latest_available_repo }) + end + end + end + return outdated +end + +local function list_outdated(trees, query, porcelain) + util.title("Outdated rocks:", porcelain) + local outdated = check_outdated(trees, query) + for _, item in ipairs(outdated) do + if porcelain then + util.printout(item.name, item.installed, item.available, item.repo) + else + util.printout(item.name) + util.printout(" "..item.installed.." < "..item.available.." at "..item.repo) + util.printout() + end + end + return true +end + +--- Driver function for "list" command. +-- @return boolean: True if succeeded, nil on errors. +function list.command(args) + local query = queries.new(args.filter and args.filter:lower() or "", args.namespace, args.version, true) + local trees = cfg.rocks_trees + local title = "Rocks installed for Lua "..cfg.lua_version + if args.tree then + trees = { args.tree } + title = title .. " in " .. args.tree + end + + if args.outdated then + return list_outdated(trees, query, args.porcelain) + end + + local results = {} + for _, tree in ipairs(trees) do + local ok, err, errcode = search.local_manifest_search(results, path.rocks_dir(tree), query) + if not ok and errcode ~= "open" then + util.warning(err) + end + end + util.title(title, args.porcelain) + search.print_result_tree(results, args.porcelain) + return true +end + +return list diff --git a/src/luarocks/cmd/make.lua b/src/luarocks/cmd/make.lua new file mode 100644 index 0000000..0b3db27 --- /dev/null +++ b/src/luarocks/cmd/make.lua @@ -0,0 +1,163 @@ + +--- Module implementing the LuaRocks "make" command. +-- Builds sources in the current directory, but unlike "build", +-- it does not fetch sources, etc., assuming everything is +-- available in the current directory. +local make = {} + +local build = require("luarocks.build") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local fetch = require("luarocks.fetch") +local pack = require("luarocks.pack") +local remove = require("luarocks.remove") +local deps = require("luarocks.deps") +local writer = require("luarocks.manif.writer") + +function make.cmd_options(parser) + parser:flag("--no-install", "Do not install the rock.") + parser:flag("--no-doc", "Install the rock without its documentation.") + parser:flag("--pack-binary-rock", "Do not install rock. Instead, produce a ".. + ".rock file with the contents of compilation in the current directory.") + parser:flag("--keep", "Do not remove previously installed versions of the ".. + "rock after building a new one. This behavior can be made permanent by ".. + "setting keep_other_versions=true in the configuration file.") + parser:flag("--force", "If --keep is not specified, force removal of ".. + "previously installed versions if it would break dependencies. ".. + "If rock is already installed, reinstall it anyway.") + parser:flag("--force-fast", "Like --force, but performs a forced removal ".. + "without reporting dependency issues.") + parser:flag("--verify", "Verify signature of the rockspec or src.rock being ".. + "built. If the rockspec or src.rock is being downloaded, LuaRocks will ".. + "attempt to download the signature as well. Otherwise, the signature ".. + "file should be already available locally in the same directory.\n".. + "You need the signer’s public key in your local keyring for this ".. + "option to work properly.") + parser:flag("--sign", "To be used with --pack-binary-rock. Also produce a ".. + "signature file for the generated .rock file.") + parser:flag("--check-lua-versions", "If the rock can't be found, check repository ".. + "and report if it is available for another Lua version.") + parser:flag("--pin", "Pin the exact dependencies used for the rockspec".. + "being built into a luarocks.lock file in the current directory.") + parser:flag("--no-manifest", "Skip creating/updating the manifest") + parser:flag("--only-deps --deps-only", "Install only the dependencies of the rock.") + util.deps_mode_option(parser) +end + +function make.add_to_parser(parser) + -- luacheck: push ignore 431 + local cmd = parser:command("make", [[ +Builds sources in the current directory, but unlike "build", it does not fetch +sources, etc., assuming everything is available in the current directory. If no +argument is given, it looks for a rockspec in the current directory and in +"rockspec/" and "rockspecs/" subdirectories, picking the rockspec with newest +version or without version name. If rockspecs for different rocks are found or +there are several rockspecs without version, you must specify which to use, +through the command-line. + +This command is useful as a tool for debugging rockspecs. +To install rocks, you'll normally want to use the "install" and "build" +commands. See the help on those for details. + +If the current directory contains a luarocks.lock file, it is used as the +authoritative source for exact version of dependencies. The --pin flag +overrides and recreates this file scanning dependency based on ranges. +]], util.see_also()) + :summary("Compile package in current directory using a rockspec.") + -- luacheck: pop + + cmd:argument("rockspec", "Rockspec for the rock to build.") + :args("?") + + make.cmd_options(cmd) +end + +--- Driver function for "make" command. +-- @return boolean or (nil, string, exitcode): True if build was successful; nil and an +-- error message otherwise. exitcode is optionally returned. +function make.command(args) + local rockspec_filename = args.rockspec + if not rockspec_filename then + local err + rockspec_filename, err = util.get_default_rockspec() + if not rockspec_filename then + return nil, err + end + end + if not rockspec_filename:match("rockspec$") then + return nil, "Invalid argument: 'make' takes a rockspec as a parameter. "..util.see_help("make") + end + + local rockspec, err, errcode = fetch.load_rockspec(rockspec_filename) + if not rockspec then + return nil, err + end + + local name, namespace = util.split_namespace(rockspec.name) + namespace = namespace or args.namespace + + local opts = build.opts({ + need_to_fetch = false, + minimal_mode = true, + deps_mode = deps.get_deps_mode(args), + build_only_deps = not not (args.only_deps and not args.pack_binary_rock), + namespace = namespace, + branch = args.branch, + verify = not not args.verify, + check_lua_versions = not not args.check_lua_versions, + pin = not not args.pin, + rebuild = true, + no_install = not not args.no_install + }) + + if args.sign and not args.pack_binary_rock then + return nil, "In the make command, --sign is meant to be used only with --pack-binary-rock" + end + + if args.no_install then + return build.build_rockspec(rockspec, opts) + elseif args.pack_binary_rock then + return pack.pack_binary_rock(name, namespace, rockspec.version, args.sign, function() + local name, version = build.build_rockspec(rockspec, opts) -- luacheck: ignore 431 + if name and args.no_doc then + util.remove_doc_dir(name, version) + end + return name, version + end) + else + local ok, err = build.build_rockspec(rockspec, opts) + if not ok then return nil, err end + local name, version = ok, err -- luacheck: ignore 421 + + if opts.build_only_deps then + util.printout("Stopping after installing dependencies for " ..name.." "..version) + util.printout() + return name, version + end + + if args.no_doc then + util.remove_doc_dir(name, version) + end + + if (not args.keep) and not cfg.keep_other_versions then + local ok, err, warn = remove.remove_other_versions(name, version, args.force, args.force_fast) + if not ok then + return nil, err + elseif warn then + util.printerr(warn) + end + end + + writer.check_dependencies(nil, deps.get_deps_mode(args)) + return name, version + end +end + +make.needs_lock = function(args) + if args.pack_binary_rock or args.no_install then + return false + end + return true +end + +return make diff --git a/src/luarocks/cmd/new_version.lua b/src/luarocks/cmd/new_version.lua new file mode 100644 index 0000000..ccba933 --- /dev/null +++ b/src/luarocks/cmd/new_version.lua @@ -0,0 +1,228 @@ + +--- Module implementing the LuaRocks "new_version" command. +-- Utility function that writes a new rockspec, updating data from a previous one. +local new_version = {} + +local util = require("luarocks.util") +local download = require("luarocks.download") +local fetch = require("luarocks.fetch") +local persist = require("luarocks.persist") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local type_rockspec = require("luarocks.type.rockspec") + +function new_version.add_to_parser(parser) + local cmd = parser:command("new_version", [[ +This is a utility function that writes a new rockspec, updating data from a +previous one. + +If a package name is given, it downloads the latest rockspec from the default +server. If a rockspec is given, it uses it instead. If no argument is given, it +looks for a rockspec same way 'luarocks make' does. + +If the version number is not given and tag is passed using --tag, it is used as +the version, with 'v' removed from beginning. Otherwise, it only increments the +revision number of the given (or downloaded) rockspec. + +If a URL is given, it replaces the one from the old rockspec with the given URL. +If a URL is not given and a new version is given, it tries to guess the new URL +by replacing occurrences of the version number in the URL or tag; if the guessed +URL is invalid, the old URL is restored. It also tries to download the new URL +to determine the new MD5 checksum. + +If a tag is given, it replaces the one from the old rockspec. If there is an old +tag but no new one passed, it is guessed in the same way URL is. + +If a directory is not given, it defaults to the current directory. + +WARNING: it writes the new rockspec to the given directory, overwriting the file +if it already exists.]], util.see_also()) + :summary("Auto-write a rockspec for a new version of a rock.") + + cmd:argument("rock", "Package name or rockspec.") + :args("?") + cmd:argument("new_version", "New version of the rock.") + :args("?") + cmd:argument("new_url", "New URL of the rock.") + :args("?") + + cmd:option("--dir", "Output directory for the new rockspec.") + cmd:option("--tag", "New SCM tag.") +end + + +local function try_replace(tbl, field, old, new) + if not tbl[field] then + return false + end + local old_field = tbl[field] + local new_field = tbl[field]:gsub(old, new) + if new_field ~= old_field then + util.printout("Guessing new '"..field.."' field as "..new_field) + tbl[field] = new_field + return true + end + return false +end + +-- Try to download source file using URL from a rockspec. +-- If it specified MD5, update it. +-- @return (true, false) if MD5 was not specified or it stayed same, +-- (true, true) if MD5 changed, (nil, string) on error. +local function check_url_and_update_md5(out_rs, invalid_is_error) + local file, temp_dir = fetch.fetch_url_at_temp_dir(out_rs.source.url, "luarocks-new-version-"..out_rs.package) + if not file then + if invalid_is_error then + return nil, "invalid URL - "..temp_dir + end + util.warning("invalid URL - "..temp_dir) + return true, false + end + do + local inferred_dir, found_dir = fetch.find_base_dir(file, temp_dir, out_rs.source.url, out_rs.source.dir) + if not inferred_dir then + return nil, found_dir + end + + if found_dir and found_dir ~= inferred_dir then + out_rs.source.dir = found_dir + end + end + if file then + if out_rs.source.md5 then + util.printout("File successfully downloaded. Updating MD5 checksum...") + local new_md5, err = fs.get_md5(file) + if not new_md5 then + return nil, err + end + local old_md5 = out_rs.source.md5 + out_rs.source.md5 = new_md5 + return true, new_md5 ~= old_md5 + else + util.printout("File successfully downloaded.") + return true, false + end + end +end + +local function update_source_section(out_rs, url, tag, old_ver, new_ver) + if tag then + out_rs.source.tag = tag + end + if url then + out_rs.source.url = url + return check_url_and_update_md5(out_rs) + end + if new_ver == old_ver then + return true + end + if out_rs.source.dir then + try_replace(out_rs.source, "dir", old_ver, new_ver) + end + if out_rs.source.file then + try_replace(out_rs.source, "file", old_ver, new_ver) + end + + local old_url = out_rs.source.url + if try_replace(out_rs.source, "url", old_ver, new_ver) then + local ok, md5_changed = check_url_and_update_md5(out_rs, true) + if ok then + return ok, md5_changed + end + out_rs.source.url = old_url + end + if tag or try_replace(out_rs.source, "tag", old_ver, new_ver) then + return true + end + -- Couldn't replace anything significant, use the old URL. + local ok, md5_changed = check_url_and_update_md5(out_rs) + if not ok then + return nil, md5_changed + end + if md5_changed then + util.warning("URL is the same, but MD5 has changed. Old rockspec is broken.") + end + return true +end + +function new_version.command(args) + if not args.rock then + local err + args.rock, err = util.get_default_rockspec() + if not args.rock then + return nil, err + end + end + + local filename, err + if args.rock:match("rockspec$") then + filename, err = fetch.fetch_url(args.rock) + if not filename then + return nil, err + end + else + filename, err = download.download("rockspec", args.rock:lower()) + if not filename then + return nil, err + end + end + + local valid_rs, err = fetch.load_rockspec(filename) + if not valid_rs then + return nil, err + end + + local old_ver, old_rev = valid_rs.version:match("(.*)%-(%d+)$") + local new_ver, new_rev + + if args.tag and not args.new_version then + args.new_version = args.tag:gsub("^v", "") + end + + local out_dir + if args.dir then + out_dir = dir.normalize(args.dir) + end + + if args.new_version then + new_ver, new_rev = args.new_version:match("(.*)%-(%d+)$") + new_rev = tonumber(new_rev) + if not new_rev then + new_ver = args.new_version + new_rev = 1 + end + else + new_ver = old_ver + new_rev = tonumber(old_rev) + 1 + end + local new_rockver = new_ver:gsub("-", "") + + local out_rs, err = persist.load_into_table(filename) + local out_name = out_rs.package:lower() + out_rs.version = new_rockver.."-"..new_rev + + local ok, err = update_source_section(out_rs, args.new_url, args.tag, old_ver, new_ver) + if not ok then return nil, err end + + if out_rs.build and out_rs.build.type == "module" then + out_rs.build.type = "builtin" + end + + local out_filename = out_name.."-"..new_rockver.."-"..new_rev..".rockspec" + if out_dir then + out_filename = dir.path(out_dir, out_filename) + fs.make_dir(out_dir) + end + persist.save_from_table(out_filename, out_rs, type_rockspec.order) + + util.printout("Wrote "..out_filename) + + local valid_out_rs, err = fetch.load_local_rockspec(out_filename) + if not valid_out_rs then + return nil, "Failed loading generated rockspec: "..err + end + + return true +end + +return new_version diff --git a/src/luarocks/cmd/pack.lua b/src/luarocks/cmd/pack.lua new file mode 100644 index 0000000..29a43e7 --- /dev/null +++ b/src/luarocks/cmd/pack.lua @@ -0,0 +1,36 @@ + +--- Module implementing the LuaRocks "pack" command. +-- Creates a rock, packing sources or binaries. +local cmd_pack = {} + +local util = require("luarocks.util") +local pack = require("luarocks.pack") +local queries = require("luarocks.queries") + +function cmd_pack.add_to_parser(parser) + local cmd = parser:command("pack", "Create a rock, packing sources or binaries.", util.see_also()) + + cmd:argument("rock", "A rockspec file, for creating a source rock, or the ".. + "name of an installed package, for creating a binary rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "A version may be given if the first argument is a rock name.") + :args("?") + + cmd:flag("--sign", "Produce a signature file as well.") +end + +--- Driver function for the "pack" command. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +function cmd_pack.command(args) + local file, err + if args.rock:match(".*%.rockspec") then + file, err = pack.pack_source_rock(args.rock) + else + local query = queries.new(args.rock, args.namespace, args.version) + file, err = pack.pack_installed_rock(query, args.tree) + end + return pack.report_and_sign_local_file(file, err, args.sign) +end + +return cmd_pack diff --git a/src/luarocks/cmd/path.lua b/src/luarocks/cmd/path.lua new file mode 100644 index 0000000..ba34655 --- /dev/null +++ b/src/luarocks/cmd/path.lua @@ -0,0 +1,83 @@ + +--- @module luarocks.path_cmd +-- Driver for the `luarocks path` command. +local path_cmd = {} + +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") + +function path_cmd.add_to_parser(parser) + local cmd = parser:command("path", [[ +Returns the package path currently configured for this installation +of LuaRocks, formatted as shell commands to update LUA_PATH and LUA_CPATH. + +On Unix systems, you may run: + eval `luarocks path` +And on Windows: + luarocks path > "%temp%\_lrp.bat" + call "%temp%\_lrp.bat" && del "%temp%\_lrp.bat"]], + util.see_also()) + :summary("Return the currently configured package path.") + + cmd:flag("--no-bin", "Do not export the PATH variable.") + cmd:flag("--append", "Appends the paths to the existing paths. Default is ".. + "to prefix the LR paths to the existing paths.") + cmd:flag("--lr-path", "Prints Lua path components defined by the configured rocks trees " .. + "(not formatted as a shell command)") + cmd:flag("--lr-cpath", "Prints Lua cpath components defined by the configured rocks trees " .. + "(not formatted as a shell command)") + cmd:flag("--full", "By default, --lr-path and --lr-cpath only include the paths " .. + "derived by the LuaRocks rocks_trees. Using --full includes any other components " .. + "defined in your system's package.(c)path, either via the running interpreter's " .. + "default paths or via LUA_(C)PATH(_5_x) environment variables (in short, using " .. + "--full produces the same lists as shown in the shell outputs of 'luarocks path').") + cmd:flag("--lr-bin", "Exports the system path (not formatted as shell command).") + cmd:flag("--bin"):hidden(true) +end + +--- Driver function for "path" command. +-- @return boolean This function always succeeds. +function path_cmd.command(args) + local lr_path, lr_cpath, lr_bin = cfg.package_paths(args.tree) + local path_sep = cfg.export_path_separator + + local full_list = ((not args.lr_path) and (not args.lr_cpath) and (not args.lr_bin)) + or args.full + + local clean_path = util.cleanup_path(os.getenv("PATH") or "", path_sep, nil, true) + + if full_list then + if args.append then + lr_path = package.path .. ";" .. lr_path + lr_cpath = package.cpath .. ";" .. lr_cpath + lr_bin = clean_path .. path_sep .. lr_bin + else + lr_path = lr_path.. ";" .. package.path + lr_cpath = lr_cpath .. ";" .. package.cpath + lr_bin = lr_bin .. path_sep .. clean_path + end + end + + if args.lr_path then + util.printout(util.cleanup_path(lr_path, ';', cfg.lua_version, true)) + return true + elseif args.lr_cpath then + util.printout(util.cleanup_path(lr_cpath, ';', cfg.lua_version, true)) + return true + elseif args.lr_bin then + util.printout(util.cleanup_path(lr_bin, path_sep, nil, true)) + return true + end + + local lpath_var, lcpath_var = util.lua_path_variables() + + util.printout(fs.export_cmd(lpath_var, util.cleanup_path(lr_path, ';', cfg.lua_version, args.append))) + util.printout(fs.export_cmd(lcpath_var, util.cleanup_path(lr_cpath, ';', cfg.lua_version, args.append))) + if not args.no_bin then + util.printout(fs.export_cmd("PATH", util.cleanup_path(lr_bin, path_sep, nil, args.append))) + end + return true +end + +return path_cmd diff --git a/src/luarocks/cmd/purge.lua b/src/luarocks/cmd/purge.lua new file mode 100644 index 0000000..30811dd --- /dev/null +++ b/src/luarocks/cmd/purge.lua @@ -0,0 +1,73 @@ + +--- Module implementing the LuaRocks "purge" command. +-- Remove all rocks from a given tree. +local purge = {} + +local util = require("luarocks.util") +local path = require("luarocks.path") +local search = require("luarocks.search") +local vers = require("luarocks.core.vers") +local repos = require("luarocks.repos") +local writer = require("luarocks.manif.writer") +local cfg = require("luarocks.core.cfg") +local remove = require("luarocks.remove") +local queries = require("luarocks.queries") + +function purge.add_to_parser(parser) + -- luacheck: push ignore 431 + local cmd = parser:command("purge", [[ +This command removes rocks en masse from a given tree. +By default, it removes all rocks from a tree. + +The --tree option is mandatory: luarocks purge does not assume a default tree.]], + util.see_also()) + :summary("Remove all installed rocks from a tree.") + -- luacheck: pop + + cmd:flag("--old-versions", "Keep the highest-numbered version of each ".. + "rock and remove the other ones. By default it only removes old ".. + "versions if they are not needed as dependencies. This can be ".. + "overridden with the flag --force.") + cmd:flag("--force", "If --old-versions is specified, force removal of ".. + "previously installed versions if it would break dependencies.") + cmd:flag("--force-fast", "Like --force, but performs a forced removal ".. + "without reporting dependency issues.") +end + +function purge.command(args) + local tree = args.tree + + local results = {} + search.local_manifest_search(results, path.rocks_dir(tree), queries.all()) + + local sort = function(a,b) return vers.compare_versions(b,a) end + if args.old_versions then + sort = vers.compare_versions + end + + for package, versions in util.sortedpairs(results) do + for version, _ in util.sortedpairs(versions, sort) do + if args.old_versions then + util.printout("Keeping "..package.." "..version.."...") + local ok, err, warn = remove.remove_other_versions(package, version, args.force, args.force_fast) + if not ok then + util.printerr(err) + elseif warn then + util.printerr(err) + end + break + else + util.printout("Removing "..package.." "..version.."...") + local ok, err = repos.delete_version(package, version, "none", true) + if not ok then + util.printerr(err) + end + end + end + end + return writer.make_manifest(cfg.rocks_dir, "one") +end + +purge.needs_lock = function() return true end + +return purge diff --git a/src/luarocks/cmd/remove.lua b/src/luarocks/cmd/remove.lua new file mode 100644 index 0000000..8b11bcd --- /dev/null +++ b/src/luarocks/cmd/remove.lua @@ -0,0 +1,72 @@ + +--- Module implementing the LuaRocks "remove" command. +-- Uninstalls rocks. +local cmd_remove = {} + +local remove = require("luarocks.remove") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local search = require("luarocks.search") +local path = require("luarocks.path") +local deps = require("luarocks.deps") +local writer = require("luarocks.manif.writer") +local queries = require("luarocks.queries") + +function cmd_remove.add_to_parser(parser) + -- luacheck: push ignore 431 + local cmd = parser:command("remove", [[ +Uninstall a rock. + +If a version is not given, try to remove all versions at once. +Will only perform the removal if it does not break dependencies. +To override this check and force the removal, use --force or --force-fast.]], + util.see_also()) + :summary("Uninstall a rock.") + -- luacheck: pop + + cmd:argument("rock", "Name of the rock to be uninstalled.") + :action(util.namespaced_name_action) + cmd:argument("version", "Version of the rock to uninstall.") + :args("?") + + cmd:flag("--force", "Force removal if it would break dependencies.") + cmd:flag("--force-fast", "Perform a forced removal without reporting dependency issues.") + util.deps_mode_option(cmd) +end + +--- Driver function for the "remove" command. +-- @return boolean or (nil, string, exitcode): True if removal was +-- successful, nil and an error message otherwise. exitcode is optionally returned. +function cmd_remove.command(args) + local name = args.rock + local deps_mode = deps.get_deps_mode(args) + + local rock_type = name:match("%.(rock)$") or name:match("%.(rockspec)$") + local version = args.version + local filename = name + if rock_type then + name, version = path.parse_name(filename) + if not name then return nil, "Invalid "..rock_type.." filename: "..filename end + end + + name = name:lower() + + local results = {} + search.local_manifest_search(results, cfg.rocks_dir, queries.new(name, args.namespace, version)) + if not results[name] then + local rock = util.format_rock_name(name, args.namespace, version) + return nil, "Could not find rock '"..rock.."' in "..path.rocks_tree_to_string(cfg.root_dir) + end + + local ok, err = remove.remove_search_results(results, name, deps_mode, args.force, args.force_fast) + if not ok then + return nil, err + end + + writer.check_dependencies(nil, deps.get_deps_mode(args)) + return true +end + +cmd_remove.needs_lock = function() return true end + +return cmd_remove diff --git a/src/luarocks/cmd/search.lua b/src/luarocks/cmd/search.lua new file mode 100644 index 0000000..6cab6d8 --- /dev/null +++ b/src/luarocks/cmd/search.lua @@ -0,0 +1,84 @@ + +--- Module implementing the LuaRocks "search" command. +-- Queries LuaRocks servers. +local cmd_search = {} + +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local search = require("luarocks.search") +local queries = require("luarocks.queries") +local results = require("luarocks.results") + +function cmd_search.add_to_parser(parser) + local cmd = parser:command("search", "Query the LuaRocks servers.", util.see_also()) + + cmd:argument("name", "Name of the rock to search for.") + :args("?") + :action(util.namespaced_name_action) + cmd:argument("version", "Rock version to search for.") + :args("?") + + cmd:flag("--source", "Return only rockspecs and source rocks, to be used ".. + 'with the "build" command.') + cmd:flag("--binary", "Return only pure Lua and binary rocks (rocks that ".. + 'can be used with the "install" command without requiring a C toolchain).') + cmd:flag("--all", "List all contents of the server that are suitable to ".. + "this platform, do not filter by name.") + cmd:flag("--porcelain", "Return a machine readable format.") +end + +--- Splits a list of search results into two lists, one for "source" results +-- to be used with the "build" command, and one for "binary" results to be +-- used with the "install" command. +-- @param result_tree table: A search results table. +-- @return (table, table): Two tables, one for source and one for binary +-- results. +local function split_source_and_binary_results(result_tree) + local sources, binaries = {}, {} + for name, versions in pairs(result_tree) do + for version, repositories in pairs(versions) do + for _, repo in ipairs(repositories) do + local where = sources + if repo.arch == "all" or repo.arch == cfg.arch then + where = binaries + end + local entry = results.new(name, version, repo.repo, repo.arch) + search.store_result(where, entry) + end + end + end + return sources, binaries +end + +--- Driver function for "search" command. +-- @return boolean or (nil, string): True if build was successful; nil and an +-- error message otherwise. +function cmd_search.command(args) + local name = args.name + + if args.all then + name, args.version = "", nil + end + + if not args.name and not args.all then + return nil, "Enter name and version or use --all. "..util.see_help("search") + end + + local query = queries.new(name, args.namespace, args.version, true) + local result_tree, err = search.search_repos(query) + local porcelain = args.porcelain + local full_name = util.format_rock_name(name, args.namespace, args.version) + util.title(full_name .. " - Search results for Lua "..cfg.lua_version..":", porcelain, "=") + local sources, binaries = split_source_and_binary_results(result_tree) + if next(sources) and not args.binary then + util.title("Rockspecs and source rocks:", porcelain) + search.print_result_tree(sources, porcelain) + end + if next(binaries) and not args.source then + util.title("Binary and pure-Lua rocks:", porcelain) + search.print_result_tree(binaries, porcelain) + end + return true +end + +return cmd_search diff --git a/src/luarocks/cmd/show.lua b/src/luarocks/cmd/show.lua new file mode 100644 index 0000000..88cbbad --- /dev/null +++ b/src/luarocks/cmd/show.lua @@ -0,0 +1,314 @@ +--- Module implementing the LuaRocks "show" command. +-- Shows information about an installed rock. +local show = {} + +local queries = require("luarocks.queries") +local search = require("luarocks.search") +local dir = require("luarocks.core.dir") +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local path = require("luarocks.path") +local fetch = require("luarocks.fetch") +local manif = require("luarocks.manif") +local repos = require("luarocks.repos") + +function show.add_to_parser(parser) + local cmd = parser:command("show", [[ +Show information about an installed rock. + +Without any flags, show all module information. +With flags, return only the desired information.]], util.see_also()) + :summary("Show information about an installed rock.") + + cmd:argument("rock", "Name of an installed rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "Rock version.") + :args("?") + + cmd:flag("--home", "Show home page of project.") + cmd:flag("--modules", "Show all modules provided by the package as used by require().") + cmd:flag("--deps", "Show packages the package depends on.") + cmd:flag("--build-deps", "Show build-only dependencies for the package.") + cmd:flag("--test-deps", "Show dependencies for testing the package.") + cmd:flag("--rockspec", "Show the full path of the rockspec file.") + cmd:flag("--mversion", "Show the package version.") + cmd:flag("--rock-tree", "Show local tree where rock is installed.") + cmd:flag("--rock-namespace", "Show rock namespace.") + cmd:flag("--rock-dir", "Show data directory of the installed rock.") + cmd:flag("--rock-license", "Show rock license.") + cmd:flag("--issues", "Show URL for project's issue tracker.") + cmd:flag("--labels", "List the labels of the rock.") + cmd:flag("--porcelain", "Produce machine-friendly output.") +end + +local friendly_template = [[ + : +?namespace:${namespace}/${package} ${version} - ${summary} +!namespace:${package} ${version} - ${summary} + : +*detailed :${detailed} +?detailed : +?license :License: \t${license} +?homepage :Homepage: \t${homepage} +?issues :Issues: \t${issues} +?labels :Labels: \t${labels} +?location :Installed in: \t${location} +?commands : +?commands :Commands: +*commands :\t${name} (${file}) +?modules : +?modules :Modules: +*modules :\t${name} (${file}) +?bdeps : +?bdeps :Has build dependency on: +*bdeps :\t${name} (${label}) +?tdeps : +?tdeps :Tests depend on: +*tdeps :\t${name} (${label}) +?deps : +?deps :Depends on: +*deps :\t${name} (${label}) +?ideps : +?ideps :Indirectly pulling: +*ideps :\t${name} (${label}) + : +]] + +local porcelain_template = [[ +?namespace:namespace\t${namespace} +?package :package\t${package} +?version :version\t${version} +?summary :summary\t${summary} +*detailed :detailed\t${detailed} +?license :license\t${license} +?homepage :homepage\t${homepage} +?issues :issues\t${issues} +?labels :labels\t${labels} +?location :location\t${location} +*commands :command\t${name}\t${file} +*modules :module\t${name}\t${file} +*bdeps :build_dependency\t${name}\t${label} +*tdeps :test_dependency\t${name}\t${label} +*deps :dependency\t${name}\t${label} +*ideps :indirect_dependency\t${name}\t${label} +]] + +local function keys_as_string(t, sep) + local keys = util.keys(t) + table.sort(keys) + return table.concat(keys, sep or " ") +end + +local function word_wrap(line) + local width = tonumber(os.getenv("COLUMNS")) or 80 + if width > 80 then width = 80 end + if #line > width then + local brk = width + while brk > 0 and line:sub(brk, brk) ~= " " do + brk = brk - 1 + end + if brk > 0 then + return line:sub(1, brk-1) .. "\n" .. word_wrap(line:sub(brk+1)) + end + end + return line +end + +local function format_text(text) + text = text:gsub("^%s*",""):gsub("%s$", ""):gsub("\n[ \t]+","\n"):gsub("([^\n])\n([^\n])","%1 %2") + local paragraphs = util.split_string(text, "\n\n") + for n, line in ipairs(paragraphs) do + paragraphs[n] = word_wrap(line) + end + return (table.concat(paragraphs, "\n\n"):gsub("%s$", "")) +end + +local function installed_rock_label(dep, tree) + local installed, version + local rocks_provided = util.get_rocks_provided() + if rocks_provided[dep.name] then + installed, version = true, rocks_provided[dep.name] + else + installed, version = search.pick_installed_rock(dep, tree) + end + return installed and "using "..version or "missing" +end + +local function render(template, data) + local out = {} + for cmd, var, line in template:gmatch("(.)([a-z]*)%s*:([^\n]*)\n") do + line = line:gsub("\\t", "\t") + local d = data[var] + if cmd == " " then + table.insert(out, line) + elseif cmd == "?" or cmd == "*" or cmd == "!" then + if (cmd == "!" and d == nil) + or (cmd ~= "!" and (type(d) == "string" + or (type(d) == "table" and next(d)))) then + local n = cmd == "*" and #d or 1 + for i = 1, n do + local tbl = cmd == "*" and d[i] or data + if type(tbl) == "string" then + tbl = tbl:gsub("%%", "%%%%") + end + table.insert(out, (line:gsub("${([a-z]+)}", tbl))) + end + end + end + end + return table.concat(out, "\n") +end + +local function adjust_path(name, version, basedir, pathname, suffix) + pathname = dir.path(basedir, pathname) + local vpathname = path.versioned_name(pathname, basedir, name, version) + return (fs.exists(vpathname) + and vpathname + or pathname) .. (suffix or "") +end + +local function modules_to_list(name, version, repo) + local ret = {} + local rock_manifest = manif.load_rock_manifest(name, version, repo) + + local lua_dir = path.deploy_lua_dir(repo) + local lib_dir = path.deploy_lib_dir(repo) + repos.recurse_rock_manifest_entry(rock_manifest.lua, function(pathname) + table.insert(ret, { + name = path.path_to_module(pathname), + file = adjust_path(name, version, lua_dir, pathname), + }) + end) + repos.recurse_rock_manifest_entry(rock_manifest.lib, function(pathname) + table.insert(ret, { + name = path.path_to_module(pathname), + file = adjust_path(name, version, lib_dir, pathname), + }) + end) + table.sort(ret, function(a, b) + if a.name == b.name then + return a.file < b.file + end + return a.name < b.name + end) + return ret +end + +local function commands_to_list(name, version, repo) + local ret = {} + local rock_manifest = manif.load_rock_manifest(name, version, repo) + + local bin_dir = path.deploy_bin_dir(repo) + repos.recurse_rock_manifest_entry(rock_manifest.bin, function(pathname) + table.insert(ret, { + name = name, + file = adjust_path(name, version, bin_dir, pathname, cfg.wrapper_suffix), + }) + end) + table.sort(ret, function(a, b) + if a.name == b.name then + return a.file < b.file + end + return a.name < b.name + end) + return ret +end + +local function deps_to_list(dependencies, tree) + local ret = {} + for _, dep in ipairs(dependencies or {}) do + table.insert(ret, { name = tostring(dep), label = installed_rock_label(dep, tree) }) + end + return ret +end + +local function indirect_deps(mdeps, rdeps, tree) + local ret = {} + local direct_deps = {} + for _, dep in ipairs(rdeps) do + direct_deps[dep] = true + end + for dep_name in util.sortedpairs(mdeps or {}) do + if not direct_deps[dep_name] then + table.insert(ret, { name = tostring(dep_name), label = installed_rock_label(queries.new(dep_name), tree) }) + end + end + return ret +end + +local function show_rock(template, namespace, name, version, rockspec, repo, minfo, tree) + local desc = rockspec.description or {} + local data = { + namespace = namespace, + package = rockspec.package, + version = rockspec.version, + summary = desc.summary or "", + detailed = desc.detailed and util.split_string(format_text(desc.detailed), "\n"), + license = desc.license, + homepage = desc.homepage, + issues = desc.issues_url, + labels = desc.labels and table.concat(desc.labels, ", "), + location = path.rocks_tree_to_string(repo), + commands = commands_to_list(name, version, repo), + modules = modules_to_list(name, version, repo), + bdeps = deps_to_list(rockspec.build_dependencies, tree), + tdeps = deps_to_list(rockspec.test_dependencies, tree), + deps = deps_to_list(rockspec.dependencies, tree), + ideps = indirect_deps(minfo.dependencies, rockspec.dependencies, tree), + } + util.printout(render(template, data)) +end + +--- Driver function for "show" command. +-- @return boolean: True if succeeded, nil on errors. +function show.command(args) + local query = queries.new(args.rock, args.namespace, args.version, true) + + local name, version, repo, repo_url = search.pick_installed_rock(query, args.tree) + if not name then + return nil, version + end + local tree = path.rocks_tree_to_string(repo) + local directory = path.install_dir(name, version, repo) + local namespace = path.read_namespace(name, version, tree) + local rockspec_file = path.rockspec_file(name, version, repo) + local rockspec, err = fetch.load_local_rockspec(rockspec_file) + if not rockspec then return nil,err end + + local descript = rockspec.description or {} + local manifest, err = manif.load_manifest(repo_url) + if not manifest then return nil,err end + local minfo = manifest.repository[name][version][1] + + if args.rock_tree then util.printout(tree) + elseif args.rock_namespace then util.printout(namespace) + elseif args.rock_dir then util.printout(directory) + elseif args.home then util.printout(descript.homepage) + elseif args.rock_license then util.printout(descript.license) + elseif args.issues then util.printout(descript.issues_url) + elseif args.labels then util.printout(descript.labels and table.concat(descript.labels, "\n")) + elseif args.modules then util.printout(keys_as_string(minfo.modules, "\n")) + elseif args.deps then + for _, dep in ipairs(rockspec.dependencies) do + util.printout(tostring(dep)) + end + elseif args.build_deps then + for _, dep in ipairs(rockspec.build_dependencies) do + util.printout(tostring(dep)) + end + elseif args.test_deps then + for _, dep in ipairs(rockspec.test_dependencies) do + util.printout(tostring(dep)) + end + elseif args.rockspec then util.printout(rockspec_file) + elseif args.mversion then util.printout(version) + elseif args.porcelain then + show_rock(porcelain_template, namespace, name, version, rockspec, repo, minfo, args.tree) + else + show_rock(friendly_template, namespace, name, version, rockspec, repo, minfo, args.tree) + end + return true +end + +return show diff --git a/src/luarocks/cmd/test.lua b/src/luarocks/cmd/test.lua new file mode 100644 index 0000000..b353bd8 --- /dev/null +++ b/src/luarocks/cmd/test.lua @@ -0,0 +1,48 @@ + +--- Module implementing the LuaRocks "test" command. +-- Tests a rock, compiling its C parts if any. +local cmd_test = {} + +local util = require("luarocks.util") +local test = require("luarocks.test") + +function cmd_test.add_to_parser(parser) + local cmd = parser:command("test", [[ +Run the test suite for the Lua project in the current directory. + +If the first argument is a rockspec, it will use it to determine the parameters +for running tests; otherwise, it will attempt to detect the rockspec. + +Any additional arguments are forwarded to the test suite. +To make sure that test suite flags are not interpreted as LuaRocks flags, use -- +to separate LuaRocks arguments from test suite arguments.]], + util.see_also()) + :summary("Run the test suite in the current directory.") + + cmd:argument("rockspec", "Project rockspec.") + :args("?") + cmd:argument("args", "Test suite arguments.") + :args("*") + cmd:flag("--prepare", "Only install dependencies needed for testing only, but do not run the test") + + cmd:option("--test-type", "Specify the test suite type manually if it was ".. + "not specified in the rockspec and it could not be auto-detected.") + :argname("") +end + +function cmd_test.command(args) + if args.rockspec and args.rockspec:match("rockspec$") then + return test.run_test_suite(args.rockspec, args.test_type, args.args, args.prepare) + end + + table.insert(args.args, 1, args.rockspec) + + local rockspec, err = util.get_default_rockspec() + if not rockspec then + return nil, err + end + + return test.run_test_suite(rockspec, args.test_type, args.args, args.prepare) +end + +return cmd_test diff --git a/src/luarocks/cmd/unpack.lua b/src/luarocks/cmd/unpack.lua new file mode 100644 index 0000000..a0ade4f --- /dev/null +++ b/src/luarocks/cmd/unpack.lua @@ -0,0 +1,169 @@ + +--- Module implementing the LuaRocks "unpack" command. +-- Unpack the contents of a rock. +local unpack = {} + +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local build = require("luarocks.build") +local dir = require("luarocks.dir") +local search = require("luarocks.search") + +function unpack.add_to_parser(parser) + local cmd = parser:command("unpack", [[ +Unpacks the contents of a rock in a newly created directory. +Argument may be a rock file, or the name of a rock in a rocks server. +In the latter case, the rock version may be given as a second argument.]], + util.see_also()) + :summary("Unpack the contents of a rock.") + + cmd:argument("rock", "A rock file or the name of a rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "Rock version.") + :args("?") + + cmd:flag("--force", "Unpack files even if the output directory already exists.") + cmd:flag("--check-lua-versions", "If the rock can't be found, check repository ".. + "and report if it is available for another Lua version.") +end + +--- Load a rockspec file to the given directory, fetches the source +-- files specified in the rockspec, and unpack them inside the directory. +-- @param rockspec_file string: The URL for a rockspec file. +-- @param dir_name string: The directory where to store and unpack files. +-- @return table or (nil, string): the loaded rockspec table or +-- nil and an error message. +local function unpack_rockspec(rockspec_file, dir_name) + assert(type(rockspec_file) == "string") + assert(type(dir_name) == "string") + + local rockspec, err = fetch.load_rockspec(rockspec_file) + if not rockspec then + return nil, "Failed loading rockspec "..rockspec_file..": "..err + end + local ok, err = fs.change_dir(dir_name) + if not ok then return nil, err end + local ok, sources_dir = fetch.fetch_sources(rockspec, true, ".") + if not ok then + return nil, sources_dir + end + ok, err = fs.change_dir(sources_dir) + if not ok then return nil, err end + ok, err = build.apply_patches(rockspec) + fs.pop_dir() + if not ok then return nil, err end + return rockspec +end + +--- Load a .rock file to the given directory and unpack it inside it. +-- @param rock_file string: The URL for a .rock file. +-- @param dir_name string: The directory where to unpack. +-- @param kind string: the kind of rock file, as in the second-level +-- extension in the rock filename (eg. "src", "all", "linux-x86") +-- @return table or (nil, string): the loaded rockspec table or +-- nil and an error message. +local function unpack_rock(rock_file, dir_name, kind) + assert(type(rock_file) == "string") + assert(type(dir_name) == "string") + + local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, dir_name) + if not ok then + return nil, err, errcode + end + ok, err = fs.change_dir(dir_name) + if not ok then return nil, err end + local rockspec_file = dir_name..".rockspec" + local rockspec, err = fetch.load_rockspec(rockspec_file) + if not rockspec then + return nil, "Failed loading rockspec "..rockspec_file..": "..err + end + if kind == "src" then + if rockspec.source.file then + local ok, err = fs.unpack_archive(rockspec.source.file) + if not ok then return nil, err end + ok, err = fetch.find_rockspec_source_dir(rockspec, ".") + if not ok then return nil, err end + ok, err = fs.change_dir(rockspec.source.dir) + if not ok then return nil, err end + ok, err = build.apply_patches(rockspec) + fs.pop_dir() + if not ok then return nil, err end + end + end + return rockspec +end + +--- Create a directory and perform the necessary actions so that +-- the sources for the rock and its rockspec are unpacked inside it, +-- laid out properly so that the 'make' command is able to build the module. +-- @param file string: A rockspec or .rock URL. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +local function run_unpacker(file, force) + assert(type(file) == "string") + + local base_name = dir.base_name(file) + local dir_name, kind, extension = base_name:match("(.*)%.([^.]+)%.(rock)$") + if not extension then + dir_name, extension = base_name:match("(.*)%.(rockspec)$") + kind = "rockspec" + end + if not extension then + return nil, file.." does not seem to be a valid filename." + end + + local exists = fs.exists(dir_name) + if exists and not force then + return nil, "Directory "..dir_name.." already exists." + end + if not exists then + local ok, err = fs.make_dir(dir_name) + if not ok then return nil, err end + end + local rollback = util.schedule_function(fs.delete, fs.absolute_name(dir_name)) + + local rockspec, err + if extension == "rock" then + rockspec, err = unpack_rock(file, dir_name, kind) + elseif extension == "rockspec" then + rockspec, err = unpack_rockspec(file, dir_name) + end + if not rockspec then + return nil, err + end + if kind == "src" or kind == "rockspec" then + fetch.find_rockspec_source_dir(rockspec, ".") + if rockspec.source.dir ~= "." then + local ok = fs.copy(rockspec.local_abs_filename, rockspec.source.dir, "read") + if not ok then + return nil, "Failed copying unpacked rockspec into unpacked source directory." + end + end + util.printout() + util.printout("Done. You may now enter directory ") + util.printout(dir.path(dir_name, rockspec.source.dir)) + util.printout("and type 'luarocks make' to build.") + end + util.remove_scheduled_function(rollback) + return true +end + +--- Driver function for the "unpack" command. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +function unpack.command(args) + local url, err + if args.rock:match(".*%.rock") or args.rock:match(".*%.rockspec") then + url = args.rock + else + url, err = search.find_src_or_rockspec(args.rock, args.namespace, args.version, args.check_lua_versions) + if not url then + return nil, err + end + end + + return run_unpacker(url, args.force) +end + +return unpack diff --git a/src/luarocks/cmd/upload.lua b/src/luarocks/cmd/upload.lua new file mode 100644 index 0000000..6b84e45 --- /dev/null +++ b/src/luarocks/cmd/upload.lua @@ -0,0 +1,128 @@ + +local upload = {} + +local signing = require("luarocks.signing") +local util = require("luarocks.util") +local fetch = require("luarocks.fetch") +local pack = require("luarocks.pack") +local cfg = require("luarocks.core.cfg") +local Api = require("luarocks.upload.api") + +function upload.add_to_parser(parser) + local cmd = parser:command("upload", "Pack a source rock file (.src.rock extension) ".. + "and upload it and the rockspec to the public rocks repository.", util.see_also()) + :summary("Upload a rockspec to the public rocks repository.") + + cmd:argument("rockspec", "Rockspec for the rock to upload.") + cmd:argument("src-rock", "A corresponding .src.rock file; if not given it will be generated.") + :args("?") + + cmd:flag("--skip-pack", "Do not pack and send source rock.") + cmd:option("--api-key", "Pass an API key. It will be stored for subsequent uses.") + :argname("") + cmd:option("--temp-key", "Use the given a temporary API key in this ".. + "invocation only. It will not be stored.") + :argname("") + cmd:flag("--force", "Replace existing rockspec if the same revision of a ".. + "module already exists. This should be used only in case of upload ".. + "mistakes: when updating a rockspec, increment the revision number ".. + "instead.") + cmd:flag("--sign", "Upload a signature file alongside each file as well.") + cmd:flag("--debug"):hidden(true) +end + +local function is_dev_version(version) + return version:match("^dev") or version:match("^scm") +end + +function upload.command(args) + local api, err = Api.new(args) + if not api then + return nil, err + end + if cfg.verbose then + api.debug = true + end + + local rockspec, err, errcode = fetch.load_rockspec(args.rockspec) + if err then + return nil, err, errcode + end + + util.printout("Sending " .. tostring(args.rockspec) .. " ...") + local res, err = api:method("check_rockspec", { + package = rockspec.package, + version = rockspec.version + }) + if not res then return nil, err end + + if not res.module then + util.printout("Will create new module (" .. tostring(rockspec.package) .. ")") + end + if res.version and not args.force then + return nil, "Revision "..rockspec.version.." already exists on the server. "..util.see_help("upload") + end + + local sigfname + local rock_sigfname + + if args.sign then + sigfname, err = signing.sign_file(args.rockspec) + if err then + return nil, "Failed signing rockspec: " .. err + end + util.printout("Signed rockspec: "..sigfname) + end + + local rock_fname + if args.src_rock then + rock_fname = args.src_rock + elseif not args.skip_pack and not is_dev_version(rockspec.version) then + util.printout("Packing " .. tostring(rockspec.package)) + rock_fname, err = pack.pack_source_rock(args.rockspec) + if not rock_fname then + return nil, err + end + end + + if rock_fname and args.sign then + rock_sigfname, err = signing.sign_file(rock_fname) + if err then + return nil, "Failed signing rock: " .. err + end + util.printout("Signed packed rock: "..rock_sigfname) + end + + local multipart = require("luarocks.upload.multipart") + + res, err = api:method("upload", nil, { + rockspec_file = multipart.new_file(args.rockspec), + rockspec_sig = sigfname and multipart.new_file(sigfname), + }) + if not res then return nil, err end + + if res.is_new and #res.manifests == 0 then + util.printerr("Warning: module not added to root manifest due to name taken.") + end + + local module_url = res.module_url + + if rock_fname then + if (not res.version) or (not res.version.id) then + return nil, "Invalid response from server." + end + util.printout(("Sending " .. tostring(rock_fname) .. " ...")) + res, err = api:method("upload_rock/" .. ("%d"):format(res.version.id), nil, { + rock_file = multipart.new_file(rock_fname), + rock_sig = rock_sigfname and multipart.new_file(rock_sigfname), + }) + if not res then return nil, err end + end + + util.printout() + util.printout("Done: " .. tostring(module_url)) + util.printout() + return true +end + +return upload diff --git a/src/luarocks/cmd/which.lua b/src/luarocks/cmd/which.lua new file mode 100644 index 0000000..f50a43c --- /dev/null +++ b/src/luarocks/cmd/which.lua @@ -0,0 +1,40 @@ + +--- @module luarocks.which_cmd +-- Driver for the `luarocks which` command. +local which_cmd = {} + +local loader = require("luarocks.loader") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") + +function which_cmd.add_to_parser(parser) + local cmd = parser:command("which", 'Given a module name like "foo.bar", '.. + "output which file would be loaded to resolve that module by ".. + 'luarocks.loader, like "/usr/local/lua/'..cfg.lua_version..'/foo/bar.lua".', + util.see_also()) + :summary("Tell which file corresponds to a given module name.") + + cmd:argument("modname", "Module name.") +end + +--- Driver function for "which" command. +-- @return boolean This function terminates the interpreter. +function which_cmd.command(args) + local pathname, rock_name, rock_version, where = loader.which(args.modname, "lp") + + if pathname then + util.printout(pathname) + if where == "l" then + util.printout("(provided by " .. tostring(rock_name) .. " " .. tostring(rock_version) .. ")") + else + local key = rock_name + util.printout("(found directly via package." .. key.. " -- not installed as a rock?)") + end + return true + end + + return nil, "Module '" .. args.modname .. "' not found." +end + +return which_cmd + diff --git a/src/luarocks/cmd/write_rockspec.lua b/src/luarocks/cmd/write_rockspec.lua new file mode 100644 index 0000000..871cdd4 --- /dev/null +++ b/src/luarocks/cmd/write_rockspec.lua @@ -0,0 +1,408 @@ + +local write_rockspec = {} + +local builtin = require("luarocks.build.builtin") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local persist = require("luarocks.persist") +local rockspecs = require("luarocks.rockspecs") +local type_rockspec = require("luarocks.type.rockspec") +local util = require("luarocks.util") + +local lua_versions = { + "5.1", + "5.2", + "5.3", + "5.4", + "5.1,5.2", + "5.2,5.3", + "5.3,5.4", + "5.1,5.2,5.3", + "5.2,5.3,5.4", + "5.1,5.2,5.3,5.4" +} + +function write_rockspec.cmd_options(parser) + return parser:option("--output", "Write the rockspec with the given filename.\n".. + "If not given, a file is written in the current directory with a ".. + "filename based on given name and version.") + :argname(""), + parser:option("--license", 'A license string, such as "MIT/X11" or "GNU GPL v3".') + :argname(""), + parser:option("--summary", "A short one-line description summary.") + :argname(""), + parser:option("--detailed", "A longer description string.") + :argname(""), + parser:option("--homepage", "Project homepage.") + :argname(""), + parser:option("--lua-versions", 'Supported Lua versions. Accepted values are: "'.. + table.concat(lua_versions, '", "')..'".') + :argname("") + :choices(lua_versions), + parser:option("--rockspec-format", 'Rockspec format version, such as "1.0" or "1.1".') + :argname(""), + parser:option("--tag", "Tag to use. Will attempt to extract version number from it."), + parser:option("--lib", "A comma-separated list of libraries that C files need to link to.") + :argname("") +end + +function write_rockspec.add_to_parser(parser) + local cmd = parser:command("write_rockspec", [[ +This command writes an initial version of a rockspec file, +based on a name, a version, and a location (an URL or a local path). +If only two arguments are given, the first one is considered the name and the +second one is the location. +If only one argument is given, it must be the location. +If no arguments are given, current directory is used as the location. +LuaRocks will attempt to infer name and version if not given, +using 'dev' as a fallback default version. + +Note that the generated file is a _starting point_ for writing a +rockspec, and is not guaranteed to be complete or correct. ]], util.see_also()) + :summary("Write a template for a rockspec file.") + + cmd:argument("name", "Name of the rock.") + :args("?") + cmd:argument("version", "Rock version.") + :args("?") + cmd:argument("location", "URL or path to the rock sources.") + :args("?") + + write_rockspec.cmd_options(cmd) +end + +local function open_file(name) + return io.open(dir.path(fs.current_dir(), name), "r") +end + +local function fetch_url(rockspec) + local file, temp_dir, err_code, err_file, err_temp_dir = fetch.fetch_sources(rockspec, false) + if err_code == "source.dir" then + file, temp_dir = err_file, err_temp_dir + elseif not file then + util.warning("Could not fetch sources - "..temp_dir) + return false + end + util.printout("File successfully downloaded. Making checksum and checking base dir...") + if dir.is_basic_protocol(rockspec.source.protocol) then + rockspec.source.md5 = fs.get_md5(file) + end + local inferred_dir, found_dir = fetch.find_base_dir(file, temp_dir, rockspec.source.url) + return true, found_dir or inferred_dir, temp_dir +end + +local lua_version_dep = { + ["5.1"] = "lua ~> 5.1", + ["5.2"] = "lua ~> 5.2", + ["5.3"] = "lua ~> 5.3", + ["5.4"] = "lua ~> 5.4", + ["5.1,5.2"] = "lua >= 5.1, < 5.3", + ["5.2,5.3"] = "lua >= 5.2, < 5.4", + ["5.3,5.4"] = "lua >= 5.3, < 5.5", + ["5.1,5.2,5.3"] = "lua >= 5.1, < 5.4", + ["5.2,5.3,5.4"] = "lua >= 5.2, < 5.5", + ["5.1,5.2,5.3,5.4"] = "lua >= 5.1, < 5.5", +} + +local simple_scm_protocols = { + git = true, + ["git+http"] = true, + ["git+https"] = true, + ["git+ssh"] = true, + hg = true, + ["hg+http"] = true, + ["hg+https"] = true, + ["hg+ssh"] = true, +} + +local detect_url +do + local function detect_url_from_command(program, args, directory) + local command = fs.Q(cfg.variables[program:upper()]).. " "..args + local pipe = io.popen(fs.command_at(directory, fs.quiet_stderr(command))) + if not pipe then return nil end + local url = pipe:read("*a"):match("^([^\r\n]+)") + pipe:close() + if not url then return nil end + if url:match("^[^@:/]+@[^@:/]+:.*$") then + local u, h, p = url:match("^([^@]+)@([^:]+):(.*)$") + url = program.."+ssh://"..u.."@"..h.."/"..p + elseif not util.starts_with(url, program.."://") then + url = program.."+"..url + end + + if simple_scm_protocols[dir.split_url(url)] then + return url + end + end + + local function detect_scm_url(directory) + return detect_url_from_command("git", "config --get remote.origin.url", directory) or + detect_url_from_command("hg", "paths default", directory) + end + + detect_url = function(url_or_dir) + if url_or_dir:match("://") then + return url_or_dir + else + return detect_scm_url(url_or_dir) or "*** please add URL for source tarball, zip or repository here ***" + end + end +end + +local function detect_homepage(url, homepage) + if homepage then + return homepage + end + local url_protocol, url_path = dir.split_url(url) + + if simple_scm_protocols[url_protocol] then + for _, domain in ipairs({"github.com", "bitbucket.org", "gitlab.com"}) do + if util.starts_with(url_path, domain) then + return "https://"..url_path:gsub("%.git$", "") + end + end + end + + return "*** please enter a project homepage ***" +end + +local function detect_description() + local fd = open_file("README.md") or open_file("README") + if not fd then return end + local data = fd:read("*a") + fd:close() + local paragraph = data:match("\n\n([^%[].-)\n\n") + if not paragraph then paragraph = data:match("\n\n(.*)") end + local summary, detailed + if paragraph then + detailed = paragraph + + if #paragraph < 80 then + summary = paragraph:gsub("\n", "") + else + summary = paragraph:gsub("\n", " "):match("([^.]*%.) ") + end + end + return summary, detailed +end + +local licenses = { + [78656] = "MIT", + [49311] = "ISC", +} + +local function detect_license(data) + local strip_copyright = (data:gsub("^Copyright [^\n]*\n", "")) + local sum = 0 + for i = 1, #strip_copyright do + local num = string.byte(strip_copyright:sub(i,i)) + if num > 32 and num <= 128 then + sum = sum + num + end + end + return licenses[sum] +end + +local function check_license() + local fd = open_file("COPYING") or open_file("LICENSE") or open_file("MIT-LICENSE.txt") + if not fd then return nil end + local data = fd:read("*a") + fd:close() + local license = detect_license(data) + if license then + return license, data + end + return nil, data +end + +local function fill_as_builtin(rockspec, libs) + rockspec.build.type = "builtin" + + local incdirs, libdirs + if libs then + incdirs, libdirs = {}, {} + for _, lib in ipairs(libs) do + local upper = lib:upper() + incdirs[#incdirs+1] = "$("..upper.."_INCDIR)" + libdirs[#libdirs+1] = "$("..upper.."_LIBDIR)" + end + end + + rockspec.build.modules, rockspec.build.install, rockspec.build.copy_directories = builtin.autodetect_modules(libs, incdirs, libdirs) +end + +local function rockspec_cleanup(rockspec) + rockspec.source.file = nil + rockspec.source.protocol = nil + rockspec.source.identifier = nil + rockspec.source.dir = nil + rockspec.source.dir_set = nil + rockspec.source.pathname = nil + rockspec.variables = nil + rockspec.name = nil + rockspec.format_is_at_least = nil + rockspec.local_abs_filename = nil + rockspec.rocks_provided = nil + for _, list in ipairs({"dependencies", "build_dependencies", "test_dependencies"}) do + if rockspec[list] and not next(rockspec[list]) then + rockspec[list] = nil + end + end + for _, list in ipairs({"dependencies", "build_dependencies", "test_dependencies"}) do + if rockspec[list] then + for i, entry in ipairs(rockspec[list]) do + rockspec[list][i] = tostring(entry) + end + end + end +end + +function write_rockspec.command(args) + local name, version = args.name, args.version + local location = args.location + + if not name then + location = "." + elseif not version then + location = name + name = nil + elseif not location then + location = version + version = nil + end + + if args.tag then + if not version then + version = args.tag:gsub("^v", "") + end + end + + local protocol, pathname = dir.split_url(location) + if protocol == "file" then + if pathname == "." then + name = name or dir.base_name(fs.current_dir()) + end + elseif dir.is_basic_protocol(protocol) then + local filename = dir.base_name(location) + local newname, newversion = filename:match("(.*)-([^-]+)") + if newname then + name = name or newname + version = version or newversion:gsub("%.[a-z]+$", ""):gsub("%.tar$", "") + end + else + name = name or dir.base_name(location):gsub("%.[^.]+$", "") + end + + if not name then + return nil, "Could not infer rock name. "..util.see_help("write_rockspec") + end + version = version or "dev" + + local filename = args.output or dir.path(fs.current_dir(), name:lower().."-"..version.."-1.rockspec") + + local url = detect_url(location) + local homepage = detect_homepage(url, args.homepage) + + local rockspec, err = rockspecs.from_persisted_table(filename, { + rockspec_format = args.rockspec_format, + package = name, + version = version.."-1", + source = { + url = url, + tag = args.tag, + }, + description = { + summary = args.summary or "*** please specify description summary ***", + detailed = args.detailed or "*** please enter a detailed description ***", + homepage = homepage, + license = args.license or "*** please specify a license ***", + }, + dependencies = { + lua_version_dep[args.lua_versions], + }, + build = {}, + }) + assert(not err, err) + rockspec.source.protocol = protocol + + if not next(rockspec.dependencies) then + util.warning("Please specify supported Lua versions with --lua-versions=. "..util.see_help("write_rockspec")) + end + + local local_dir = location + + if location:match("://") then + rockspec.source.file = dir.base_name(location) + if not dir.is_basic_protocol(rockspec.source.protocol) then + if version ~= "dev" then + rockspec.source.tag = args.tag or "v" .. version + end + end + rockspec.source.dir = nil + local ok, base_dir, temp_dir = fetch_url(rockspec) + if ok then + if base_dir ~= dir.base_name(location) then + rockspec.source.dir = base_dir + end + end + if base_dir then + local_dir = dir.path(temp_dir, base_dir) + else + local_dir = nil + end + end + + if not local_dir then + local_dir = "." + end + + local libs = nil + if args.lib then + libs = {} + rockspec.external_dependencies = {} + for lib in args.lib:gmatch("([^,]+)") do + table.insert(libs, lib) + rockspec.external_dependencies[lib:upper()] = { + library = lib + } + end + end + + local ok, err = fs.change_dir(local_dir) + if not ok then return nil, "Failed reaching files from project - error entering directory "..local_dir end + + if not (args.summary and args.detailed) then + local summary, detailed = detect_description() + rockspec.description.summary = args.summary or summary + rockspec.description.detailed = args.detailed or detailed + end + + if not args.license then + local license, fulltext = check_license() + if license then + rockspec.description.license = license + elseif license then + util.title("Could not auto-detect type for project license:") + util.printout(fulltext) + util.printout() + util.title("Please fill in the source.license field manually or use --license.") + end + end + + fill_as_builtin(rockspec, libs) + + rockspec_cleanup(rockspec) + + persist.save_from_table(filename, rockspec, type_rockspec.order) + + util.printout() + util.printout("Wrote template at "..filename.." -- you should now edit and finish it.") + util.printout() + + return true +end + +return write_rockspec diff --git a/src/luarocks/config.lua b/src/luarocks/config.lua new file mode 100644 index 0000000..019b388 --- /dev/null +++ b/src/luarocks/config.lua @@ -0,0 +1,36 @@ +local config = {} + +local persist = require("luarocks.persist") + +local cfg_skip = { + errorcodes = true, + flags = true, + platforms = true, + root_dir = true, + upload_servers = true, +} + +function config.should_skip(k, v) + return type(v) == "function" or cfg_skip[k] +end + +local function cleanup(tbl) + local copy = {} + for k, v in pairs(tbl) do + if not config.should_skip(k, v) then + copy[k] = v + end + end + return copy +end + +function config.get_config_for_display(cfg) + return cleanup(cfg) +end + +function config.to_string(cfg) + local cleancfg = config.get_config_for_display(cfg) + return persist.save_from_table_to_string(cleancfg) +end + +return config diff --git a/src/luarocks/core/cfg.lua b/src/luarocks/core/cfg.lua new file mode 100644 index 0000000..9cfd9dd --- /dev/null +++ b/src/luarocks/core/cfg.lua @@ -0,0 +1,940 @@ + +--- Configuration for LuaRocks. +-- Tries to load the user's configuration file and +-- defines defaults for unset values. See the +-- config +-- file format documentation for details. +-- +-- End-users shouldn't edit this file. They can override any defaults +-- set in this file using their system-wide or user-specific configuration +-- files. Run `luarocks` with no arguments to see the locations of +-- these files in your platform. + +local table, pairs, require, os, pcall, ipairs, package, type, assert = + table, pairs, require, os, pcall, ipairs, package, type, assert + +local dir = require("luarocks.core.dir") +local util = require("luarocks.core.util") +local persist = require("luarocks.core.persist") +local sysdetect = require("luarocks.core.sysdetect") +local vers = require("luarocks.core.vers") + +-------------------------------------------------------------------------------- + +local program_version = "3.11.1" + +local is_windows = package.config:sub(1,1) == "\\" + +-- Set order for platform overrides. +-- More general platform identifiers should be listed first, +-- more specific ones later. +local platform_order = { + -- Unixes + "unix", + "bsd", + "solaris", + "netbsd", + "openbsd", + "freebsd", + "dragonfly", + "linux", + "macosx", + "cygwin", + "msys", + "haiku", + -- Windows + "windows", + "win32", + "mingw", + "mingw32", + "msys2_mingw_w64", +} + +local function detect_sysconfdir() + if not debug then + return + end + local src = debug.getinfo(1, "S").source + if not src then + return + end + src = dir.normalize(src) + if src:sub(1, 1) == "@" then + src = src:sub(2) + end + local basedir = src:match("^(.*)[\\/]luarocks[\\/]core[\\/]cfg.lua$") + if not basedir then + return + end + -- If installed in a Unix-like tree, use a Unix-like sysconfdir + local installdir = basedir:match("^(.*)[\\/]share[\\/]lua[\\/][^/]*$") + if installdir then + if installdir == "/usr" then + return "/etc/luarocks" + end + return dir.path(installdir, "etc", "luarocks") + end + -- Otherwise, use base directory of sources + return basedir +end + +local load_config_file +do + -- Create global environment for the config files; + local function env_for_config_file(cfg, platforms) + local platforms_copy = {} + for k,v in pairs(platforms) do + platforms_copy[k] = v + end + + local e + e = { + home = cfg.home, + lua_version = cfg.lua_version, + platforms = platforms_copy, + processor = cfg.target_cpu, -- remains for compat reasons + target_cpu = cfg.target_cpu, -- replaces `processor` + os_getenv = os.getenv, + variables = cfg.variables or {}, + dump_env = function() + -- debug function, calling it from a config file will show all + -- available globals to that config file + print(util.show_table(e, "global environment")) + end, + } + return e + end + + -- Merge values from config files read into the `cfg` table + local function merge_overrides(cfg, overrides) + -- remove some stuff we do not want to integrate + overrides.os_getenv = nil + overrides.dump_env = nil + -- remove tables to be copied verbatim instead of deeply merged + if overrides.rocks_trees then cfg.rocks_trees = nil end + if overrides.rocks_servers then cfg.rocks_servers = nil end + -- perform actual merge + util.deep_merge(cfg, overrides) + end + + local function update_platforms(platforms, overrides) + if overrides[1] then + for k, _ in pairs(platforms) do + platforms[k] = nil + end + for _, v in ipairs(overrides) do + platforms[v] = true + end + -- set some fallback default in case the user provides an incomplete configuration. + -- LuaRocks expects a set of defaults to be available. + if not (platforms.unix or platforms.windows) then + platforms[is_windows and "windows" or "unix"] = true + end + end + end + + -- Load config file and merge its contents into the `cfg` module table. + -- @return filepath of successfully loaded file or nil if it failed + load_config_file = function(cfg, platforms, filepath) + local result, err, errcode = persist.load_into_table(filepath, env_for_config_file(cfg, platforms)) + if (not result) and errcode ~= "open" then + -- errcode is either "load" or "run"; bad config file, so error out + return nil, err, "config" + end + if result then + -- success in loading and running, merge contents and exit + update_platforms(platforms, result.platforms) + result.platforms = nil + merge_overrides(cfg, result) + return filepath + end + return nil -- nothing was loaded + end +end + +local platform_sets = { + freebsd = { unix = true, bsd = true, freebsd = true }, + openbsd = { unix = true, bsd = true, openbsd = true }, + dragonfly = { unix = true, bsd = true, dragonfly = true }, + solaris = { unix = true, solaris = true }, + windows = { windows = true, win32 = true }, + cygwin = { unix = true, cygwin = true }, + macosx = { unix = true, bsd = true, macosx = true, macos = true }, + netbsd = { unix = true, bsd = true, netbsd = true }, + haiku = { unix = true, haiku = true }, + linux = { unix = true, linux = true }, + mingw = { windows = true, win32 = true, mingw32 = true, mingw = true }, + msys = { unix = true, cygwin = true, msys = true }, + msys2_mingw_w64 = { windows = true, win32 = true, mingw32 = true, mingw = true, msys = true, msys2_mingw_w64 = true }, +} + +local function make_platforms(system) + -- fallback to Unix in unknown systems + return platform_sets[system] or { unix = true } +end + +-------------------------------------------------------------------------------- + +local function make_defaults(lua_version, target_cpu, platforms, home) + + -- Configure defaults: + local defaults = { + + local_by_default = false, + accept_unknown_fields = false, + fs_use_modules = true, + hooks_enabled = true, + deps_mode = "one", + no_manifest = false, + check_certificates = false, + + cache_timeout = 60, + cache_fail_timeout = 86400, + + lua_modules_path = dir.path("share", "lua", lua_version), + lib_modules_path = dir.path("lib", "lua", lua_version), + rocks_subdir = dir.path("lib", "luarocks", "rocks-"..lua_version), + + arch = "unknown", + lib_extension = "unknown", + obj_extension = "unknown", + link_lua_explicitly = false, + + rocks_servers = { + { + "https://luarocks.org", + "https://raw.githubusercontent.com/rocks-moonscript-org/moonrocks-mirror/master/", + "https://loadk.com/luarocks/", + } + }, + disabled_servers = {}, + + upload = { + server = "https://luarocks.org", + tool_version = "1.0.0", + api_version = "1", + }, + + lua_extension = "lua", + connection_timeout = 30, -- 0 = no timeout + + variables = { + MAKE = os.getenv("MAKE") or "make", + CC = os.getenv("CC") or "cc", + LD = os.getenv("CC") or "ld", + AR = os.getenv("AR") or "ar", + RANLIB = os.getenv("RANLIB") or "ranlib", + + CVS = "cvs", + GIT = "git", + SSCM = "sscm", + SVN = "svn", + HG = "hg", + + GPG = "gpg", + + RSYNC = "rsync", + WGET = "wget", + SCP = "scp", + CURL = "curl", + + PWD = "pwd", + MKDIR = "mkdir", + RMDIR = "rmdir", + CP = "cp", + LN = "ln", + LS = "ls", + RM = "rm", + FIND = "find", + CHMOD = "chmod", + ICACLS = "icacls", + MKTEMP = "mktemp", + + ZIP = "zip", + UNZIP = "unzip -n", + GUNZIP = "gunzip", + BUNZIP2 = "bunzip2", + TAR = "tar", + + MD5SUM = "md5sum", + OPENSSL = "openssl", + MD5 = "md5", + TOUCH = "touch", + + CMAKE = "cmake", + SEVENZ = "7z", + + RSYNCFLAGS = "--exclude=.git -Oavz", + CURLNOCERTFLAG = "", + WGETNOCERTFLAG = "", + }, + + external_deps_subdirs = { + bin = "bin", + lib = "lib", + include = "include" + }, + runtime_external_deps_subdirs = { + bin = "bin", + lib = "lib", + include = "include" + }, + } + + if platforms.windows then + + defaults.arch = "win32-"..target_cpu + defaults.lib_extension = "dll" + defaults.external_lib_extension = "dll" + defaults.static_lib_extension = "lib" + defaults.obj_extension = "obj" + defaults.external_deps_dirs = { + dir.path("c:", "external"), + dir.path("c:", "windows", "system32"), + } + + defaults.makefile = "Makefile.win" + defaults.variables.PWD = "echo %cd%" + defaults.variables.MKDIR = "md" + defaults.variables.MAKE = os.getenv("MAKE") or "nmake" + defaults.variables.CC = os.getenv("CC") or "cl" + defaults.variables.RC = os.getenv("WINDRES") or "rc" + defaults.variables.LD = os.getenv("LINK") or "link" + defaults.variables.MT = os.getenv("MT") or "mt" + defaults.variables.AR = os.getenv("AR") or "lib" + defaults.variables.CFLAGS = os.getenv("CFLAGS") or "/nologo /MD /O2" + defaults.variables.LDFLAGS = os.getenv("LDFLAGS") + defaults.variables.LIBFLAG = "/nologo /dll" + + defaults.external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "?.lib", "lib?.lib", "?.dll", "lib?.dll" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "?.dll", "lib?.dll" }, + include = { "?.h" } + } + defaults.export_path_separator = ";" + defaults.wrapper_suffix = ".bat" + + local localappdata = os.getenv("LOCALAPPDATA") + if not localappdata then + -- for Windows versions below Vista + localappdata = dir.path((os.getenv("USERPROFILE") or dir.path("c:", "Users", "All Users")), "Local Settings", "Application Data") + end + defaults.local_cache = dir.path(localappdata, "LuaRocks", "Cache") + defaults.web_browser = "start" + + defaults.external_deps_subdirs.lib = { "lib", "", "bin" } + defaults.runtime_external_deps_subdirs.lib = { "lib", "", "bin" } + defaults.link_lua_explicitly = true + defaults.fs_use_modules = false + end + + if platforms.mingw32 then + defaults.obj_extension = "o" + defaults.static_lib_extension = "a" + defaults.external_deps_dirs = { + dir.path("c:", "external"), + dir.path("c:", "mingw"), + dir.path("c:", "windows", "system32"), + } + defaults.cmake_generator = "MinGW Makefiles" + defaults.variables.MAKE = os.getenv("MAKE") or "mingw32-make" + if target_cpu == "x86_64" then + defaults.variables.CC = os.getenv("CC") or "x86_64-w64-mingw32-gcc" + defaults.variables.LD = os.getenv("CC") or "x86_64-w64-mingw32-gcc" + else + defaults.variables.CC = os.getenv("CC") or "mingw32-gcc" + defaults.variables.LD = os.getenv("CC") or "mingw32-gcc" + end + defaults.variables.AR = os.getenv("AR") or "ar" + defaults.variables.RC = os.getenv("WINDRES") or "windres" + defaults.variables.RANLIB = os.getenv("RANLIB") or "ranlib" + defaults.variables.CFLAGS = os.getenv("CFLAGS") or "-O2" + defaults.variables.LDFLAGS = os.getenv("LDFLAGS") + defaults.variables.LIBFLAG = "-shared" + defaults.makefile = "Makefile" + defaults.external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + -- mingw lookup list from http://stackoverflow.com/a/15853231/1793220 + -- ...should we keep ?.lib at the end? It's not in the above list. + lib = { "lib?.dll.a", "?.dll.a", "lib?.a", "cyg?.dll", "lib?.dll", "?.dll", "?.lib" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "cyg?.dll", "?.dll", "lib?.dll" }, + include = { "?.h" } + } + defaults.link_lua_explicitly = true + end + + if platforms.unix then + defaults.lib_extension = "so" + defaults.static_lib_extension = "a" + defaults.external_lib_extension = "so" + defaults.obj_extension = "o" + defaults.external_deps_dirs = { "/usr/local", "/usr", "/" } + + defaults.variables.CFLAGS = os.getenv("CFLAGS") or "-O2" + -- we pass -fPIC via CFLAGS because of old Makefile-based Lua projects + -- which didn't have -fPIC in their Makefiles but which honor CFLAGS + if not defaults.variables.CFLAGS:match("-fPIC") then + defaults.variables.CFLAGS = defaults.variables.CFLAGS.." -fPIC" + end + + defaults.variables.LDFLAGS = os.getenv("LDFLAGS") + + defaults.cmake_generator = "Unix Makefiles" + defaults.variables.CC = os.getenv("CC") or "gcc" + defaults.variables.LD = os.getenv("CC") or "gcc" + defaults.gcc_rpath = true + defaults.variables.LIBFLAG = "-shared" + defaults.variables.TEST = "test" + + defaults.external_deps_patterns = { + bin = { "?" }, + lib = { "lib?.a", "lib?.so", "lib?.so.*" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?" }, + lib = { "lib?.so", "lib?.so.*" }, + include = { "?.h" } + } + defaults.export_path_separator = ":" + defaults.wrapper_suffix = "" + local xdg_cache_home = os.getenv("XDG_CACHE_HOME") or home.."/.cache" + defaults.local_cache = xdg_cache_home.."/luarocks" + defaults.web_browser = "xdg-open" + end + + if platforms.cygwin then + defaults.lib_extension = "so" -- can be overridden in the config file for mingw builds + defaults.arch = "cygwin-"..target_cpu + defaults.cmake_generator = "Unix Makefiles" + defaults.variables.CC = "echo -llua | xargs " .. (os.getenv("CC") or "gcc") + defaults.variables.LD = "echo -llua | xargs " .. (os.getenv("CC") or "gcc") + defaults.variables.LIBFLAG = "-shared" + defaults.link_lua_explicitly = true + end + + if platforms.msys then + -- msys is basically cygwin made out of mingw, meaning the subsytem is unixish + -- enough, yet we can freely mix with native win32 + defaults.external_deps_patterns = { + bin = { "?.exe", "?.bat", "?" }, + lib = { "lib?.so", "lib?.so.*", "lib?.dll.a", "?.dll.a", + "lib?.a", "lib?.dll", "?.dll", "?.lib" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "lib?.so", "?.dll", "lib?.dll" }, + include = { "?.h" } + } + if platforms.mingw then + -- MSYS2 can build Windows programs that depend on + -- msys-2.0.dll (based on Cygwin) but MSYS2 is also designed + -- for building native Windows programs by MinGW. These + -- programs don't depend on msys-2.0.dll. + local pipe = io.popen("cygpath --windows %MINGW_PREFIX%") + local mingw_prefix = pipe:read("*l") + pipe:close() + defaults.external_deps_dirs = { + mingw_prefix, + dir.path("c:", "windows", "system32"), + } + defaults.makefile = "Makefile" + defaults.cmake_generator = "MSYS Makefiles" + defaults.local_cache = dir.path(home, ".cache", "luarocks") + defaults.variables.MAKE = os.getenv("MAKE") or "make" + defaults.variables.CC = os.getenv("CC") or "gcc" + defaults.variables.RC = os.getenv("WINDRES") or "windres" + defaults.variables.LD = os.getenv("CC") or "gcc" + defaults.variables.MT = os.getenv("MT") or nil + defaults.variables.AR = os.getenv("AR") or "ar" + defaults.variables.RANLIB = os.getenv("RANLIB") or "ranlib" + + defaults.variables.CFLAGS = os.getenv("CFLAGS") or "-O2 -fPIC" + if not defaults.variables.CFLAGS:match("-fPIC") then + defaults.variables.CFLAGS = defaults.variables.CFLAGS.." -fPIC" + end + + defaults.variables.LIBFLAG = "-shared" + end + end + + if platforms.bsd then + defaults.variables.MAKE = "gmake" + defaults.gcc_rpath = false + defaults.variables.CC = os.getenv("CC") or "cc" + defaults.variables.LD = os.getenv("CC") or defaults.variables.CC + end + + if platforms.macosx then + defaults.variables.MAKE = os.getenv("MAKE") or "make" + defaults.external_lib_extension = "dylib" + defaults.arch = "macosx-"..target_cpu + defaults.variables.LIBFLAG = "-bundle -undefined dynamic_lookup -all_load" + local version = util.popen_read("sw_vers -productVersion") + if not (version:match("^%d+%.%d+%.%d+$") or version:match("^%d+%.%d+$")) then + version = "10.3" + end + version = vers.parse_version(version) + if version >= vers.parse_version("11.0") then + version = vers.parse_version("11.0") + elseif version >= vers.parse_version("10.10") then + version = vers.parse_version("10.8") + elseif version >= vers.parse_version("10.5") then + version = vers.parse_version("10.5") + else + defaults.gcc_rpath = false + end + defaults.variables.CC = "env MACOSX_DEPLOYMENT_TARGET="..tostring(version).." gcc" + defaults.variables.LD = "env MACOSX_DEPLOYMENT_TARGET="..tostring(version).." gcc" + defaults.web_browser = "open" + + -- XCode SDK + local sdk_path = util.popen_read("xcrun --show-sdk-path 2>/dev/null") + if sdk_path then + table.insert(defaults.external_deps_dirs, sdk_path .. "/usr") + table.insert(defaults.external_deps_patterns.lib, 1, "lib?.tbd") + table.insert(defaults.runtime_external_deps_patterns.lib, 1, "lib?.tbd") + end + + -- Homebrew + table.insert(defaults.external_deps_dirs, "/usr/local/opt") + defaults.external_deps_subdirs.lib = { "lib", "" } + defaults.runtime_external_deps_subdirs.lib = { "lib", "" } + table.insert(defaults.external_deps_patterns.lib, 1, "/?/lib/lib?.dylib") + table.insert(defaults.runtime_external_deps_patterns.lib, 1, "/?/lib/lib?.dylib") + end + + if platforms.linux then + defaults.arch = "linux-"..target_cpu + + local gcc_arch = util.popen_read("gcc -print-multiarch 2>/dev/null") + if gcc_arch and gcc_arch ~= "" then + defaults.external_deps_subdirs.lib = { "lib/" .. gcc_arch, "lib64", "lib" } + defaults.runtime_external_deps_subdirs.lib = { "lib/" .. gcc_arch, "lib64", "lib" } + else + defaults.external_deps_subdirs.lib = { "lib64", "lib" } + defaults.runtime_external_deps_subdirs.lib = { "lib64", "lib" } + end + end + + if platforms.freebsd then + defaults.arch = "freebsd-"..target_cpu + elseif platforms.dragonfly then + defaults.arch = "dragonfly-"..target_cpu + elseif platforms.openbsd then + defaults.arch = "openbsd-"..target_cpu + elseif platforms.netbsd then + defaults.arch = "netbsd-"..target_cpu + elseif platforms.solaris then + defaults.arch = "solaris-"..target_cpu + defaults.variables.MAKE = "gmake" + end + + -- Expose some more values detected by LuaRocks for use by rockspec authors. + defaults.variables.LIB_EXTENSION = defaults.lib_extension + defaults.variables.OBJ_EXTENSION = defaults.obj_extension + + return defaults +end + +local function use_defaults(cfg, defaults) + + -- Populate variables with values from their 'defaults' counterparts + -- if they were not already set by user. + if not cfg.variables then + cfg.variables = {} + end + for k,v in pairs(defaults.variables) do + if not cfg.variables[k] then + cfg.variables[k] = v + end + end + + util.deep_merge_under(cfg, defaults) + + -- FIXME get rid of this + if not cfg.check_certificates then + cfg.variables.CURLNOCERTFLAG = "-k" + cfg.variables.WGETNOCERTFLAG = "--no-check-certificate" + end +end + +-------------------------------------------------------------------------------- + +local cfg = {} + +--- Initializes the LuaRocks configuration for variables, paths +-- and OS detection. +-- @param detected table containing information detected about the +-- environment. All fields below are optional: +-- * lua_version (in x.y format, e.g. "5.3") +-- * lua_bindir (e.g. "/usr/local/bin") +-- * lua_dir (e.g. "/usr/local") +-- * lua (e.g. "/usr/local/bin/lua-5.3") +-- * project_dir (a string with the path of the project directory +-- when using per-project environments, as created with `luarocks init`) +-- @param warning a logging function for warnings that takes a string +-- @return true on success; nil and an error message on failure. +function cfg.init(detected, warning) + detected = detected or {} + + local exit_ok = true + local exit_err = nil + local exit_what = nil + + local hc_ok, hardcoded = pcall(require, "luarocks.core.hardcoded") + if not hc_ok then + hardcoded = {} + end + + local init = cfg.init + + ---------------------------------------- + -- Reset the cfg table. + ---------------------------------------- + + for k, _ in pairs(cfg) do + cfg[k] = nil + end + + cfg.program_version = program_version + + if hardcoded.IS_BINARY then + cfg.is_binary = true + end + + -- Use detected values as defaults, overridable via config files or CLI args + + local hardcoded_lua = hardcoded.LUA + local hardcoded_lua_dir = hardcoded.LUA_DIR + local hardcoded_lua_bindir = hardcoded.LUA_BINDIR + local hardcoded_lua_incdir = hardcoded.LUA_INCDIR + local hardcoded_lua_libdir = hardcoded.LUA_LIBDIR + local hardcoded_lua_version = hardcoded.LUA_VERSION or _VERSION:sub(5) + + -- if --lua-version or --lua-dir are passed from the CLI, + -- don't use the hardcoded paths at all + if detected.given_lua_version or detected.given_lua_dir then + hardcoded_lua = nil + hardcoded_lua_dir = nil + hardcoded_lua_bindir = nil + hardcoded_lua_incdir = nil + hardcoded_lua_libdir = nil + hardcoded_lua_version = nil + end + + cfg.lua_version = detected.lua_version or hardcoded_lua_version + cfg.project_dir = (not hardcoded.FORCE_CONFIG) and detected.project_dir + + do + local lua = detected.lua or hardcoded_lua + local lua_dir = detected.lua_dir or hardcoded_lua_dir + local lua_bindir = detected.lua_bindir or hardcoded_lua_bindir + cfg.variables = { + LUA = lua, + LUA_DIR = lua_dir, + LUA_BINDIR = lua_bindir, + LUA_LIBDIR = hardcoded_lua_libdir, + LUA_INCDIR = hardcoded_lua_incdir, + } + end + + cfg.init = init + + ---------------------------------------- + -- System detection. + ---------------------------------------- + + -- A proper build of LuaRocks will hardcode the system + -- and proc values with hardcoded.SYSTEM and hardcoded.PROCESSOR. + -- If that is not available, we try to identify the system. + local system, processor = sysdetect.detect() + if hardcoded.SYSTEM then + system = hardcoded.SYSTEM + end + if hardcoded.PROCESSOR then + processor = hardcoded.PROCESSOR + end + + if system == "windows" then + if os.getenv("VCINSTALLDIR") then + -- running from the Development Command prompt for VS 2017 + system = "windows" + else + local msystem = os.getenv("MSYSTEM") + if msystem == nil then + system = "mingw" + elseif msystem == "MSYS" then + system = "msys" + else + -- MINGW32 or MINGW64 + system = "msys2_mingw_w64" + end + end + end + + cfg.target_cpu = processor + + local platforms = make_platforms(system) + + ---------------------------------------- + -- Platform is determined. + -- Let's load the config files. + ---------------------------------------- + + local sys_config_file + local home_config_file + local project_config_file + + local config_file_name = "config-"..cfg.lua_version..".lua" + + do + local sysconfdir = os.getenv("LUAROCKS_SYSCONFDIR") or hardcoded.SYSCONFDIR + if platforms.windows and not platforms.msys2_mingw_w64 then + cfg.home = os.getenv("APPDATA") or "c:" + cfg.home_tree = dir.path(cfg.home, "luarocks") + cfg.sysconfdir = sysconfdir or dir.path((os.getenv("PROGRAMFILES") or "c:"), "luarocks") + else + cfg.home = os.getenv("HOME") or "" + cfg.home_tree = dir.path(cfg.home, ".luarocks") + cfg.sysconfdir = sysconfdir or detect_sysconfdir() or "/etc/luarocks" + end + end + + -- Load system configuration file + sys_config_file = dir.path(cfg.sysconfdir, config_file_name) + local sys_config_ok, err = load_config_file(cfg, platforms, sys_config_file) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + end + + -- Load user configuration file (if allowed) + local home_config_ok + local project_config_ok + if not hardcoded.FORCE_CONFIG then + local env_var = "LUAROCKS_CONFIG_" .. cfg.lua_version:gsub("%.", "_") + local env_value = os.getenv(env_var) + if not env_value then + env_var = "LUAROCKS_CONFIG" + env_value = os.getenv(env_var) + end + -- first try environment provided file, so we can explicitly warn when it is missing + if env_value then + local env_ok, err = load_config_file(cfg, platforms, env_value) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + elseif warning and not env_ok then + warning("Warning: could not load configuration file `"..env_value.."` given in environment variable "..env_var.."\n") + end + if env_ok then + home_config_ok = true + home_config_file = env_value + end + end + + -- try XDG config home + if platforms.unix and not home_config_ok then + local xdg_config_home = os.getenv("XDG_CONFIG_HOME") or dir.path(cfg.home, ".config") + cfg.homeconfdir = dir.path(xdg_config_home, "luarocks") + home_config_file = dir.path(cfg.homeconfdir, config_file_name) + home_config_ok, err = load_config_file(cfg, platforms, home_config_file) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + end + end + + -- try the alternative defaults if there was no environment specified file or it didn't work + if not home_config_ok then + cfg.homeconfdir = cfg.home_tree + home_config_file = dir.path(cfg.homeconfdir, config_file_name) + home_config_ok, err = load_config_file(cfg, platforms, home_config_file) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + end + end + + -- finally, use the project-specific config file if any + if cfg.project_dir then + project_config_file = dir.path(cfg.project_dir, ".luarocks", config_file_name) + project_config_ok, err = load_config_file(cfg, platforms, project_config_file) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + end + end + end + + -- backwards compatibility: + if cfg.lua_interpreter and cfg.variables.LUA_BINDIR and not cfg.variables.LUA then + cfg.variables.LUA = dir.path(cfg.variables.LUA_BINDIR, cfg.lua_interpreter) + end + + ---------------------------------------- + -- Config files are loaded. + -- Let's finish up the cfg table. + ---------------------------------------- + + -- Settings given via the CLI (i.e. --lua-dir) take precedence over config files. + cfg.project_dir = detected.given_project_dir or cfg.project_dir + cfg.lua_version = detected.given_lua_version or cfg.lua_version + if detected.given_lua_dir then + cfg.variables.LUA = detected.lua + cfg.variables.LUA_DIR = detected.given_lua_dir + cfg.variables.LUA_BINDIR = detected.lua_bindir + cfg.variables.LUA_LIBDIR = nil + cfg.variables.LUA_INCDIR = nil + end + + -- Build a default list of rocks trees if not given + if cfg.rocks_trees == nil then + cfg.rocks_trees = {} + if cfg.home_tree then + table.insert(cfg.rocks_trees, { name = "user", root = cfg.home_tree } ) + end + if hardcoded.PREFIX and hardcoded.PREFIX ~= cfg.home_tree then + table.insert(cfg.rocks_trees, { name = "system", root = hardcoded.PREFIX } ) + end + end + + local defaults = make_defaults(cfg.lua_version, processor, platforms, cfg.home) + + if platforms.windows and hardcoded.WIN_TOOLS then + local tools = { "SEVENZ", "CP", "FIND", "LS", "MD5SUM", "WGET", } + for _, tool in ipairs(tools) do + defaults.variables[tool] = '"' .. dir.path(hardcoded.WIN_TOOLS, defaults.variables[tool] .. '.exe') .. '"' + end + else + defaults.fs_use_modules = true + end + + -- if only cfg.variables.LUA is given in config files, + -- derive LUA_BINDIR and LUA_DIR from them. + if cfg.variables.LUA and not cfg.variables.LUA_BINDIR then + cfg.variables.LUA_BINDIR = cfg.variables.LUA:match("^(.*)[\\/][^\\/]*$") + if not cfg.variables.LUA_DIR then + cfg.variables.LUA_DIR = cfg.variables.LUA_BINDIR:gsub("[\\/]bin$", "") or cfg.variables.LUA_BINDIR + end + end + + use_defaults(cfg, defaults) + + cfg.user_agent = "LuaRocks/"..cfg.program_version.." "..cfg.arch + + cfg.config_files = { + project = cfg.project_dir and { + file = project_config_file, + found = not not project_config_ok, + }, + system = { + file = sys_config_file, + found = not not sys_config_ok, + }, + user = { + file = home_config_file, + found = not not home_config_ok, + }, + nearest = project_config_ok + and project_config_file + or (home_config_ok + and home_config_file + or sys_config_file), + } + + cfg.cache = {} + + ---------------------------------------- + -- Attributes of cfg are set. + -- Let's add some methods. + ---------------------------------------- + + do + local function make_paths_from_tree(tree) + local lua_path, lib_path, bin_path + if type(tree) == "string" then + lua_path = dir.path(tree, cfg.lua_modules_path) + lib_path = dir.path(tree, cfg.lib_modules_path) + bin_path = dir.path(tree, "bin") + else + lua_path = tree.lua_dir or dir.path(tree.root, cfg.lua_modules_path) + lib_path = tree.lib_dir or dir.path(tree.root, cfg.lib_modules_path) + bin_path = tree.bin_dir or dir.path(tree.root, "bin") + end + return lua_path, lib_path, bin_path + end + + function cfg.package_paths(current) + local new_path, new_cpath, new_bin = {}, {}, {} + local function add_tree_to_paths(tree) + local lua_path, lib_path, bin_path = make_paths_from_tree(tree) + table.insert(new_path, dir.path(lua_path, "?.lua")) + table.insert(new_path, dir.path(lua_path, "?", "init.lua")) + table.insert(new_cpath, dir.path(lib_path, "?."..cfg.lib_extension)) + table.insert(new_bin, bin_path) + end + if current then + add_tree_to_paths(current) + end + for _,tree in ipairs(cfg.rocks_trees) do + add_tree_to_paths(tree) + end + return table.concat(new_path, ";"), table.concat(new_cpath, ";"), table.concat(new_bin, cfg.export_path_separator) + end + end + + function cfg.init_package_paths() + local lr_path, lr_cpath, lr_bin = cfg.package_paths() + package.path = util.cleanup_path(package.path .. ";" .. lr_path, ";", cfg.lua_version, true) + package.cpath = util.cleanup_path(package.cpath .. ";" .. lr_cpath, ";", cfg.lua_version, true) + end + + --- Check if platform was detected + -- @param name string: The platform name to check. + -- @return boolean: true if LuaRocks is currently running on queried platform. + function cfg.is_platform(name) + assert(type(name) == "string") + return platforms[name] + end + + -- @param direction (optional) "least-specific-first" (default) or "most-specific-first" + function cfg.each_platform(direction) + direction = direction or "least-specific-first" + local i, delta + if direction == "least-specific-first" then + i = 0 + delta = 1 + else + i = #platform_order + 1 + delta = -1 + end + return function() + local p + repeat + i = i + delta + p = platform_order[i] + until (not p) or platforms[p] + return p + end + end + + function cfg.print_platforms() + local platform_keys = {} + for k,_ in pairs(platforms) do + table.insert(platform_keys, k) + end + table.sort(platform_keys) + return table.concat(platform_keys, ", ") + end + + return exit_ok, exit_err, exit_what +end + +return cfg diff --git a/src/luarocks/core/dir.lua b/src/luarocks/core/dir.lua new file mode 100644 index 0000000..5d6f2c9 --- /dev/null +++ b/src/luarocks/core/dir.lua @@ -0,0 +1,98 @@ + +local dir = {} + +local require = nil +-------------------------------------------------------------------------------- + +local dir_sep = package.config:sub(1, 1) + +local function unquote(c) + local first, last = c:sub(1,1), c:sub(-1) + if (first == '"' and last == '"') or + (first == "'" and last == "'") then + return c:sub(2,-2) + end + return c +end + +--- Describe a path in a cross-platform way. +-- Use this function to avoid platform-specific directory +-- separators in other modules. Removes trailing slashes from +-- each component given, to avoid repeated separators. +-- Separators inside strings are kept, to handle URLs containing +-- protocols. +-- @param ... strings representing directories +-- @return string: a string with a platform-specific representation +-- of the path. +function dir.path(...) + local t = {...} + while t[1] == "" do + table.remove(t, 1) + end + for i, c in ipairs(t) do + t[i] = unquote(c) + end + return dir.normalize(table.concat(t, "/")) +end + +--- Split protocol and path from an URL or local pathname. +-- URLs should be in the "protocol://path" format. +-- For local pathnames, "file" is returned as the protocol. +-- @param url string: an URL or a local pathname. +-- @return string, string: the protocol, and the pathname without the protocol. +function dir.split_url(url) + assert(type(url) == "string") + + url = unquote(url) + local protocol, pathname = url:match("^([^:]*)://(.*)") + if not protocol then + protocol = "file" + pathname = url + end + return protocol, pathname +end + +--- Normalize a url or local path. +-- URLs should be in the "protocol://path" format. +-- Removes trailing and double slashes, and '.' and '..' components. +-- for 'file' URLs, the native system's slashes are used. +-- @param url string: an URL or a local pathname. +-- @return string: Normalized result. +function dir.normalize(name) + local protocol, pathname = dir.split_url(name) + pathname = pathname:gsub("\\", "/"):gsub("(.)/*$", "%1"):gsub("//", "/") + local pieces = {} + local drive = "" + if pathname:match("^.:") then + drive, pathname = pathname:match("^(.:)(.*)$") + end + pathname = pathname .. "/" + for piece in pathname:gmatch("(.-)/") do + if piece == ".." then + local prev = pieces[#pieces] + if not prev or prev == ".." then + table.insert(pieces, "..") + elseif prev ~= "" then + table.remove(pieces) + end + elseif piece ~= "." then + table.insert(pieces, piece) + end + end + if #pieces == 0 then + pathname = drive .. "." + elseif #pieces == 1 and pieces[1] == "" then + pathname = drive .. "/" + else + pathname = drive .. table.concat(pieces, "/") + end + if protocol ~= "file" then + pathname = protocol .. "://" .. pathname + else + pathname = pathname:gsub("/", dir_sep) + end + return pathname +end + +return dir + diff --git a/src/luarocks/core/manif.lua b/src/luarocks/core/manif.lua new file mode 100644 index 0000000..3925f63 --- /dev/null +++ b/src/luarocks/core/manif.lua @@ -0,0 +1,114 @@ + +--- Core functions for querying manifest files. +local manif = {} + +local persist = require("luarocks.core.persist") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.core.dir") +local util = require("luarocks.core.util") +local vers = require("luarocks.core.vers") +local path = require("luarocks.core.path") +local require = nil +-------------------------------------------------------------------------------- + +-- Table with repository identifiers as keys and tables mapping +-- Lua versions to cached loaded manifests as values. +local manifest_cache = {} + +--- Cache a loaded manifest. +-- @param repo_url string: The repository identifier. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @param manifest table: the manifest to be cached. +function manif.cache_manifest(repo_url, lua_version, manifest) + lua_version = lua_version or cfg.lua_version + manifest_cache[repo_url] = manifest_cache[repo_url] or {} + manifest_cache[repo_url][lua_version] = manifest +end + +--- Attempt to get cached loaded manifest. +-- @param repo_url string: The repository identifier. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @return table or nil: loaded manifest or nil if cache is empty. +function manif.get_cached_manifest(repo_url, lua_version) + lua_version = lua_version or cfg.lua_version + return manifest_cache[repo_url] and manifest_cache[repo_url][lua_version] +end + +--- Back-end function that actually loads the manifest +-- and stores it in the manifest cache. +-- @param file string: The local filename of the manifest file. +-- @param repo_url string: The repository identifier. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @return table or (nil, string, string): the manifest or nil, +-- error message and error code ("open", "load", "run"). +function manif.manifest_loader(file, repo_url, lua_version) + local manifest, err, errcode = persist.load_into_table(file) + if not manifest then + return nil, "Failed loading manifest for "..repo_url..": "..err, errcode + end + manif.cache_manifest(repo_url, lua_version, manifest) + return manifest, err, errcode +end + +--- Load a local manifest describing a repository. +-- This is used by the luarocks.loader only. +-- @param repo_url string: URL or pathname for the repository. +-- @return table or (nil, string, string): A table representing the manifest, +-- or nil followed by an error message and an error code, see manifest_loader. +function manif.fast_load_local_manifest(repo_url) + assert(type(repo_url) == "string") + + local cached_manifest = manif.get_cached_manifest(repo_url) + if cached_manifest then + return cached_manifest + end + + local pathname = dir.path(repo_url, "manifest") + return manif.manifest_loader(pathname, repo_url, nil, true) +end + +function manif.load_rocks_tree_manifests(deps_mode) + local trees = {} + path.map_trees(deps_mode, function(tree) + local manifest, err = manif.fast_load_local_manifest(path.rocks_dir(tree)) + if manifest then + table.insert(trees, {tree=tree, manifest=manifest}) + end + end) + return trees +end + +function manif.scan_dependencies(name, version, tree_manifests, dest) + if dest[name] then + return + end + dest[name] = version + + for _, tree in ipairs(tree_manifests) do + local manifest = tree.manifest + + local pkgdeps + if manifest.dependencies and manifest.dependencies[name] then + pkgdeps = manifest.dependencies[name][version] + end + if pkgdeps then + for _, dep in ipairs(pkgdeps) do + local pkg, constraints = dep.name, dep.constraints + + for _, t in ipairs(tree_manifests) do + local entries = t.manifest.repository[pkg] + if entries then + for ver, _ in util.sortedpairs(entries, vers.compare_versions) do + if (not constraints) or vers.match_constraints(vers.parse_version(ver), constraints) then + manif.scan_dependencies(pkg, ver, tree_manifests, dest) + end + end + end + end + end + return + end + end +end + +return manif diff --git a/src/luarocks/core/path.lua b/src/luarocks/core/path.lua new file mode 100644 index 0000000..2f037b4 --- /dev/null +++ b/src/luarocks/core/path.lua @@ -0,0 +1,157 @@ + +--- Core LuaRocks-specific path handling functions. +local path = {} + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.core.dir") +local require = nil + +local dir_sep = package.config:sub(1, 1) +-------------------------------------------------------------------------------- + +function path.rocks_dir(tree) + if tree == nil then + tree = cfg.root_dir + end + if type(tree) == "string" then + return dir.path(tree, cfg.rocks_subdir) + end + assert(type(tree) == "table") + return tree.rocks_dir or dir.path(tree.root, cfg.rocks_subdir) +end + +--- Produce a versioned version of a filename. +-- @param file string: filename (must start with prefix) +-- @param prefix string: Path prefix for file +-- @param name string: Rock name +-- @param version string: Rock version +-- @return string: a pathname with the same directory parts and a versioned basename. +function path.versioned_name(file, prefix, name, version) + assert(type(file) == "string") + assert(type(name) == "string" and not name:match(dir_sep)) + assert(type(version) == "string") + + local rest = file:sub(#prefix+1):gsub("^" .. dir_sep .. "*", "") + local name_version = (name.."_"..version):gsub("%-", "_"):gsub("%.", "_") + return dir.path(prefix, name_version.."-"..rest) +end + +--- Convert a pathname to a module identifier. +-- In Unix, for example, a path "foo/bar/baz.lua" is converted to +-- "foo.bar.baz"; "bla/init.lua" returns "bla.init"; "foo.so" returns "foo". +-- @param file string: Pathname of module +-- @return string: The module identifier, or nil if given path is +-- not a conformant module path (the function does not check if the +-- path actually exists). +function path.path_to_module(file) + assert(type(file) == "string") + + local exts = {} + local paths = package.path .. ";" .. package.cpath + for entry in paths:gmatch("[^;]+") do + local ext = entry:match("%.([a-z]+)$") + if ext then + exts[ext] = true + end + end + + local name + for ext, _ in pairs(exts) do + name = file:match("(.*)%." .. ext .. "$") + if name then + name = name:gsub("[\\/]", ".") + break + end + end + + if not name then name = file end + + -- remove any beginning and trailing slashes-converted-to-dots + name = name:gsub("^%.+", ""):gsub("%.+$", "") + + return name +end + +function path.deploy_lua_dir(tree) + if type(tree) == "string" then + return dir.path(tree, cfg.lua_modules_path) + else + assert(type(tree) == "table") + return tree.lua_dir or dir.path(tree.root, cfg.lua_modules_path) + end +end + +function path.deploy_lib_dir(tree) + if type(tree) == "string" then + return dir.path(tree, cfg.lib_modules_path) + else + assert(type(tree) == "table") + return tree.lib_dir or dir.path(tree.root, cfg.lib_modules_path) + end +end + +local is_src_extension = { [".lua"] = true, [".tl"] = true, [".tld"] = true, [".moon"] = true } + +--- Return the pathname of the file that would be loaded for a module, indexed. +-- @param file_name string: module file name as in manifest (eg. "socket/core.so") +-- @param name string: name of the package (eg. "luasocket") +-- @param version string: version number (eg. "2.0.2-1") +-- @param tree string: repository path (eg. "/usr/local") +-- @param i number: the index, 1 if version is the current default, > 1 otherwise. +-- This is done this way for use by select_module in luarocks.loader. +-- @return string: filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so") +function path.which_i(file_name, name, version, tree, i) + local deploy_dir + local extension = file_name:match("%.[a-z]+$") + if is_src_extension[extension] then + deploy_dir = path.deploy_lua_dir(tree) + file_name = dir.path(deploy_dir, file_name) + else + deploy_dir = path.deploy_lib_dir(tree) + file_name = dir.path(deploy_dir, file_name) + end + if i > 1 then + file_name = path.versioned_name(file_name, deploy_dir, name, version) + end + return file_name +end + +function path.rocks_tree_to_string(tree) + if type(tree) == "string" then + return tree + else + assert(type(tree) == "table") + return tree.root + end +end + +--- Apply a given function to the active rocks trees based on chosen dependency mode. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for no trees (this function becomes a nop). +-- @param fn function: function to be applied, with the tree dir (string) as the first +-- argument and the remaining varargs of map_trees as the following arguments. +-- @return a table with all results of invocations of fn collected. +function path.map_trees(deps_mode, fn, ...) + local result = {} + local current = cfg.root_dir or cfg.rocks_trees[1] + if deps_mode == "one" then + table.insert(result, (fn(current, ...)) or 0) + else + local use = false + if deps_mode == "all" then + use = true + end + for _, tree in ipairs(cfg.rocks_trees or {}) do + if dir.normalize(path.rocks_tree_to_string(tree)) == dir.normalize(path.rocks_tree_to_string(current)) then + use = true + end + if use then + table.insert(result, (fn(tree, ...)) or 0) + end + end + end + return result +end + +return path diff --git a/src/luarocks/core/persist.lua b/src/luarocks/core/persist.lua new file mode 100644 index 0000000..57e7b5d --- /dev/null +++ b/src/luarocks/core/persist.lua @@ -0,0 +1,81 @@ + +local persist = {} + +local require = nil +-------------------------------------------------------------------------------- + +--- Load and run a Lua file in an environment. +-- @param filename string: the name of the file. +-- @param env table: the environment table. +-- @return (true, any) or (nil, string, string): true and the return value +-- of the file, or nil, an error message and an error code ("open", "load" +-- or "run") in case of errors. +function persist.run_file(filename, env) + local fd, err = io.open(filename) + if not fd then + return nil, err, "open" + end + local str, err = fd:read("*a") + fd:close() + if not str then + return nil, err, "open" + end + str = str:gsub("^#![^\n]*\n", "") + local chunk, ran + if _VERSION == "Lua 5.1" then -- Lua 5.1 + chunk, err = loadstring(str, filename) + if chunk then + setfenv(chunk, env) + ran, err = pcall(chunk) + end + else -- Lua 5.2 + chunk, err = load(str, filename, "t", env) + if chunk then + ran, err = pcall(chunk) + end + end + if not chunk then + return nil, "Error loading file: "..err, "load" + end + if not ran then + return nil, "Error running file: "..err, "run" + end + return true, err +end + +--- Load a Lua file containing assignments, storing them in a table. +-- The global environment is not propagated to the loaded file. +-- @param filename string: the name of the file. +-- @param tbl table or nil: if given, this table is used to store +-- loaded values. +-- @return (table, table) or (nil, string, string): a table with the file's assignments +-- as fields and set of undefined globals accessed in file, +-- or nil, an error message and an error code ("open"; couldn't open the file, +-- "load"; compile-time error, or "run"; run-time error) +-- in case of errors. +function persist.load_into_table(filename, tbl) + assert(type(filename) == "string") + assert(type(tbl) == "table" or not tbl) + + local result = tbl or {} + local globals = {} + local globals_mt = { + __index = function(t, k) + globals[k] = true + end + } + local save_mt = getmetatable(result) + setmetatable(result, globals_mt) + + local ok, err, errcode = persist.run_file(filename, result) + + setmetatable(result, save_mt) + + if not ok then + return nil, err, errcode + end + return result, globals +end + +return persist + diff --git a/src/luarocks/core/sysdetect.lua b/src/luarocks/core/sysdetect.lua new file mode 100644 index 0000000..06454f2 --- /dev/null +++ b/src/luarocks/core/sysdetect.lua @@ -0,0 +1,419 @@ +-- Detect the operating system and architecture without forking a subprocess. +-- +-- We are not going for exhaustive list of every historical system here, +-- but aiming to cover every platform where LuaRocks is known to run. +-- If your system is not detected, patches are welcome! + +local sysdetect = {} + +local function hex(s) + return s:gsub("$(..)", function(x) + return string.char(tonumber(x, 16)) + end) +end + +local function read_int8(fd) + if io.type(fd) == "closed file" then + return nil + end + local s = fd:read(1) + if not s then + fd:close() + return nil + end + return s:byte() +end + +local LITTLE = 1 +-- local BIG = 2 + +local function bytes2number(s, endian) + local r = 0 + if endian == LITTLE then + for i = #s, 1, -1 do + r = r*256 + s:byte(i,i) + end + else + for i = 1, #s do + r = r*256 + s:byte(i,i) + end + end + return r +end + +local function read(fd, bytes, endian) + if io.type(fd) == "closed file" then + return nil + end + local s = fd:read(bytes) + if not s + then fd:close() + return nil + end + return bytes2number(s, endian) +end + +local function read_int32le(fd) + return read(fd, 4, LITTLE) +end + +-------------------------------------------------------------------------------- +-- @section ELF +-------------------------------------------------------------------------------- + +local e_osabi = { + [0x00] = "sysv", + [0x01] = "hpux", + [0x02] = "netbsd", + [0x03] = "linux", + [0x04] = "hurd", + [0x06] = "solaris", + [0x07] = "aix", + [0x08] = "irix", + [0x09] = "freebsd", + [0x0c] = "openbsd", +} + +local e_machines = { + [0x02] = "sparc", + [0x03] = "x86", + [0x08] = "mips", + [0x0f] = "hppa", + [0x12] = "sparcv8", + [0x14] = "ppc", + [0x15] = "ppc64", + [0x16] = "s390", + [0x28] = "arm", + [0x2a] = "superh", + [0x2b] = "sparcv9", + [0x32] = "ia_64", + [0x3E] = "x86_64", + [0xB6] = "alpha", + [0xB7] = "aarch64", + [0xF3] = "riscv64", + [0x9026] = "alpha", +} + +local SHT_NOTE = 7 + +local function read_elf_section_headers(fd, hdr) + local endian = hdr.endian + local word = hdr.word + + local strtab_offset + local sections = {} + for i = 0, hdr.e_shnum - 1 do + fd:seek("set", hdr.e_shoff + (i * hdr.e_shentsize)) + local section = {} + section.sh_name_off = read(fd, 4, endian) + section.sh_type = read(fd, 4, endian) + section.sh_flags = read(fd, word, endian) + section.sh_addr = read(fd, word, endian) + section.sh_offset = read(fd, word, endian) + section.sh_size = read(fd, word, endian) + section.sh_link = read(fd, 4, endian) + section.sh_info = read(fd, 4, endian) + if section.sh_type == SHT_NOTE then + fd:seek("set", section.sh_offset) + section.namesz = read(fd, 4, endian) + section.descsz = read(fd, 4, endian) + section.type = read(fd, 4, endian) + section.namedata = fd:read(section.namesz):gsub("%z.*", "") + section.descdata = fd:read(section.descsz) + elseif i == hdr.e_shstrndx then + strtab_offset = section.sh_offset + end + table.insert(sections, section) + end + if strtab_offset then + for _, section in ipairs(sections) do + fd:seek("set", strtab_offset + section.sh_name_off) + section.name = fd:read(32):gsub("%z.*", "") + sections[section.name] = section + end + end + return sections +end + +local function detect_elf_system(fd, hdr, sections) + local system = e_osabi[hdr.osabi] + local endian = hdr.endian + + if system == "sysv" then + local abitag = sections[".note.ABI-tag"] + if abitag then + if abitag.namedata == "GNU" and abitag.type == 1 + and abitag.descdata:sub(0, 4) == "\0\0\0\0" then + return "linux" + end + elseif sections[".SUNW_version"] + or sections[".SUNW_signature"] then + return "solaris" + elseif sections[".note.netbsd.ident"] then + return "netbsd" + elseif sections[".note.openbsd.ident"] then + return "openbsd" + elseif sections[".note.tag"] and + sections[".note.tag"].namedata == "DragonFly" then + return "dragonfly" + end + + local gnu_version_r = sections[".gnu.version_r"] + if gnu_version_r then + + local dynstr = sections[".dynstr"].sh_offset + + local idx = 0 + for _ = 0, gnu_version_r.sh_info - 1 do + fd:seek("set", gnu_version_r.sh_offset + idx) + assert(read(fd, 2, endian)) -- vn_version + local vn_cnt = read(fd, 2, endian) + local vn_file = read(fd, 4, endian) + local vn_next = read(fd, 2, endian) + + fd:seek("set", dynstr + vn_file) + local libname = fd:read(64):gsub("%z.*", "") + + if hdr.e_type == 0x03 and libname == "libroot.so" then + return "haiku" + elseif libname:match("linux") then + return "linux" + end + + idx = idx + (vn_next * (vn_cnt + 1)) + end + end + + local procfile = io.open("/proc/sys/kernel/ostype") + if procfile then + local version = procfile:read(6) + procfile:close() + if version == "Linux\n" then + return "linux" + end + end + end + + return system +end + +local function read_elf_header(fd) + local hdr = {} + + hdr.bits = read_int8(fd) + hdr.endian = read_int8(fd) + hdr.elf_version = read_int8(fd) + if hdr.elf_version ~= 1 then + return nil + end + hdr.osabi = read_int8(fd) + if not hdr.osabi then + return nil + end + + local endian = hdr.endian + fd:seek("set", 0x10) + hdr.e_type = read(fd, 2, endian) + local machine = read(fd, 2, endian) + local processor = e_machines[machine] or "unknown" + if endian == 1 and processor == "ppc64" then + processor = "ppc64le" + end + + local elfversion = read(fd, 4, endian) + if elfversion ~= 1 then + return nil + end + + local word = (hdr.bits == 1) and 4 or 8 + hdr.word = word + + hdr.e_entry = read(fd, word, endian) + hdr.e_phoff = read(fd, word, endian) + hdr.e_shoff = read(fd, word, endian) + hdr.e_flags = read(fd, 4, endian) + hdr.e_ehsize = read(fd, 2, endian) + hdr.e_phentsize = read(fd, 2, endian) + hdr.e_phnum = read(fd, 2, endian) + hdr.e_shentsize = read(fd, 2, endian) + hdr.e_shnum = read(fd, 2, endian) + hdr.e_shstrndx = read(fd, 2, endian) + + return hdr, processor +end + +local function detect_elf(fd) + local hdr, processor = read_elf_header(fd) + if not hdr then + return nil + end + local sections = read_elf_section_headers(fd, hdr) + local system = detect_elf_system(fd, hdr, sections) + return system, processor +end + +-------------------------------------------------------------------------------- +-- @section Mach Objects (Apple) +-------------------------------------------------------------------------------- + +local mach_l64 = { + [7] = "x86_64", + [12] = "aarch64", +} + +local mach_b64 = { + [0] = "ppc64", +} + +local mach_l32 = { + [7] = "x86", + [12] = "arm", +} + +local mach_b32 = { + [0] = "ppc", +} + +local function detect_mach(magic, fd) + if not magic then + return nil + end + + if magic == hex("$CA$FE$BA$BE") then + -- fat binary, go for the first one + fd:seek("set", 0x12) + local offs = read_int8(fd) + if not offs then + return nil + end + fd:seek("set", offs * 256) + magic = fd:read(4) + return detect_mach(magic, fd) + end + + local cputype = read_int8(fd) + + if magic == hex("$CF$FA$ED$FE") then + return "macosx", mach_l64[cputype] or "unknown" + elseif magic == hex("$FE$ED$CF$FA") then + return "macosx", mach_b64[cputype] or "unknown" + elseif magic == hex("$CE$FA$ED$FE") then + return "macosx", mach_l32[cputype] or "unknown" + elseif magic == hex("$FE$ED$FA$CE") then + return "macosx", mach_b32[cputype] or "unknown" + end +end + +-------------------------------------------------------------------------------- +-- @section PE (Windows) +-------------------------------------------------------------------------------- + +local pe_machine = { + [0x8664] = "x86_64", + [0x01c0] = "arm", + [0x01c4] = "armv7l", + [0xaa64] = "arm64", + [0x014c] = "x86", +} + +local function detect_pe(fd) + fd:seek("set", 60) -- position of PE header position + local peoffset = read_int32le(fd) -- read position of PE header + if not peoffset then + return nil + end + local system = "windows" + fd:seek("set", peoffset + 4) -- move to position of Machine section + local machine = read(fd, 2, LITTLE) + local processor = pe_machine[machine] + + local rdata_pos = fd:read(736):match(".rdata%z%z............(....)") + if rdata_pos then + rdata_pos = bytes2number(rdata_pos, LITTLE) + fd:seek("set", rdata_pos) + local data = fd:read(512) + if data:match("cygwin") or data:match("cyggcc") then + system = "cygwin" + end + end + + return system, processor or "unknown" +end + +-------------------------------------------------------------------------------- +-- @section API +-------------------------------------------------------------------------------- + +function sysdetect.detect_file(file) + assert(type(file) == "string") + local fd = io.open(file, "rb") + if not fd then + return nil + end + local magic = fd:read(4) + if magic == hex("$7FELF") then + return detect_elf(fd) + end + if magic == hex("MZ$90$00") then + return detect_pe(fd) + end + return detect_mach(magic, fd) +end + +local cache_system +local cache_processor + +function sysdetect.detect(input_file) + local dirsep = package.config:sub(1,1) + local files + + if input_file then + files = { input_file } + else + if cache_system then + return cache_system, cache_processor + end + + local PATHsep + local interp = arg and arg[-1] + if dirsep == "/" then + -- Unix + files = { + "/bin/sh", -- Unix: well-known POSIX path + "/proc/self/exe", -- Linux: this should always have a working binary + } + PATHsep = ":" + else + -- Windows + local systemroot = os.getenv("SystemRoot") + files = { + systemroot .. "\\system32\\notepad.exe", -- well-known Windows path + systemroot .. "\\explorer.exe", -- well-known Windows path + } + if interp and not interp:lower():match("exe$") then + interp = interp .. ".exe" + end + PATHsep = ";" + end + if interp then + if interp:match(dirsep) then + -- interpreter path is absolute + table.insert(files, 1, interp) + else + for d in (os.getenv("PATH") or ""):gmatch("[^"..PATHsep.."]+") do + table.insert(files, d .. dirsep .. interp) + end + end + end + end + for _, f in ipairs(files) do + local system, processor = sysdetect.detect_file(f) + if system then + cache_system = system + cache_processor = processor + return system, processor + end + end +end + +return sysdetect diff --git a/src/luarocks/core/util.lua b/src/luarocks/core/util.lua new file mode 100644 index 0000000..e9abdd3 --- /dev/null +++ b/src/luarocks/core/util.lua @@ -0,0 +1,322 @@ + +local util = {} + +local require = nil +-------------------------------------------------------------------------------- + +local dir_sep = package.config:sub(1, 1) + +--- Run a process and read a its output. +-- Equivalent to io.popen(cmd):read("*l"), except that it +-- closes the fd right away. +-- @param cmd string: The command to execute +-- @param spec string: "*l" by default, to read a single line. +-- May be used to read more, passing, for instance, "*a". +-- @return string: the output of the program. +function util.popen_read(cmd, spec) + local tmpfile = (dir_sep == "\\") + and (os.getenv("TMP") .. "/luarocks-" .. tostring(math.floor(math.random() * 10000))) + or os.tmpname() + os.execute(cmd .. " > " .. tmpfile) + local fd = io.open(tmpfile, "rb") + if not fd then + os.remove(tmpfile) + return "" + end + local out = fd:read(spec or "*l") + fd:close() + os.remove(tmpfile) + return out or "" +end + +--- +-- Formats tables with cycles recursively to any depth. +-- References to other tables are shown as values. +-- Self references are indicated. +-- The string returned is "Lua code", which can be processed +-- (in the case in which indent is composed by spaces or "--"). +-- Userdata and function keys and values are shown as strings, +-- which logically are exactly not equivalent to the original code. +-- This routine can serve for pretty formating tables with +-- proper indentations, apart from printing them: +-- io.write(table.show(t, "t")) -- a typical use +-- Written by Julio Manuel Fernandez-Diaz, +-- Heavily based on "Saving tables with cycles", PIL2, p. 113. +-- @param t table: is the table. +-- @param tname string: is the name of the table (optional) +-- @param top_indent string: is a first indentation (optional). +-- @return string: the pretty-printed table +function util.show_table(t, tname, top_indent) + local cart -- a container + local autoref -- for self references + + local function is_empty_table(tbl) return next(tbl) == nil end + + local function basic_serialize(o) + local so = tostring(o) + if type(o) == "function" then + local info = debug and debug.getinfo(o, "S") + if not info then + return ("%q"):format(so) + end + -- info.name is nil because o is not a calling level + if info.what == "C" then + return ("%q"):format(so .. ", C function") + else + -- the information is defined through lines + return ("%q"):format(so .. ", defined in (" .. info.linedefined .. "-" .. info.lastlinedefined .. ")" .. info.source) + end + elseif type(o) == "number" then + return so + else + return ("%q"):format(so) + end + end + + local function add_to_cart(value, name, indent, saved, field) + indent = indent or "" + saved = saved or {} + field = field or name + + cart = cart .. indent .. field + + if type(value) ~= "table" then + cart = cart .. " = " .. basic_serialize(value) .. ";\n" + else + if saved[value] then + cart = cart .. " = {}; -- " .. saved[value] .. " (self reference)\n" + autoref = autoref .. name .. " = " .. saved[value] .. ";\n" + else + saved[value] = name + if is_empty_table(value) then + cart = cart .. " = {};\n" + else + cart = cart .. " = {\n" + for k, v in pairs(value) do + k = basic_serialize(k) + local fname = ("%s[%s]"):format(name, k) + field = ("[%s]"):format(k) + -- three spaces between levels + add_to_cart(v, fname, indent .. " ", saved, field) + end + cart = cart .. indent .. "};\n" + end + end + end + end + + tname = tname or "__unnamed__" + if type(t) ~= "table" then + return tname .. " = " .. basic_serialize(t) + end + cart, autoref = "", "" + add_to_cart(t, tname, top_indent) + return cart .. autoref +end + +--- Merges contents of src on top of dst's contents +-- (i.e. if an key from src already exists in dst, replace it). +-- @param dst Destination table, which will receive src's contents. +-- @param src Table which provides new contents to dst. +function util.deep_merge(dst, src) + for k, v in pairs(src) do + if type(v) == "table" then + if dst[k] == nil then + dst[k] = {} + end + if type(dst[k]) == "table" then + util.deep_merge(dst[k], v) + else + dst[k] = v + end + else + dst[k] = v + end + end +end + +--- Merges contents of src below those of dst's contents +-- (i.e. if an key from src already exists in dst, do not replace it). +-- @param dst Destination table, which will receive src's contents. +-- @param src Table which provides new contents to dst. +function util.deep_merge_under(dst, src) + for k, v in pairs(src) do + if type(v) == "table" then + if dst[k] == nil then + dst[k] = {} + end + if type(dst[k]) == "table" then + util.deep_merge_under(dst[k], v) + end + elseif dst[k] == nil then + dst[k] = v + end + end +end + +--- Clean up a path-style string ($PATH, $LUA_PATH/package.path, etc.), +-- removing repeated entries and making sure only the relevant +-- Lua version is used. +-- Example: given ("a;b;c;a;b;d", ";"), returns "a;b;c;d". +-- @param list string: A path string (from $PATH or package.path) +-- @param sep string: The separator +-- @param lua_version (optional) string: The Lua version to use. +-- @param keep_first (optional) if true, keep first occurrence in case +-- of duplicates; otherwise keep last occurrence. The default is false. +function util.cleanup_path(list, sep, lua_version, keep_first) + assert(type(list) == "string") + assert(type(sep) == "string") + + list = list:gsub(dir_sep, "/") + + local parts = util.split_string(list, sep) + local final, entries = {}, {} + local start, stop, step + + if keep_first then + start, stop, step = 1, #parts, 1 + else + start, stop, step = #parts, 1, -1 + end + + for i = start, stop, step do + local part = parts[i]:gsub("//", "/") + if lua_version then + part = part:gsub("/lua/([%d.]+)/", function(part_version) + if part_version:sub(1, #lua_version) ~= lua_version then + return "/lua/"..lua_version.."/" + end + end) + end + if not entries[part] then + local at = keep_first and #final+1 or 1 + table.insert(final, at, part) + entries[part] = true + end + end + + return (table.concat(final, sep):gsub("/", dir_sep)) +end + +-- from http://lua-users.org/wiki/SplitJoin +-- by Philippe Lhoste +function util.split_string(str, delim, maxNb) + -- Eliminate bad cases... + if string.find(str, delim) == nil then + return { str } + end + if maxNb == nil or maxNb < 1 then + maxNb = 0 -- No limit + end + local result = {} + local pat = "(.-)" .. delim .. "()" + local nb = 0 + local lastPos + for part, pos in string.gmatch(str, pat) do + nb = nb + 1 + result[nb] = part + lastPos = pos + if nb == maxNb then break end + end + -- Handle the last field + if nb ~= maxNb then + result[nb + 1] = string.sub(str, lastPos) + end + return result +end + +--- Return an array of keys of a table. +-- @param tbl table: The input table. +-- @return table: The array of keys. +function util.keys(tbl) + local ks = {} + for k,_ in pairs(tbl) do + table.insert(ks, k) + end + return ks +end + +--- Print a line to standard error +function util.printerr(...) + io.stderr:write(table.concat({...},"\t")) + io.stderr:write("\n") +end + +--- Display a warning message. +-- @param msg string: the warning message +function util.warning(msg) + util.printerr("Warning: "..msg) +end + +--- Simple sort function used as a default for util.sortedpairs. +local function default_sort(a, b) + local ta = type(a) + local tb = type(b) + if ta == "number" and tb == "number" then + return a < b + elseif ta == "number" then + return true + elseif tb == "number" then + return false + else + return tostring(a) < tostring(b) + end +end + +--- A table iterator generator that returns elements sorted by key, +-- to be used in "for" loops. +-- @param tbl table: The table to be iterated. +-- @param sort_function function or table or nil: An optional comparison function +-- to be used by table.sort when sorting keys, or an array listing an explicit order +-- for keys. If a value itself is an array, it is taken so that the first element +-- is a string representing the field name, and the second element is a priority table +-- for that key, which is returned by the iterator as the third value after the key +-- and the value. +-- @return function: the iterator function. +function util.sortedpairs(tbl, sort_function) + sort_function = sort_function or default_sort + local keys = util.keys(tbl) + local sub_orders = {} + + if type(sort_function) == "function" then + table.sort(keys, sort_function) + else + local order = sort_function + local ordered_keys = {} + local all_keys = keys + keys = {} + + for _, order_entry in ipairs(order) do + local key, sub_order + if type(order_entry) == "table" then + key = order_entry[1] + sub_order = order_entry[2] + else + key = order_entry + end + + if tbl[key] then + ordered_keys[key] = true + sub_orders[key] = sub_order + table.insert(keys, key) + end + end + + table.sort(all_keys, default_sort) + for _, key in ipairs(all_keys) do + if not ordered_keys[key] then + table.insert(keys, key) + end + end + end + + local i = 1 + return function() + local key = keys[i] + i = i + 1 + return key, tbl[key], sub_orders[key] + end +end + +return util + diff --git a/src/luarocks/core/vers.lua b/src/luarocks/core/vers.lua new file mode 100644 index 0000000..8e61798 --- /dev/null +++ b/src/luarocks/core/vers.lua @@ -0,0 +1,207 @@ + +local vers = {} + +local util = require("luarocks.core.util") +local require = nil +-------------------------------------------------------------------------------- + +local deltas = { + dev = 120000000, + scm = 110000000, + cvs = 100000000, + rc = -1000, + pre = -10000, + beta = -100000, + alpha = -1000000 +} + +local version_mt = { + --- Equality comparison for versions. + -- All version numbers must be equal. + -- If both versions have revision numbers, they must be equal; + -- otherwise the revision number is ignored. + -- @param v1 table: version table to compare. + -- @param v2 table: version table to compare. + -- @return boolean: true if they are considered equivalent. + __eq = function(v1, v2) + if #v1 ~= #v2 then + return false + end + for i = 1, #v1 do + if v1[i] ~= v2[i] then + return false + end + end + if v1.revision and v2.revision then + return (v1.revision == v2.revision) + end + return true + end, + --- Size comparison for versions. + -- All version numbers are compared. + -- If both versions have revision numbers, they are compared; + -- otherwise the revision number is ignored. + -- @param v1 table: version table to compare. + -- @param v2 table: version table to compare. + -- @return boolean: true if v1 is considered lower than v2. + __lt = function(v1, v2) + for i = 1, math.max(#v1, #v2) do + local v1i, v2i = v1[i] or 0, v2[i] or 0 + if v1i ~= v2i then + return (v1i < v2i) + end + end + if v1.revision and v2.revision then + return (v1.revision < v2.revision) + end + return false + end, + -- @param v1 table: version table to compare. + -- @param v2 table: version table to compare. + -- @return boolean: true if v1 is considered lower than or equal to v2. + __le = function(v1, v2) + return not (v2 < v1) -- luacheck: ignore + end, + --- Return version as a string. + -- @param v The version table. + -- @return The string representation. + __tostring = function(v) + return v.string + end, +} + +local version_cache = {} +setmetatable(version_cache, { + __mode = "kv" +}) + +--- Parse a version string, converting to table format. +-- A version table contains all components of the version string +-- converted to numeric format, stored in the array part of the table. +-- If the version contains a revision, it is stored numerically +-- in the 'revision' field. The original string representation of +-- the string is preserved in the 'string' field. +-- Returned version tables use a metatable +-- allowing later comparison through relational operators. +-- @param vstring string: A version number in string format. +-- @return table or nil: A version table or nil +-- if the input string contains invalid characters. +function vers.parse_version(vstring) + if not vstring then return nil end + assert(type(vstring) == "string") + + local cached = version_cache[vstring] + if cached then + return cached + end + + local version = {} + local i = 1 + + local function add_token(number) + version[i] = version[i] and version[i] + number/100000 or number + i = i + 1 + end + + -- trim leading and trailing spaces + local v = vstring:match("^%s*(.*)%s*$") + version.string = v + -- store revision separately if any + local main, revision = v:match("(.*)%-(%d+)$") + if revision then + v = main + version.revision = tonumber(revision) + end + while #v > 0 do + -- extract a number + local token, rest = v:match("^(%d+)[%.%-%_]*(.*)") + if token then + add_token(tonumber(token)) + else + -- extract a word + token, rest = v:match("^(%a+)[%.%-%_]*(.*)") + if not token then + util.warning("version number '"..v.."' could not be parsed.") + version[i] = 0 + break + end + version[i] = deltas[token] or (token:byte() / 1000) + end + v = rest + end + setmetatable(version, version_mt) + version_cache[vstring] = version + return version +end + +--- Utility function to compare version numbers given as strings. +-- @param a string: one version. +-- @param b string: another version. +-- @return boolean: True if a > b. +function vers.compare_versions(a, b) + if a == b then + return false + end + return vers.parse_version(a) > vers.parse_version(b) +end + +--- A more lenient check for equivalence between versions. +-- This returns true if the requested components of a version +-- match and ignore the ones that were not given. For example, +-- when requesting "2", then "2", "2.1", "2.3.5-9"... all match. +-- When requesting "2.1", then "2.1", "2.1.3" match, but "2.2" +-- doesn't. +-- @param version string or table: Version to be tested; may be +-- in string format or already parsed into a table. +-- @param requested string or table: Version requested; may be +-- in string format or already parsed into a table. +-- @return boolean: True if the tested version matches the requested +-- version, false otherwise. +local function partial_match(version, requested) + assert(type(version) == "string" or type(version) == "table") + assert(type(requested) == "string" or type(version) == "table") + + if type(version) ~= "table" then version = vers.parse_version(version) end + if type(requested) ~= "table" then requested = vers.parse_version(requested) end + if not version or not requested then return false end + + for i, ri in ipairs(requested) do + local vi = version[i] or 0 + if ri ~= vi then return false end + end + if requested.revision then + return requested.revision == version.revision + end + return true +end + +--- Check if a version satisfies a set of constraints. +-- @param version table: A version in table format +-- @param constraints table: An array of constraints in table format. +-- @return boolean: True if version satisfies all constraints, +-- false otherwise. +function vers.match_constraints(version, constraints) + assert(type(version) == "table") + assert(type(constraints) == "table") + local ok = true + setmetatable(version, version_mt) + for _, constr in pairs(constraints) do + if type(constr.version) == "string" then + constr.version = vers.parse_version(constr.version) + end + local constr_version, constr_op = constr.version, constr.op + setmetatable(constr_version, version_mt) + if constr_op == "==" then ok = version == constr_version + elseif constr_op == "~=" then ok = version ~= constr_version + elseif constr_op == ">" then ok = version > constr_version + elseif constr_op == "<" then ok = version < constr_version + elseif constr_op == ">=" then ok = version >= constr_version + elseif constr_op == "<=" then ok = version <= constr_version + elseif constr_op == "~>" then ok = partial_match(version, constr_version) + end + if not ok then break end + end + return ok +end + +return vers diff --git a/src/luarocks/deplocks.lua b/src/luarocks/deplocks.lua new file mode 100644 index 0000000..d62908f --- /dev/null +++ b/src/luarocks/deplocks.lua @@ -0,0 +1,106 @@ +local deplocks = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local persist = require("luarocks.persist") + +local deptable = {} +local deptable_mode = "start" +local deplock_abs_filename +local deplock_root_rock_name + +function deplocks.init(root_rock_name, dirname) + if deptable_mode ~= "start" then + return + end + deptable_mode = "create" + + local filename = dir.path(dirname, "luarocks.lock") + deplock_abs_filename = fs.absolute_name(filename) + deplock_root_rock_name = root_rock_name + + deptable = {} +end + +function deplocks.get_abs_filename(root_rock_name) + if root_rock_name == deplock_root_rock_name then + return deplock_abs_filename + end +end + +function deplocks.load(root_rock_name, dirname) + if deptable_mode ~= "start" then + return true, nil + end + deptable_mode = "locked" + + local filename = dir.path(dirname, "luarocks.lock") + local ok, result, errcode = persist.run_file(filename, {}) + if errcode == "load" or errcode == "run" then + -- bad config file or depends on env, so error out + return nil, nil, "Could not read existing lockfile " .. filename + end + + if errcode == "open" then + -- could not open, maybe file does not exist + return true, nil + end + + deplock_abs_filename = fs.absolute_name(filename) + deplock_root_rock_name = root_rock_name + + deptable = result + return true, filename +end + +function deplocks.add(depskey, name, version) + if deptable_mode == "locked" then + return + end + + local dk = deptable[depskey] + if not dk then + dk = {} + deptable[depskey] = dk + end + + if not dk[name] then + dk[name] = version + end +end + +function deplocks.get(depskey, name) + local dk = deptable[depskey] + if not dk then + return nil + end + + return deptable[name] +end + +function deplocks.write_file() + if deptable_mode ~= "create" then + return true + end + + return persist.save_as_module(deplock_abs_filename, deptable) +end + +-- a table-like interface to deplocks +function deplocks.proxy(depskey) + return setmetatable({}, { + __index = function(_, k) + return deplocks.get(depskey, k) + end, + __newindex = function(_, k, v) + return deplocks.add(depskey, k, v) + end, + }) +end + +function deplocks.each(depskey) + return util.sortedpairs(deptable[depskey] or {}) +end + +return deplocks diff --git a/src/luarocks/deps.lua b/src/luarocks/deps.lua new file mode 100644 index 0000000..1cd500c --- /dev/null +++ b/src/luarocks/deps.lua @@ -0,0 +1,831 @@ + +--- High-level dependency related functions. +local deps = {} + +local cfg = require("luarocks.core.cfg") +local manif = require("luarocks.manif") +local path = require("luarocks.path") +local dir = require("luarocks.dir") +local fun = require("luarocks.fun") +local util = require("luarocks.util") +local vers = require("luarocks.core.vers") +local queries = require("luarocks.queries") +local deplocks = require("luarocks.deplocks") + +--- Generate a function that matches dep queries against the manifest, +-- taking into account rocks_provided, the list of versions to skip, +-- and the lockfile. +-- @param deps_mode "one", "none", "all" or "order" +-- @param rocks_provided a one-level table mapping names to versions, +-- listing rocks to consider provided by the VM +-- @param rocks_provided table: A table of auto-provided dependencies. +-- by this Lua implementation for the given dependency. +-- @param depskey key to use when matching the lockfile ("dependencies", +-- "build_dependencies", etc.) +-- @param skip_set a two-level table mapping names to versions to +-- boolean, listing rocks that should not be matched +-- @return function(dep): {string}, {string:string}, string, boolean +-- * array of matching versions +-- * map of versions to locations +-- * version matched via lockfile if any +-- * true if rock matched via rocks_provided +local function prepare_get_versions(deps_mode, rocks_provided, depskey, skip_set) + assert(type(deps_mode) == "string") + assert(type(rocks_provided) == "table") + assert(type(depskey) == "string") + assert(type(skip_set) == "table" or skip_set == nil) + + return function(dep) + local versions, locations + local provided = rocks_provided[dep.name] + if provided then + -- Provided rocks have higher priority than manifest's rocks. + versions, locations = { provided }, {} + else + if deps_mode == "none" then + deps_mode = "one" + end + versions, locations = manif.get_versions(dep, deps_mode) + end + + if skip_set and skip_set[dep.name] then + for i = #versions, 1, -1 do + local v = versions[i] + if skip_set[dep.name][v] then + table.remove(versions, i) + end + end + end + + local lockversion = deplocks.get(depskey, dep.name) + + return versions, locations, lockversion, provided ~= nil + end +end + +--- Attempt to match a dependency to an installed rock. +-- @param get_versions a getter function obtained via prepare_get_versions +-- @return (string, string, table) or (nil, nil, table): +-- 1. latest installed version of the rock matching the dependency +-- 2. location where the installed version is installed +-- 3. the 'dep' query table +-- 4. true if provided via VM +-- or +-- 1. nil +-- 2. nil +-- 3. either 'dep' or an alternative query to be used +-- 4. false +local function match_dep(dep, get_versions) + assert(type(dep) == "table") + assert(type(get_versions) == "function") + + local versions, locations, lockversion, provided = get_versions(dep) + + local latest_version + local latest_vstring + for _, vstring in ipairs(versions) do + local version = vers.parse_version(vstring) + if vers.match_constraints(version, dep.constraints) then + if not latest_version or version > latest_version then + latest_version = version + latest_vstring = vstring + end + end + end + + if lockversion and not locations[lockversion] then + local latest_matching_msg = "" + if latest_vstring and latest_vstring ~= lockversion then + latest_matching_msg = " (latest matching is " .. latest_vstring .. ")" + end + util.printout("Forcing " .. dep.name .. " to pinned version " .. lockversion .. latest_matching_msg) + return nil, nil, queries.new(dep.name, dep.namespace, lockversion) + end + + return latest_vstring, locations[latest_vstring], dep, provided +end + +local function match_all_deps(dependencies, get_versions) + assert(type(dependencies) == "table") + assert(type(get_versions) == "function") + + local matched, missing, no_upgrade = {}, {}, {} + + for _, dep in ipairs(dependencies) do + local found, _, provided + found, _, dep, provided = match_dep(dep, get_versions) + if found then + if not provided then + matched[dep] = {name = dep.name, version = found} + end + else + if dep.constraints[1] and dep.constraints[1].no_upgrade then + no_upgrade[dep.name] = dep + else + missing[dep.name] = dep + end + end + end + return matched, missing, no_upgrade +end + +--- Attempt to match dependencies of a rockspec to installed rocks. +-- @param dependencies table: The table of dependencies. +-- @param rocks_provided table: The table of auto-provided dependencies. +-- @param skip_set table or nil: Program versions to not use as valid matches. +-- Table where keys are program names and values are tables where keys +-- are program versions and values are 'true'. +-- @param deps_mode string: Which trees to check dependencies for +-- @return table, table, table: A table where keys are dependencies parsed +-- in table format and values are tables containing fields 'name' and +-- version' representing matches; a table of missing dependencies +-- parsed as tables; and a table of "no-upgrade" missing dependencies +-- (to be used in plugin modules so that a plugin does not force upgrade of +-- its parent application). +function deps.match_deps(dependencies, rocks_provided, skip_set, deps_mode) + assert(type(dependencies) == "table") + assert(type(rocks_provided) == "table") + assert(type(skip_set) == "table" or skip_set == nil) + assert(type(deps_mode) == "string") + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies", skip_set) + return match_all_deps(dependencies, get_versions) +end + +local function rock_status(dep, get_versions) + assert(dep:type() == "query") + assert(type(get_versions) == "function") + + local installed, _, _, provided = match_dep(dep, get_versions) + local installation_type = provided and "provided by VM" or "installed" + return installed and installed.." "..installation_type..": success" or "not installed" +end + +--- Check depenendencies of a package and report any missing ones. +-- @param name string: package name. +-- @param version string: package version. +-- @param dependencies table: array of dependencies. +-- @param deps_mode string: Which trees to check dependencies for +-- @param rocks_provided table: A table of auto-dependencies provided +-- by this Lua implementation for the given dependency. +-- "one" for the current default tree, "all" for all trees, +-- "order" for all trees with priority >= the current default, "none" for no trees. +function deps.report_missing_dependencies(name, version, dependencies, deps_mode, rocks_provided) + assert(type(name) == "string") + assert(type(version) == "string") + assert(type(dependencies) == "table") + assert(type(deps_mode) == "string") + assert(type(rocks_provided) == "table") + + if deps_mode == "none" then + return + end + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies") + + local first_missing_dep = true + + for _, dep in ipairs(dependencies) do + local found, _ + found, _, dep = match_dep(dep, get_versions) + if not found then + if first_missing_dep then + util.printout(("Missing dependencies for %s %s:"):format(name, version)) + first_missing_dep = false + end + + util.printout((" %s (%s)"):format(tostring(dep), rock_status(dep, get_versions))) + end + end +end + +function deps.fulfill_dependency(dep, deps_mode, rocks_provided, verify, depskey) + assert(dep:type() == "query") + assert(type(deps_mode) == "string" or deps_mode == nil) + assert(type(rocks_provided) == "table" or rocks_provided == nil) + assert(type(verify) == "boolean" or verify == nil) + assert(type(depskey) == "string") + + deps_mode = deps_mode or "all" + rocks_provided = rocks_provided or {} + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey) + + local found, where + found, where, dep = match_dep(dep, get_versions) + if found then + local tree_manifests = manif.load_rocks_tree_manifests(deps_mode) + manif.scan_dependencies(dep.name, found, tree_manifests, deplocks.proxy(depskey)) + return true, found, where + end + + local search = require("luarocks.search") + local install = require("luarocks.cmd.install") + + local url, search_err = search.find_suitable_rock(dep) + if not url then + return nil, "Could not satisfy dependency "..tostring(dep)..": "..search_err + end + util.printout("Installing "..url) + local install_args = { + rock = url, + deps_mode = deps_mode, + namespace = dep.namespace, + verify = verify, + } + local ok, install_err, errcode = install.command(install_args) + if not ok then + return nil, "Failed installing dependency: "..url.." - "..install_err, errcode + end + + found, where = match_dep(dep, get_versions) + assert(found) + return true, found, where +end + +local function check_supported_platforms(rockspec) + if rockspec.supported_platforms and next(rockspec.supported_platforms) then + local all_negative = true + local supported = false + for _, plat in pairs(rockspec.supported_platforms) do + local neg + neg, plat = plat:match("^(!?)(.*)") + if neg == "!" then + if cfg.is_platform(plat) then + return nil, "This rockspec for "..rockspec.package.." does not support "..plat.." platforms." + end + else + all_negative = false + if cfg.is_platform(plat) then + supported = true + break + end + end + end + if supported == false and not all_negative then + local plats = cfg.print_platforms() + return nil, "This rockspec for "..rockspec.package.." does not support "..plats.." platforms." + end + end + + return true +end + +--- Check dependencies of a rock and attempt to install any missing ones. +-- Packages are installed using the LuaRocks "install" command. +-- Aborts the program if a dependency could not be fulfilled. +-- @param rockspec table: A rockspec in table format. +-- @param depskey string: Rockspec key to fetch to get dependency table +-- ("dependencies", "build_dependencies", etc.). +-- @param deps_mode string +-- @param verify boolean +-- @param deplock_dir string: dirname of the deplock file +-- @return boolean or (nil, string, [string]): True if no errors occurred, or +-- nil and an error message if any test failed, followed by an optional +-- error code. +function deps.fulfill_dependencies(rockspec, depskey, deps_mode, verify, deplock_dir) + assert(type(rockspec) == "table") + assert(type(depskey) == "string") + assert(type(deps_mode) == "string") + assert(type(verify) == "boolean" or verify == nil) + assert(type(deplock_dir) == "string" or deplock_dir == nil) + + local name = rockspec.name + local version = rockspec.version + local rocks_provided = rockspec.rocks_provided + + local ok, filename, err = deplocks.load(name, deplock_dir or ".") + if filename then + util.printout("Using dependencies pinned in lockfile: " .. filename) + + local get_versions = prepare_get_versions("none", rocks_provided, depskey) + for dnsname, dversion in deplocks.each(depskey) do + local dname, dnamespace = util.split_namespace(dnsname) + local dep = queries.new(dname, dnamespace, dversion) + + util.printout(("%s %s is pinned to %s (%s)"):format( + name, version, tostring(dep), rock_status(dep, get_versions))) + + local ok, err = deps.fulfill_dependency(dep, "none", rocks_provided, verify, depskey) + if not ok then + return nil, err + end + end + util.printout() + return true + elseif err then + util.warning(err) + end + + ok, err = check_supported_platforms(rockspec) + if not ok then + return nil, err + end + + deps.report_missing_dependencies(name, version, rockspec[depskey], deps_mode, rocks_provided) + + util.printout() + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey) + for _, dep in ipairs(rockspec[depskey]) do + + util.printout(("%s %s depends on %s (%s)"):format( + name, version, tostring(dep), rock_status(dep, get_versions))) + + local ok, found_or_err, _, no_upgrade = deps.fulfill_dependency(dep, deps_mode, rocks_provided, verify, depskey) + if ok then + deplocks.add(depskey, dep.name, found_or_err) + else + if no_upgrade then + util.printerr("This version of "..name.." is designed for use with") + util.printerr(tostring(dep)..", but is configured to avoid upgrading it") + util.printerr("automatically. Please upgrade "..dep.name.." with") + util.printerr(" luarocks install "..dep.name) + util.printerr("or look for a suitable version of "..name.." with") + util.printerr(" luarocks search "..name) + end + return nil, found_or_err + end + end + + return true +end + +--- If filename matches a pattern, return the capture. +-- For example, given "libfoo.so" and "lib?.so" is a pattern, +-- returns "foo" (which can then be used to build names +-- based on other patterns. +-- @param file string: a filename +-- @param pattern string: a pattern, where ? is to be matched by the filename. +-- @return string The pattern, if found, or nil. +local function deconstruct_pattern(file, pattern) + local depattern = "^"..(pattern:gsub("%.", "%%."):gsub("%*", ".*"):gsub("?", "(.*)")).."$" + return (file:match(depattern)) +end + +--- Construct all possible patterns for a name and add to the files array. +-- Run through the patterns array replacing all occurrences of "?" +-- with the given file name and store them in the files array. +-- @param file string A raw name (e.g. "foo") +-- @param array of string An array of patterns with "?" as the wildcard +-- (e.g. {"?.so", "lib?.so"}) +-- @param files The array of constructed names +local function add_all_patterns(file, patterns, files) + for _, pattern in ipairs(patterns) do + table.insert(files, {#files + 1, (pattern:gsub("?", file))}) + end +end + +local function get_external_deps_dirs(mode) + local patterns = cfg.external_deps_patterns + local subdirs = cfg.external_deps_subdirs + if mode == "install" then + patterns = cfg.runtime_external_deps_patterns + subdirs = cfg.runtime_external_deps_subdirs + end + local dirs = { + BINDIR = { subdir = subdirs.bin, testfile = "program", pattern = patterns.bin }, + INCDIR = { subdir = subdirs.include, testfile = "header", pattern = patterns.include }, + LIBDIR = { subdir = subdirs.lib, testfile = "library", pattern = patterns.lib } + } + if mode == "install" then + dirs.INCDIR = nil + end + return dirs +end + +local function resolve_prefix(prefix, dirs) + if type(prefix) == "string" then + return prefix + elseif type(prefix) == "table" then + if prefix.bin then + dirs.BINDIR.subdir = prefix.bin + end + if prefix.include then + if dirs.INCDIR then + dirs.INCDIR.subdir = prefix.include + end + end + if prefix.lib then + dirs.LIBDIR.subdir = prefix.lib + end + return prefix.prefix + end +end + +local function add_patterns_for_file(files, file, patterns) + -- If it doesn't look like it contains a filename extension + if not (file:match("%.[a-z]+$") or file:match("%.[a-z]+%.")) then + add_all_patterns(file, patterns, files) + else + for _, pattern in ipairs(patterns) do + local matched = deconstruct_pattern(file, pattern) + if matched then + add_all_patterns(matched, patterns, files) + end + end + table.insert(files, {#files + 1, file}) + end +end + +local function check_external_dependency_at(prefix, name, ext_files, vars, dirs, err_files, cache) + local fs = require("luarocks.fs") + cache = cache or {} + + for dirname, dirdata in util.sortedpairs(dirs) do + local paths + local path_var_value = vars[name.."_"..dirname] + if path_var_value then + paths = { path_var_value } + elseif type(dirdata.subdir) == "table" then + paths = {} + for i,v in ipairs(dirdata.subdir) do + paths[i] = dir.path(prefix, v) + end + else + paths = { dir.path(prefix, dirdata.subdir) } + end + local file_or_files = ext_files[dirdata.testfile] + if file_or_files then + local files = {} + if type(file_or_files) == "string" then + add_patterns_for_file(files, file_or_files, dirdata.pattern) + elseif type(file_or_files) == "table" then + for _, f in ipairs(file_or_files) do + add_patterns_for_file(files, f, dirdata.pattern) + end + end + + local found = false + table.sort(files, function(a, b) + if (not a[2]:match("%*")) and b[2]:match("%*") then + return true + elseif a[2]:match("%*") and (not b[2]:match("%*")) then + return false + else + return a[1] < b[1] + end + end) + for _, fa in ipairs(files) do + + local f = fa[2] + -- small convenience hack + if f:match("%.so$") or f:match("%.dylib$") or f:match("%.dll$") then + f = f:gsub("%.[^.]+$", "."..cfg.external_lib_extension) + end + + local pattern + if f:match("%*") then + pattern = "^" .. f:gsub("([-.+])", "%%%1"):gsub("%*", ".*") .. "$" + f = "matching "..f + end + + for _, d in ipairs(paths) do + if pattern then + if not cache[d] then + cache[d] = fs.list_dir(d) + end + local match = string.match + for _, entry in ipairs(cache[d]) do + if match(entry, pattern) then + found = true + break + end + end + else + found = fs.is_file(dir.path(d, f)) + end + if found then + dirdata.dir = d + dirdata.file = f + break + else + table.insert(err_files[dirdata.testfile], f.." in "..d) + end + end + if found then + break + end + end + if not found then + return nil, dirname, dirdata.testfile + end + else + -- When we have a set of subdir suffixes, look for one that exists. + -- For these reason, we now put "lib" ahead of "" on Windows in our + -- default set. + dirdata.dir = paths[1] + for _, p in ipairs(paths) do + if fs.exists(p) then + dirdata.dir = p + break + end + end + end + end + + for dirname, dirdata in pairs(dirs) do + vars[name.."_"..dirname] = dirdata.dir + vars[name.."_"..dirname.."_FILE"] = dirdata.file + end + vars[name.."_DIR"] = prefix + return true +end + +local function check_external_dependency(name, ext_files, vars, mode, cache) + local ok + local err_dirname + local err_testfile + local err_files = {program = {}, header = {}, library = {}} + + local dirs = get_external_deps_dirs(mode) + + local prefixes + if vars[name .. "_DIR"] then + prefixes = { vars[name .. "_DIR"] } + elseif vars.DEPS_DIR then + prefixes = { vars.DEPS_DIR } + else + prefixes = cfg.external_deps_dirs + end + + for _, prefix in ipairs(prefixes) do + prefix = resolve_prefix(prefix, dirs) + if cfg.is_platform("mingw32") and name == "LUA" then + dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern), function(s) + return not s:match("%.a$") + end) + elseif cfg.is_platform("windows") and name == "LUA" then + dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern), function(s) + return not s:match("%.dll$") + end) + end + ok, err_dirname, err_testfile = check_external_dependency_at(prefix, name, ext_files, vars, dirs, err_files, cache) + if ok then + return true + end + end + + return nil, err_dirname, err_testfile, err_files +end + +function deps.autodetect_external_dependencies(build) + -- only applies to the 'builtin' build type + if not build or not build.modules then + return nil + end + + local extdeps = {} + local any = false + for _, data in pairs(build.modules) do + if type(data) == "table" and data.libraries then + local libraries = data.libraries + if type(libraries) == "string" then + libraries = { libraries } + end + local incdirs = {} + local libdirs = {} + for _, lib in ipairs(libraries) do + local upper = lib:upper():gsub("%+", "P"):gsub("[^%w]", "_") + any = true + extdeps[upper] = { library = lib } + table.insert(incdirs, "$(" .. upper .. "_INCDIR)") + table.insert(libdirs, "$(" .. upper .. "_LIBDIR)") + end + if not data.incdirs then + data.incdirs = incdirs + end + if not data.libdirs then + data.libdirs = libdirs + end + end + end + return any and extdeps or nil +end + +--- Set up path-related variables for external dependencies. +-- For each key in the external_dependencies table in the +-- rockspec file, four variables are created: _DIR, _BINDIR, +-- _INCDIR and _LIBDIR. These are not overwritten +-- if already set (e.g. by the LuaRocks config file or through the +-- command-line). Values in the external_dependencies table +-- are tables that may contain a "header" or a "library" field, +-- with filenames to be tested for existence. +-- @param rockspec table: The rockspec table. +-- @param mode string: if "build" is given, checks all files; +-- if "install" is given, do not scan for headers. +-- @return boolean or (nil, string): True if no errors occurred, or +-- nil and an error message if any test failed. +function deps.check_external_deps(rockspec, mode) + assert(rockspec:type() == "rockspec") + + if not rockspec.external_dependencies then + rockspec.external_dependencies = deps.autodetect_external_dependencies(rockspec.build) + end + if not rockspec.external_dependencies then + return true + end + + for name, ext_files in util.sortedpairs(rockspec.external_dependencies) do + local ok, err_dirname, err_testfile, err_files = check_external_dependency(name, ext_files, rockspec.variables, mode) + if not ok then + local lines = {"Could not find "..err_testfile.." file for "..name} + + local err_paths = {} + for _, err_file in ipairs(err_files[err_testfile]) do + if not err_paths[err_file] then + err_paths[err_file] = true + table.insert(lines, " No file "..err_file) + end + end + + table.insert(lines, "You may have to install "..name.." in your system and/or pass "..name.."_DIR or "..name.."_"..err_dirname.." to the luarocks command.") + table.insert(lines, "Example: luarocks install "..rockspec.name.." "..name.."_DIR=/usr/local") + + return nil, table.concat(lines, "\n"), "dependency" + end + end + return true +end + +--- Recursively add satisfied dependencies of a package to a table, +-- to build a transitive closure of all dependent packages. +-- Additionally ensures that `dependencies` table of the manifest is up-to-date. +-- @param results table: The results table being built, maps package names to versions. +-- @param mdeps table: The manifest dependencies table. +-- @param name string: Package name. +-- @param version string: Package version. +function deps.scan_deps(results, mdeps, name, version, deps_mode) + assert(type(results) == "table") + assert(type(mdeps) == "table") + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local fetch = require("luarocks.fetch") + + if results[name] then + return + end + if not mdeps[name] then mdeps[name] = {} end + local mdn = mdeps[name] + local dependencies = mdn[version] + local rocks_provided + if not dependencies then + local rockspec, err = fetch.load_local_rockspec(path.rockspec_file(name, version), false) + if not rockspec then + return + end + dependencies = rockspec.dependencies + rocks_provided = rockspec.rocks_provided + mdn[version] = dependencies + else + rocks_provided = util.get_rocks_provided() + end + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies") + + local matched = match_all_deps(dependencies, get_versions) + results[name] = version + for _, match in pairs(matched) do + deps.scan_deps(results, mdeps, match.name, match.version, deps_mode) + end +end + +local function lua_h_exists(d, luaver) + local major, minor = luaver:match("(%d+)%.(%d+)") + local luanum = ("%s%02d"):format(major, tonumber(minor)) + + local lua_h = dir.path(d, "lua.h") + local fd = io.open(lua_h) + if fd then + local data = fd:read("*a") + fd:close() + if data:match("LUA_VERSION_NUM%s*" .. tostring(luanum)) then + return d + end + return nil, "Lua header lua.h found at " .. d .. " does not match Lua version " .. luaver .. ". You can use `luarocks config variables.LUA_INCDIR ` to set the correct location.", "dependency", 2 + end + + return nil, "Failed finding Lua header lua.h (searched at " .. d .. "). You may need to install Lua development headers. You can use `luarocks config variables.LUA_INCDIR ` to set the correct location.", "dependency", 1 +end + +local function find_lua_incdir(prefix, luaver, luajitver) + luajitver = luajitver and luajitver:gsub("%-.*", "") + local shortv = luaver:gsub("%.", "") + local incdirs = { + prefix .. "/include/lua/" .. luaver, + prefix .. "/include/lua" .. luaver, + prefix .. "/include/lua-" .. luaver, + prefix .. "/include/lua" .. shortv, + prefix .. "/include", + prefix, + luajitver and (prefix .. "/include/luajit-" .. (luajitver:match("^(%d+%.%d+)") or "")), + } + local errprio = 0 + local mainerr + for _, d in ipairs(incdirs) do + local ok, err, _, prio = lua_h_exists(d, luaver) + if ok then + return d + end + if prio > errprio then + mainerr = err + errprio = prio + end + end + + -- not found, will fallback to a default + return nil, mainerr +end + +function deps.check_lua_incdir(vars) + if vars.LUA_INCDIR_OK == true + then return true + end + + local ljv = util.get_luajit_version() + + if vars.LUA_INCDIR then + local ok, err = lua_h_exists(vars.LUA_INCDIR, cfg.lua_version) + if ok then + vars.LUA_INCDIR_OK = true + end + return ok, err + end + + if vars.LUA_DIR then + local d, err = find_lua_incdir(vars.LUA_DIR, cfg.lua_version, ljv) + if d then + vars.LUA_INCDIR = d + vars.LUA_INCDIR_OK = true + return true + end + return nil, err + end + + return nil, "Failed finding Lua headers; neither LUA_DIR or LUA_INCDIR are set. You may need to install them or configure LUA_INCDIR.", "dependency" +end + +function deps.check_lua_libdir(vars) + if vars.LUA_LIBDIR_OK == true + then return true + end + + local fs = require("luarocks.fs") + local ljv = util.get_luajit_version() + + if vars.LUA_LIBDIR and vars.LUALIB and fs.exists(dir.path(vars.LUA_LIBDIR, vars.LUALIB)) then + vars.LUA_LIBDIR_OK = true + return true + end + + local shortv = cfg.lua_version:gsub("%.", "") + local libnames = { + "lua" .. cfg.lua_version, + "lua" .. shortv, + "lua-" .. cfg.lua_version, + "lua-" .. shortv, + "lua", + } + if ljv then + table.insert(libnames, 1, "luajit-" .. cfg.lua_version) + table.insert(libnames, 2, "luajit") + end + local cache = {} + local save_LUA_INCDIR = vars.LUA_INCDIR + local ok, _, _, errfiles = check_external_dependency("LUA", { library = libnames }, vars, "build", cache) + vars.LUA_INCDIR = save_LUA_INCDIR + local err + if ok then + local filename = dir.path(vars.LUA_LIBDIR, vars.LUA_LIBDIR_FILE) + local fd = io.open(filename, "r") + if fd then + if not vars.LUA_LIBDIR_FILE:match((cfg.lua_version:gsub("%.", "%%.?"))) then + -- if filename isn't versioned, check file contents + local txt = fd:read("*a") + ok = txt:match("Lua " .. cfg.lua_version, 1, true) + or txt:match("lua" .. (cfg.lua_version:gsub("%.", "")), 1, true) + if not ok then + err = "Lua library at " .. filename .. " does not match Lua version " .. cfg.lua_version .. ". You can use `luarocks config variables.LUA_LIBDIR ` to set the correct location." + end + end + + fd:close() + end + end + + if ok then + vars.LUALIB = vars.LUA_LIBDIR_FILE + vars.LUA_LIBDIR_OK = true + return true + else + err = err or "Failed finding the Lua library. You can use `luarocks config variables.LUA_LIBDIR ` to set the correct location." + return nil, err, "dependency", errfiles + end +end + +function deps.get_deps_mode(args) + return args.deps_mode or cfg.deps_mode +end + +return deps diff --git a/src/luarocks/dir.lua b/src/luarocks/dir.lua new file mode 100644 index 0000000..be89e37 --- /dev/null +++ b/src/luarocks/dir.lua @@ -0,0 +1,63 @@ + +--- Generic utilities for handling pathnames. +local dir = {} + +local core = require("luarocks.core.dir") + +dir.path = core.path +dir.split_url = core.split_url +dir.normalize = core.normalize + +local dir_sep = package.config:sub(1, 1) + +--- Strip the path off a path+filename. +-- @param pathname string: A path+name, such as "/a/b/c" +-- or "\a\b\c". +-- @return string: The filename without its path, such as "c". +function dir.base_name(pathname) + assert(type(pathname) == "string") + + local b + b = pathname:gsub("[/\\]", "/") -- canonicalize to forward slashes + b = b:gsub("/*$", "") -- drop trailing slashes + b = b:match(".*[/\\]([^/\\]*)") -- match last component + b = b or pathname -- fallback to original if no slashes + + return b +end + +--- Strip the name off a path+filename. +-- @param pathname string: A path+name, such as "/a/b/c". +-- @return string: The filename without its path, such as "/a/b". +-- For entries such as "/a/b/", "/a" is returned. If there are +-- no directory separators in input, "" is returned. +function dir.dir_name(pathname) + assert(type(pathname) == "string") + + local d + d = pathname:gsub("[/\\]", "/") -- canonicalize to forward slashes + d = d:gsub("/*$", "") -- drop trailing slashes + d = d:match("(.*)[/]+[^/]*") -- match all components but the last + d = d or "" -- switch to "" if there's no match + d = d:gsub("/", dir_sep) -- decanonicalize to native slashes + + return d +end + +--- Returns true if protocol does not require additional tools. +-- @param protocol The protocol name +function dir.is_basic_protocol(protocol) + return protocol == "http" or protocol == "https" or protocol == "ftp" or protocol == "file" +end + +function dir.deduce_base_dir(url) + -- for extensions like foo.tar.gz, "gz" is stripped first + local known_exts = {} + for _, ext in ipairs{"zip", "git", "tgz", "tar", "gz", "bz2"} do + known_exts[ext] = "" + end + local base = dir.base_name(url) + return (base:gsub("%.([^.]*)$", known_exts):gsub("%.tar", "")) +end + +return dir diff --git a/src/luarocks/download.lua b/src/luarocks/download.lua new file mode 100644 index 0000000..07a2a65 --- /dev/null +++ b/src/luarocks/download.lua @@ -0,0 +1,68 @@ +local download = {} + +local path = require("luarocks.path") +local fetch = require("luarocks.fetch") +local search = require("luarocks.search") +local queries = require("luarocks.queries") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +local function get_file(filename) + local protocol, pathname = dir.split_url(filename) + if protocol == "file" then + local ok, err = fs.copy(pathname, fs.current_dir(), "read") + if ok then + return pathname + else + return nil, err + end + else + -- discard third result + local ok, err = fetch.fetch_url(filename) + return ok, err + end +end + +function download.download(arch, name, namespace, version, all, check_lua_versions) + local substring = (all and name == "") + local query = queries.new(name, namespace, version, substring, arch) + local search_err + + if all then + local results = search.search_repos(query) + local has_result = false + local all_ok = true + local any_err = "" + for name, result in pairs(results) do -- luacheck: ignore 422 + for version, items in pairs(result) do -- luacheck: ignore 422 + for _, item in ipairs(items) do + -- Ignore provided rocks. + if item.arch ~= "installed" then + has_result = true + local filename = path.make_url(item.repo, name, version, item.arch) + local ok, err = get_file(filename) + if not ok then + all_ok = false + any_err = any_err .. "\n" .. err + end + end + end + end + end + + if has_result then + return all_ok, any_err + end + else + local url + url, search_err = search.find_rock_checking_lua_versions(query, check_lua_versions) + if url then + return get_file(url) + end + end + local rock = util.format_rock_name(name, namespace, version) + return nil, "Could not find a result named "..rock..(search_err and ": "..search_err or ".") +end + +return download diff --git a/src/luarocks/fetch.lua b/src/luarocks/fetch.lua new file mode 100644 index 0000000..193e5e3 --- /dev/null +++ b/src/luarocks/fetch.lua @@ -0,0 +1,610 @@ + +--- Functions related to fetching and loading local and remote files. +local fetch = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local rockspecs = require("luarocks.rockspecs") +local signing = require("luarocks.signing") +local persist = require("luarocks.persist") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + + +--- Fetch a local or remote file, using a local cache directory. +-- Make a remote or local URL/pathname local, fetching the file if necessary. +-- Other "fetch" and "load" functions use this function to obtain files. +-- If a local pathname is given, it is returned as a result. +-- @param url string: a local pathname or a remote URL. +-- @param mirroring string: mirroring mode. +-- If set to "no_mirror", then rocks_servers mirror configuration is not used. +-- @return (string, nil, nil, boolean) or (nil, string, [string]): +-- in case of success: +-- * the absolute local pathname for the fetched file +-- * nil +-- * nil +-- * `true` if the file was fetched from cache +-- in case of failure: +-- * nil +-- * an error message +-- * an optional error code. +function fetch.fetch_caching(url, mirroring) + local repo_url, filename = url:match("^(.*)/([^/]+)$") + local name = repo_url:gsub("[/:]","_") + local cache_dir = dir.path(cfg.local_cache, name) + local ok = fs.make_dir(cache_dir) + + local cachefile = dir.path(cache_dir, filename) + local checkfile = cachefile .. ".check" + + if (fs.file_age(checkfile) < 10 or + cfg.aggressive_cache and (not name:match("^manifest"))) and fs.exists(cachefile) + then + return cachefile, nil, nil, true + end + + local lock, errlock + if ok then + lock, errlock = fs.lock_access(cache_dir) + end + + if not (ok and lock) then + cfg.local_cache = fs.make_temp_dir("local_cache") + if not cfg.local_cache then + return nil, "Failed creating temporary local_cache directory" + end + cache_dir = dir.path(cfg.local_cache, name) + ok = fs.make_dir(cache_dir) + if not ok then + return nil, "Failed creating temporary cache directory "..cache_dir + end + lock = fs.lock_access(cache_dir) + end + + local file, err, errcode, from_cache = fetch.fetch_url(url, cachefile, true, mirroring) + if not file then + fs.unlock_access(lock) + return nil, err or "Failed downloading "..url, errcode + end + + local fd, err = io.open(checkfile, "wb") + if err then + fs.unlock_access(lock) + return nil, err + end + fd:write("!") + fd:close() + + fs.unlock_access(lock) + return file, nil, nil, from_cache +end + +local function ensure_trailing_slash(url) + return (url:gsub("/*$", "/")) +end + +local function is_url_relative_to_rocks_servers(url, servers) + for _, item in ipairs(servers) do + if type(item) == "table" then + for i, s in ipairs(item) do + local base = ensure_trailing_slash(s) + if string.find(url, base, 1, true) == 1 then + return i, url:sub(#base + 1), item + end + end + end + end +end + +local function download_with_mirrors(url, filename, cache, servers) + local idx, rest, mirrors = is_url_relative_to_rocks_servers(url, servers) + + if not idx then + -- URL is not from a rock server + return fs.download(url, filename, cache) + end + + -- URL is from a rock server: try to download it falling back to mirrors. + local err = "\n" + for i = idx, #mirrors do + local try_url = ensure_trailing_slash(mirrors[i]) .. rest + if i > idx then + util.warning("Failed downloading. Attempting mirror at " .. try_url) + end + local ok, name, from_cache = fs.download(try_url, filename, cache) + if ok then + return ok, name, from_cache + else + err = err .. name .. "\n" + end + end + + return nil, err, "network" +end + +--- Fetch a local or remote file. +-- Make a remote or local URL/pathname local, fetching the file if necessary. +-- Other "fetch" and "load" functions use this function to obtain files. +-- If a local pathname is given, it is returned as a result. +-- @param url string: a local pathname or a remote URL. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to +-- re-downloading the file. +-- @param mirroring string: mirroring mode. +-- If set to "no_mirror", then rocks_servers mirror configuration is not used. +-- @return (string, nil, nil, boolean) or (nil, string, [string]): +-- in case of success: +-- * the absolute local pathname for the fetched file +-- * nil +-- * nil +-- * `true` if the file was fetched from cache +-- in case of failure: +-- * nil +-- * an error message +-- * an optional error code. +function fetch.fetch_url(url, filename, cache, mirroring) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + local protocol, pathname = dir.split_url(url) + if protocol == "file" then + local fullname = fs.absolute_name(pathname) + if not fs.exists(fullname) then + local hint = (not pathname:match("^/")) + and (" - note that given path in rockspec is not absolute: " .. url) + or "" + return nil, "Local file not found: " .. fullname .. hint + end + filename = filename or dir.base_name(pathname) + local dstname = fs.absolute_name(dir.path(".", filename)) + local ok, err + if fullname == dstname then + ok = true + else + ok, err = fs.copy(fullname, dstname) + end + if ok then + return dstname + else + return nil, "Failed copying local file " .. fullname .. " to " .. dstname .. ": " .. err + end + elseif dir.is_basic_protocol(protocol) then + local ok, name, from_cache + if mirroring ~= "no_mirror" then + ok, name, from_cache = download_with_mirrors(url, filename, cache, cfg.rocks_servers) + else + ok, name, from_cache = fs.download(url, filename, cache) + end + if not ok then + return nil, "Failed downloading "..url..(name and " - "..name or ""), from_cache + end + return name, nil, nil, from_cache + else + return nil, "Unsupported protocol "..protocol + end +end + +--- For remote URLs, create a temporary directory and download URL inside it. +-- This temporary directory will be deleted on program termination. +-- For local URLs, just return the local pathname and its directory. +-- @param url string: URL to be downloaded +-- @param tmpname string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @param filename string or nil: local filename of URL to be downloaded, +-- in case it can't be inferred from the URL. +-- @return (string, string) or (nil, string, [string]): absolute local pathname of +-- the fetched file and temporary directory name; or nil and an error message +-- followed by an optional error code +function fetch.fetch_url_at_temp_dir(url, tmpname, filename, cache) + assert(type(url) == "string") + assert(type(tmpname) == "string") + assert(type(filename) == "string" or not filename) + filename = filename or dir.base_name(url) + + local protocol, pathname = dir.split_url(url) + if protocol == "file" then + if fs.exists(pathname) then + return pathname, dir.dir_name(fs.absolute_name(pathname)) + else + return nil, "File not found: "..pathname + end + else + local temp_dir, err = fs.make_temp_dir(tmpname) + if not temp_dir then + return nil, "Failed creating temporary directory "..tmpname..": "..err + end + util.schedule_function(fs.delete, temp_dir) + local ok, err = fs.change_dir(temp_dir) + if not ok then return nil, err end + + local file, err, errcode + + if cache then + local cachefile + cachefile, err, errcode = fetch.fetch_caching(url) + + if cachefile then + file = dir.path(temp_dir, filename) + fs.copy(cachefile, file) + end + end + + if not file then + file, err, errcode = fetch.fetch_url(url, filename, cache) + end + + fs.pop_dir() + if not file then + return nil, "Error fetching file: "..err, errcode + end + + return file, temp_dir + end +end + +-- Determine base directory of a fetched URL by extracting its +-- archive and looking for a directory in the root. +-- @param file string: absolute local pathname of the fetched file +-- @param temp_dir string: temporary directory in which URL was fetched. +-- @param src_url string: URL to use when inferring base directory. +-- @param src_dir string or nil: expected base directory (inferred +-- from src_url if not given). +-- @return (string, string) or (string, nil) or (nil, string): +-- The inferred base directory and the one actually found (which may +-- be nil if not found), or nil followed by an error message. +-- The inferred dir is returned first to avoid confusion with errors, +-- because it is never nil. +function fetch.find_base_dir(file, temp_dir, src_url, src_dir) + local ok, err = fs.change_dir(temp_dir) + if not ok then return nil, err end + fs.unpack_archive(file) + + if not src_dir then + local rockspec = { + source = { + file = file, + dir = src_dir, + url = src_url, + } + } + ok, err = fetch.find_rockspec_source_dir(rockspec, ".") + if ok then + src_dir = rockspec.source.dir + end + end + + local inferred_dir = src_dir or dir.deduce_base_dir(src_url) + local found_dir = nil + if fs.exists(inferred_dir) then + found_dir = inferred_dir + else + util.printerr("Directory "..inferred_dir.." not found") + local files = fs.list_dir() + if files then + table.sort(files) + for i,filename in ipairs(files) do + if fs.is_dir(filename) then + util.printerr("Found "..filename) + found_dir = filename + break + end + end + end + end + fs.pop_dir() + return inferred_dir, found_dir +end + +local function fetch_and_verify_signature_for(url, filename, tmpdir) + local sig_url = signing.signature_url(url) + local sig_file, err, errcode = fetch.fetch_url_at_temp_dir(sig_url, tmpdir) + if not sig_file then + return nil, "Could not fetch signature file for verification: " .. err, errcode + end + + local ok, err = signing.verify_signature(filename, sig_file) + if not ok then + return nil, "Failed signature verification: " .. err + end + + return fs.absolute_name(sig_file) +end + +--- Obtain a rock and unpack it. +-- If a directory is not given, a temporary directory will be created, +-- which will be deleted on program termination. +-- @param rock_file string: URL or filename of the rock. +-- @param dest string or nil: if given, directory will be used as +-- a permanent destination. +-- @param verify boolean: if true, download and verify signature for rockspec +-- @return string or (nil, string, [string]): the directory containing the contents +-- of the unpacked rock. +function fetch.fetch_and_unpack_rock(url, dest, verify) + assert(type(url) == "string") + assert(type(dest) == "string" or not dest) + + local name = dir.base_name(url):match("(.*)%.[^.]*%.rock") + local tmpname = "luarocks-rock-" .. name + + local rock_file, err, errcode = fetch.fetch_url_at_temp_dir(url, tmpname, nil, true) + if not rock_file then + return nil, "Could not fetch rock file: " .. err, errcode + end + + local sig_file + if verify then + sig_file, err = fetch_and_verify_signature_for(url, rock_file, tmpname) + if err then + return nil, err + end + end + + rock_file = fs.absolute_name(rock_file) + + local unpack_dir + if dest then + unpack_dir = dest + local ok, err = fs.make_dir(unpack_dir) + if not ok then + return nil, "Failed unpacking rock file: " .. err + end + else + unpack_dir, err = fs.make_temp_dir(name) + if not unpack_dir then + return nil, "Failed creating temporary dir: " .. err + end + end + if not dest then + util.schedule_function(fs.delete, unpack_dir) + end + local ok, err = fs.change_dir(unpack_dir) + if not ok then return nil, err end + ok, err = fs.unzip(rock_file) + if not ok then + return nil, "Failed unpacking rock file: " .. rock_file .. ": " .. err + end + if sig_file then + ok, err = fs.copy(sig_file, ".") + if not ok then + return nil, "Failed copying signature file" + end + end + fs.pop_dir() + return unpack_dir +end + +--- Back-end function that actually loads the local rockspec. +-- Performs some validation and postprocessing of the rockspec contents. +-- @param rel_filename string: The local filename of the rockspec file. +-- @param quick boolean: if true, skips some steps when loading +-- rockspec. +-- @return table or (nil, string): A table representing the rockspec +-- or nil followed by an error message. +function fetch.load_local_rockspec(rel_filename, quick) + assert(type(rel_filename) == "string") + local abs_filename = fs.absolute_name(rel_filename) + + local basename = dir.base_name(abs_filename) + if basename ~= "rockspec" then + if not basename:match("(.*)%-[^-]*%-[0-9]*") then + return nil, "Expected filename in format 'name-version-revision.rockspec'." + end + end + + local tbl, err = persist.load_into_table(abs_filename) + if not tbl then + return nil, "Could not load rockspec file "..abs_filename.." ("..err..")" + end + + local rockspec, err = rockspecs.from_persisted_table(abs_filename, tbl, err, quick) + if not rockspec then + return nil, abs_filename .. ": " .. err + end + + local name_version = rockspec.package:lower() .. "-" .. rockspec.version + if basename ~= "rockspec" and basename ~= name_version .. ".rockspec" then + return nil, "Inconsistency between rockspec filename ("..basename..") and its contents ("..name_version..".rockspec)." + end + + return rockspec +end + +--- Load a local or remote rockspec into a table. +-- This is the entry point for the LuaRocks tools. +-- Only the LuaRocks runtime loader should use +-- load_local_rockspec directly. +-- @param filename string: Local or remote filename of a rockspec. +-- @param location string or nil: Where to download. If not given, +-- a temporary dir is created. +-- @param verify boolean: if true, download and verify signature for rockspec +-- @return table or (nil, string, [string]): A table representing the rockspec +-- or nil followed by an error message and optional error code. +function fetch.load_rockspec(url, location, verify) + assert(type(url) == "string") + + local name + local basename = dir.base_name(url) + if basename == "rockspec" then + name = "rockspec" + else + name = basename:match("(.*)%.rockspec") + if not name then + return nil, "Filename '"..url.."' does not look like a rockspec." + end + end + + local tmpname = "luarocks-rockspec-"..name + local filename, err, errcode + if location then + local ok, err = fs.change_dir(location) + if not ok then return nil, err end + filename, err = fetch.fetch_url(url) + fs.pop_dir() + else + filename, err, errcode = fetch.fetch_url_at_temp_dir(url, tmpname, nil, true) + end + if not filename then + return nil, err, errcode + end + + if verify then + local _, err = fetch_and_verify_signature_for(url, filename, tmpname) + if err then + return nil, err + end + end + + return fetch.load_local_rockspec(filename) +end + +--- Download sources for building a rock using the basic URL downloader. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Whether to extract the sources from +-- the fetched source tarball or not. +-- @param dest_dir string or nil: If set, will extract to the given directory; +-- if not given, will extract to a temporary directory. +-- @return (string, string) or (nil, string, [string]): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message and optional error code. +function fetch.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(extract) == "boolean") + assert(type(dest_dir) == "string" or not dest_dir) + + local url = rockspec.source.url + local name = rockspec.name.."-"..rockspec.version + local filename = rockspec.source.file + local source_file, store_dir + local ok, err, errcode + if dest_dir then + ok, err = fs.change_dir(dest_dir) + if not ok then return nil, err, "dest_dir" end + source_file, err, errcode = fetch.fetch_url(url, filename) + fs.pop_dir() + store_dir = dest_dir + else + source_file, store_dir, errcode = fetch.fetch_url_at_temp_dir(url, "luarocks-source-"..name, filename) + end + if not source_file then + return nil, err or store_dir, errcode + end + if rockspec.source.md5 then + if not fs.check_md5(source_file, rockspec.source.md5) then + return nil, "MD5 check for "..filename.." has failed.", "md5" + end + end + if extract then + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + ok, err = fs.unpack_archive(rockspec.source.file) + if not ok then return nil, err end + ok, err = fetch.find_rockspec_source_dir(rockspec, ".") + if not ok then return nil, err end + fs.pop_dir() + end + return source_file, store_dir +end + +function fetch.find_rockspec_source_dir(rockspec, store_dir) + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + + local file_count, dir_count, found_dir = 0, 0, 0 + + if rockspec.source.dir and fs.exists(rockspec.source.dir) then + ok, err = true, nil + elseif rockspec.source.file and rockspec.source.dir then + ok, err = nil, "Directory "..rockspec.source.dir.." not found inside archive "..rockspec.source.file + elseif not rockspec.source.dir_set then -- and rockspec:format_is_at_least("3.0") then + + local name = dir.base_name(rockspec.source.file or rockspec.source.url or "") + + if name:match("%.lua$") or name:match("%.c$") then + if fs.is_file(name) then + rockspec.source.dir = "." + ok, err = true, nil + end + end + + if not rockspec.source.dir then + for file in fs.dir() do + file_count = file_count + 1 + if fs.is_dir(file) then + dir_count = dir_count + 1 + found_dir = file + end + end + + if dir_count == 1 then + rockspec.source.dir = found_dir + ok, err = true, nil + else + ok, err = nil, "Could not determine source directory from rock contents (" .. tostring(file_count).." file(s), "..tostring(dir_count).." dir(s))" + end + end + else + ok, err = nil, "Could not determine source directory, please set source.dir in rockspec." + end + + fs.pop_dir() + + assert(rockspec.source.dir or not ok) + return ok, err +end + +--- Download sources for building a rock, calling the appropriate protocol method. +-- @param rockspec table: The rockspec table +-- @param extract boolean: When downloading compressed formats, whether to extract +-- the sources from the fetched archive or not. +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- if not given, will extract to a temporary directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function fetch.fetch_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(extract) == "boolean") + assert(type(dest_dir) == "string" or not dest_dir) + + -- auto-convert git://github.com URLs to use git+https + -- see https://github.blog/2021-09-01-improving-git-protocol-security-github/ + if rockspec.source.url:match("^git://github%.com/") + or rockspec.source.url:match("^git://www%.github%.com/") then + rockspec.source.url = rockspec.source.url:gsub("^git://", "git+https://") + rockspec.source.protocol = "git+https" + end + + local protocol = rockspec.source.protocol + local ok, err, proto + if dir.is_basic_protocol(protocol) then + proto = fetch + else + ok, proto = pcall(require, "luarocks.fetch."..protocol:gsub("[+-]", "_")) + if not ok then + return nil, "Unknown protocol "..protocol + end + end + + if cfg.only_sources_from + and rockspec.source.pathname + and #rockspec.source.pathname > 0 then + if #cfg.only_sources_from == 0 then + return nil, "Can't download "..rockspec.source.url.." -- download from remote servers disabled" + elseif rockspec.source.pathname:find(cfg.only_sources_from, 1, true) ~= 1 then + return nil, "Can't download "..rockspec.source.url.." -- only downloading from "..cfg.only_sources_from + end + end + + local source_file, store_dir = proto.get_sources(rockspec, extract, dest_dir) + if not source_file then return nil, store_dir end + + ok, err = fetch.find_rockspec_source_dir(rockspec, store_dir) + if not ok then return nil, err, "source.dir", source_file, store_dir end + + return source_file, store_dir +end + +return fetch diff --git a/src/luarocks/fetch/cvs.lua b/src/luarocks/fetch/cvs.lua new file mode 100644 index 0000000..d78e6e6 --- /dev/null +++ b/src/luarocks/fetch/cvs.lua @@ -0,0 +1,55 @@ + +--- Fetch back-end for retrieving sources from CVS. +local cvs = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +--- Download sources for building a rock, using CVS. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function cvs.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local cvs_cmd = rockspec.variables.CVS + local ok, err_msg = fs.is_tool_available(cvs_cmd, "CVS") + if not ok then + return nil, err_msg + end + + local name_version = rockspec.name .. "-" .. rockspec.version + local module = rockspec.source.module or dir.base_name(rockspec.source.url) + local command = {cvs_cmd, "-d"..rockspec.source.pathname, "export", module} + if rockspec.source.tag then + table.insert(command, 4, "-r") + table.insert(command, 5, rockspec.source.tag) + end + local store_dir + if not dest_dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, store_dir) + else + store_dir = dest_dir + end + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + if not fs.execute(unpack(command)) then + return nil, "Failed fetching files from CVS." + end + fs.pop_dir() + return module, store_dir +end + + +return cvs diff --git a/src/luarocks/fetch/git.lua b/src/luarocks/fetch/git.lua new file mode 100644 index 0000000..29892e9 --- /dev/null +++ b/src/luarocks/fetch/git.lua @@ -0,0 +1,165 @@ + +--- Fetch back-end for retrieving sources from GIT. +local git = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local vers = require("luarocks.core.vers") +local util = require("luarocks.util") + +local cached_git_version + +--- Get git version. +-- @param git_cmd string: name of git command. +-- @return table: git version as returned by luarocks.core.vers.parse_version. +local function git_version(git_cmd) + if not cached_git_version then + local version_line = io.popen(fs.Q(git_cmd)..' --version'):read() + local version_string = version_line:match('%d-%.%d+%.?%d*') + cached_git_version = vers.parse_version(version_string) + end + + return cached_git_version +end + +--- Check if git satisfies version requirement. +-- @param git_cmd string: name of git command. +-- @param version string: required version. +-- @return boolean: true if git matches version or is newer, false otherwise. +local function git_is_at_least(git_cmd, version) + return git_version(git_cmd) >= vers.parse_version(version) +end + +--- Git >= 1.7.10 can clone a branch **or tag**, < 1.7.10 by branch only. We +-- need to know this in order to build the appropriate command; if we can't +-- clone by tag then we'll have to issue a subsequent command to check out the +-- given tag. +-- @param git_cmd string: name of git command. +-- @return boolean: Whether Git can clone by tag. +local function git_can_clone_by_tag(git_cmd) + return git_is_at_least(git_cmd, "1.7.10") +end + +--- Git >= 1.8.4 can fetch submodules shallowly, saving bandwidth and time for +-- submodules with large history. +-- @param git_cmd string: name of git command. +-- @return boolean: Whether Git can fetch submodules shallowly. +local function git_supports_shallow_submodules(git_cmd) + return git_is_at_least(git_cmd, "1.8.4") +end + +--- Git >= 2.10 can fetch submodules shallowly according to .gitmodules configuration, allowing more granularity. +-- @param git_cmd string: name of git command. +-- @return boolean: Whether Git can fetch submodules shallowly according to .gitmodules. +local function git_supports_shallow_recommendations(git_cmd) + return git_is_at_least(git_cmd, "2.10.0") +end + +local function git_identifier(git_cmd, ver) + if not (ver:match("^dev%-%d+$") or ver:match("^scm%-%d+$")) then + return nil + end + local pd = io.popen(fs.command_at(fs.current_dir(), fs.Q(git_cmd).." log --pretty=format:%ai_%h -n 1")) + if not pd then + return nil + end + local date_hash = pd:read("*l") + pd:close() + if not date_hash then + return nil + end + local date, time, tz, hash = date_hash:match("([^%s]+) ([^%s]+) ([^%s]+)_([^%s]+)") + date = date:gsub("%-", "") + time = time:gsub(":", "") + return date .. "." .. time .. "." .. hash +end + +--- Download sources for building a rock, using git. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function git.get_sources(rockspec, extract, dest_dir, depth) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local git_cmd = rockspec.variables.GIT + local name_version = rockspec.name .. "-" .. rockspec.version + local module = dir.base_name(rockspec.source.url) + -- Strip off .git from base name if present + module = module:gsub("%.git$", "") + + local ok, err_msg = fs.is_tool_available(git_cmd, "Git") + if not ok then + return nil, err_msg + end + + local store_dir + if not dest_dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, store_dir) + else + store_dir = dest_dir + end + store_dir = fs.absolute_name(store_dir) + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + + local command = {fs.Q(git_cmd), "clone", depth or "--depth=1", rockspec.source.url, module} + local tag_or_branch = rockspec.source.tag or rockspec.source.branch + -- If the tag or branch is explicitly set to "master" in the rockspec, then + -- we can avoid passing it to Git since it's the default. + if tag_or_branch == "master" then tag_or_branch = nil end + if tag_or_branch then + if git_can_clone_by_tag(git_cmd) then + -- The argument to `--branch` can actually be a branch or a tag as of + -- Git 1.7.10. + table.insert(command, 3, "--branch=" .. tag_or_branch) + end + end + if not fs.execute(unpack(command)) then + return nil, "Failed cloning git repository." + end + ok, err = fs.change_dir(module) + if not ok then return nil, err end + if tag_or_branch and not git_can_clone_by_tag() then + if not fs.execute(fs.Q(git_cmd), "checkout", tag_or_branch) then + return nil, 'Failed to check out the "' .. tag_or_branch ..'" tag or branch.' + end + end + + -- Fetching git submodules is supported only when rockspec format is >= 3.0. + if rockspec:format_is_at_least("3.0") then + command = {fs.Q(git_cmd), "submodule", "update", "--init", "--recursive"} + + if git_supports_shallow_recommendations(git_cmd) then + table.insert(command, 5, "--recommend-shallow") + elseif git_supports_shallow_submodules(git_cmd) then + -- Fetch only the last commit of each submodule. + table.insert(command, 5, "--depth=1") + end + + if not fs.execute(unpack(command)) then + return nil, 'Failed to fetch submodules.' + end + end + + if not rockspec.source.tag then + rockspec.source.identifier = git_identifier(git_cmd, rockspec.version) + end + + fs.delete(dir.path(store_dir, module, ".git")) + fs.delete(dir.path(store_dir, module, ".gitignore")) + fs.pop_dir() + fs.pop_dir() + return module, store_dir +end + +return git diff --git a/src/luarocks/fetch/git_file.lua b/src/luarocks/fetch/git_file.lua new file mode 100644 index 0000000..8d46bbc --- /dev/null +++ b/src/luarocks/fetch/git_file.lua @@ -0,0 +1,19 @@ + +--- Fetch back-end for retrieving sources from local Git repositories. +local git_file = {} + +local git = require("luarocks.fetch.git") + +--- Fetch sources for building a rock from a local Git repository. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function git_file.get_sources(rockspec, extract, dest_dir) + rockspec.source.url = rockspec.source.url:gsub("^git.file://", "") + return git.get_sources(rockspec, extract, dest_dir) +end + +return git_file diff --git a/src/luarocks/fetch/git_http.lua b/src/luarocks/fetch/git_http.lua new file mode 100644 index 0000000..d85e257 --- /dev/null +++ b/src/luarocks/fetch/git_http.lua @@ -0,0 +1,26 @@ + +--- Fetch back-end for retrieving sources from Git repositories +-- that use http:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `git clone http://example.com/foo.git` +-- you can use this in the rockspec: +-- source = { url = "git+http://example.com/foo.git" } +-- Prefer using the normal git:// fetch mode as it is more widely +-- available in older versions of LuaRocks. +local git_http = {} + +local git = require("luarocks.fetch.git") + +--- Fetch sources for building a rock from a local Git repository. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function git_http.get_sources(rockspec, extract, dest_dir) + rockspec.source.url = rockspec.source.url:gsub("^git.", "") + return git.get_sources(rockspec, extract, dest_dir, "--") +end + +return git_http diff --git a/src/luarocks/fetch/git_https.lua b/src/luarocks/fetch/git_https.lua new file mode 100644 index 0000000..67f8ad6 --- /dev/null +++ b/src/luarocks/fetch/git_https.lua @@ -0,0 +1,7 @@ +--- Fetch back-end for retrieving sources from Git repositories +-- that use https:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `git clone https://example.com/foo.git` +-- you can use this in the rockspec: +-- source = { url = "git+https://example.com/foo.git" } +return require "luarocks.fetch.git_http" diff --git a/src/luarocks/fetch/git_ssh.lua b/src/luarocks/fetch/git_ssh.lua new file mode 100644 index 0000000..0c2c075 --- /dev/null +++ b/src/luarocks/fetch/git_ssh.lua @@ -0,0 +1,32 @@ +--- Fetch back-end for retrieving sources from Git repositories +-- that use ssh:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `git clone ssh://git@example.com/path/foo.git +-- you can use this in the rockspec: +-- source = { url = "git+ssh://git@example.com/path/foo.git" } +-- It also handles scp-style ssh urls: git@example.com:path/foo.git, +-- but you have to prepend the "git+ssh://" and why not use the "newer" +-- style anyway? +local git_ssh = {} + +local git = require("luarocks.fetch.git") + +--- Fetch sources for building a rock from a local Git repository. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function git_ssh.get_sources(rockspec, extract, dest_dir) + rockspec.source.url = rockspec.source.url:gsub("^git.", "") + + -- Handle old-style scp-like git ssh urls + if rockspec.source.url:match("^ssh://[^/]+:[^%d]") then + rockspec.source.url = rockspec.source.url:gsub("^ssh://", "") + end + + return git.get_sources(rockspec, extract, dest_dir, "--") +end + +return git_ssh diff --git a/src/luarocks/fetch/hg.lua b/src/luarocks/fetch/hg.lua new file mode 100644 index 0000000..0ef0f5e --- /dev/null +++ b/src/luarocks/fetch/hg.lua @@ -0,0 +1,65 @@ + +--- Fetch back-end for retrieving sources from HG. +local hg = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +--- Download sources for building a rock, using hg. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function hg.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local hg_cmd = rockspec.variables.HG + local ok, err_msg = fs.is_tool_available(hg_cmd, "Mercurial") + if not ok then + return nil, err_msg + end + + local name_version = rockspec.name .. "-" .. rockspec.version + -- Strip off special hg:// protocol type + local url = rockspec.source.url:gsub("^hg://", "") + + local module = dir.base_name(url) + + local command = {hg_cmd, "clone", url, module} + local tag_or_branch = rockspec.source.tag or rockspec.source.branch + if tag_or_branch then + command = {hg_cmd, "clone", "--rev", tag_or_branch, url, module} + end + local store_dir + if not dest_dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, store_dir) + else + store_dir = dest_dir + end + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + if not fs.execute(unpack(command)) then + return nil, "Failed cloning hg repository." + end + ok, err = fs.change_dir(module) + if not ok then return nil, err end + + fs.delete(dir.path(store_dir, module, ".hg")) + fs.delete(dir.path(store_dir, module, ".hgignore")) + fs.pop_dir() + fs.pop_dir() + return module, store_dir +end + + +return hg diff --git a/src/luarocks/fetch/hg_http.lua b/src/luarocks/fetch/hg_http.lua new file mode 100644 index 0000000..8f506da --- /dev/null +++ b/src/luarocks/fetch/hg_http.lua @@ -0,0 +1,24 @@ + +--- Fetch back-end for retrieving sources from hg repositories +-- that use http:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `hg clone http://example.com/foo` +-- you can use this in the rockspec: +-- source = { url = "hg+http://example.com/foo" } +local hg_http = {} + +local hg = require("luarocks.fetch.hg") + +--- Download sources for building a rock, using hg over http. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function hg_http.get_sources(rockspec, extract, dest_dir) + rockspec.source.url = rockspec.source.url:gsub("^hg.", "") + return hg.get_sources(rockspec, extract, dest_dir) +end + +return hg_http diff --git a/src/luarocks/fetch/hg_https.lua b/src/luarocks/fetch/hg_https.lua new file mode 100644 index 0000000..e67417f --- /dev/null +++ b/src/luarocks/fetch/hg_https.lua @@ -0,0 +1,8 @@ + +--- Fetch back-end for retrieving sources from hg repositories +-- that use https:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `hg clone https://example.com/foo` +-- you can use this in the rockspec: +-- source = { url = "hg+https://example.com/foo" } +return require "luarocks.fetch.hg_http" diff --git a/src/luarocks/fetch/hg_ssh.lua b/src/luarocks/fetch/hg_ssh.lua new file mode 100644 index 0000000..0c365fa --- /dev/null +++ b/src/luarocks/fetch/hg_ssh.lua @@ -0,0 +1,8 @@ + +--- Fetch back-end for retrieving sources from hg repositories +-- that use ssh:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `hg clone ssh://example.com/foo` +-- you can use this in the rockspec: +-- source = { url = "hg+ssh://example.com/foo" } +return require "luarocks.fetch.hg_http" diff --git a/src/luarocks/fetch/sscm.lua b/src/luarocks/fetch/sscm.lua new file mode 100644 index 0000000..32bb2ec --- /dev/null +++ b/src/luarocks/fetch/sscm.lua @@ -0,0 +1,44 @@ + +--- Fetch back-end for retrieving sources from Surround SCM Server +local sscm = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") + +--- Download sources via Surround SCM Server for building a rock. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function sscm.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local sscm_cmd = rockspec.variables.SSCM + local module = rockspec.source.module or dir.base_name(rockspec.source.url) + local branch, repository = string.match(rockspec.source.pathname, "^([^/]*)/(.*)") + if not branch or not repository then + return nil, "Error retrieving branch and repository from rockspec." + end + -- Search for working directory. + local working_dir + local tmp = io.popen(string.format(sscm_cmd..[[ property "/" -d -b%s -p%s]], branch, repository)) + for line in tmp:lines() do + --%c because a chr(13) comes in the end. + working_dir = string.match(line, "Working directory:[%s]*(.*)%c$") + if working_dir then break end + end + tmp:close() + if not working_dir then + return nil, "Error retrieving working directory from SSCM." + end + if not fs.execute(sscm_cmd, "get", "*", "-e" , "-r", "-b"..branch, "-p"..repository, "-tmodify", "-wreplace") then + return nil, "Failed fetching files from SSCM." + end + -- FIXME: This function does not honor the dest_dir parameter. + return module, working_dir +end + +return sscm diff --git a/src/luarocks/fetch/svn.lua b/src/luarocks/fetch/svn.lua new file mode 100644 index 0000000..b6618af --- /dev/null +++ b/src/luarocks/fetch/svn.lua @@ -0,0 +1,64 @@ + +--- Fetch back-end for retrieving sources from Subversion. +local svn = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +--- Download sources for building a rock, using Subversion. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function svn.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local svn_cmd = rockspec.variables.SVN + local ok, err_msg = fs.is_tool_available(svn_cmd, "Subversion") + if not ok then + return nil, err_msg + end + + local name_version = rockspec.name .. "-" .. rockspec.version + local module = rockspec.source.module or dir.base_name(rockspec.source.url) + local url = rockspec.source.url:gsub("^svn://", "") + local command = {svn_cmd, "checkout", url, module} + if rockspec.source.tag then + table.insert(command, 5, "-r") + table.insert(command, 6, rockspec.source.tag) + end + local store_dir + if not dest_dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, store_dir) + else + store_dir = dest_dir + end + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + if not fs.execute(unpack(command)) then + return nil, "Failed fetching files from Subversion." + end + ok, err = fs.change_dir(module) + if not ok then return nil, err end + for _, d in ipairs(fs.find(".")) do + if dir.base_name(d) == ".svn" then + fs.delete(dir.path(store_dir, module, d)) + end + end + fs.pop_dir() + fs.pop_dir() + return module, store_dir +end + + +return svn diff --git a/src/luarocks/fs.lua b/src/luarocks/fs.lua new file mode 100644 index 0000000..a8156a2 --- /dev/null +++ b/src/luarocks/fs.lua @@ -0,0 +1,148 @@ + +--- Proxy module for filesystem and platform abstractions. +-- All code using "fs" code should require "luarocks.fs", +-- and not the various platform-specific implementations. +-- However, see the documentation of the implementation +-- for the API reference. + +local pairs = pairs + +local fs = {} +-- To avoid a loop when loading the other fs modules. +package.loaded["luarocks.fs"] = fs + +local cfg = require("luarocks.core.cfg") + +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +math.randomseed(os.time()) + +local fs_is_verbose = false + +do + local old_popen, old_execute + + -- patch io.popen and os.execute to display commands in verbose mode + function fs.verbose() + fs_is_verbose = true + + if old_popen or old_execute then return end + old_popen = io.popen + -- luacheck: push globals io os + io.popen = function(one, two) + if two == nil then + print("\nio.popen: ", one) + else + print("\nio.popen: ", one, "Mode:", two) + end + return old_popen(one, two) + end + + old_execute = os.execute + os.execute = function(cmd) + -- redact api keys if present + print("\nos.execute: ", (cmd:gsub("(/api/[^/]+/)([^/]+)/", function(cap, key) return cap.."/" end)) ) + local a, b, c = old_execute(cmd) + if type(a) == "boolean" then + print((a and ".........." or "##########") .. ": " .. tostring(c) .. (b == "exit" and "" or " (" .. tostring(b) .. ")")) + elseif type(a) == "number" then + print(((a == 0) and ".........." or "##########") .. ": " .. tostring(a)) + end + return a, b, c + end + -- luacheck: pop + end +end + +do + local skip_verbose_wrap = { + ["current_dir"] = true, + } + + local function load_fns(fs_table, inits) + for name, fn in pairs(fs_table) do + if name ~= "init" and not fs[name] then + if skip_verbose_wrap[name] then + fs[name] = fn + else + fs[name] = function(...) + if fs_is_verbose then + local args = pack(...) + for i=1, args.n do + local arg = args[i] + local pok, v = pcall(string.format, "%q", arg) + args[i] = pok and v or tostring(arg) + end + print("fs." .. name .. "(" .. table.concat(args, ", ") .. ")") + end + return fn(...) + end + end + end + end + if fs_table.init then + table.insert(inits, fs_table.init) + end + end + + local function load_platform_fns(patt, inits) + local each_platform = cfg.each_platform + + -- FIXME A quick hack for the experimental Windows build + if os.getenv("LUAROCKS_CROSS_COMPILING") then + each_platform = function() + local i = 0 + local plats = { "linux", "unix" } + return function() + i = i + 1 + return plats[i] + end + end + end + + for platform in each_platform("most-specific-first") do + local ok, fs_plat = pcall(require, patt:format(platform)) + if ok and fs_plat then + load_fns(fs_plat, inits) + end + end + end + + function fs.init() + local inits = {} + + if fs.current_dir then + -- unload luarocks fs so it can be reloaded using all modules + -- providing extra functionality in the current package paths + for k, _ in pairs(fs) do + if k ~= "init" and k ~= "verbose" then + fs[k] = nil + end + end + for m, _ in pairs(package.loaded) do + if m:match("luarocks%.fs%.") then + package.loaded[m] = nil + end + end + end + + -- Load platform-specific functions + load_platform_fns("luarocks.fs.%s", inits) + + -- Load platform-independent pure-Lua functionality + load_fns(require("luarocks.fs.lua"), inits) + + -- Load platform-specific fallbacks for missing Lua modules + load_platform_fns("luarocks.fs.%s.tools", inits) + + -- Load platform-independent external tool functionality + load_fns(require("luarocks.fs.tools"), inits) + + -- Run platform-specific initializations after everything is loaded + for _, init in ipairs(inits) do + init() + end + end +end + +return fs diff --git a/src/luarocks/fs/linux.lua b/src/luarocks/fs/linux.lua new file mode 100644 index 0000000..c1b057c --- /dev/null +++ b/src/luarocks/fs/linux.lua @@ -0,0 +1,50 @@ +--- Linux-specific implementation of filesystem and platform abstractions. +local linux = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") + +function linux.is_dir(file) + file = fs.absolute_name(file) + file = dir.normalize(file) .. "/." + local fd, _, code = io.open(file, "r") + if code == 2 then -- "No such file or directory" + return false + end + if code == 20 then -- "Not a directory", regardless of permissions + return false + end + if code == 13 then -- "Permission denied", but is a directory + return true + end + if fd then + local _, _, ecode = fd:read(1) + fd:close() + if ecode == 21 then -- "Is a directory" + return true + end + end + return false +end + +function linux.is_file(file) + file = fs.absolute_name(file) + if fs.is_dir(file) then + return false + end + file = dir.normalize(file) + local fd, _, code = io.open(file, "r") + if code == 2 then -- "No such file or directory" + return false + end + if code == 13 then -- "Permission denied", but it exists + return true + end + if fd then + fd:close() + return true + end + return false +end + +return linux diff --git a/src/luarocks/fs/lua.lua b/src/luarocks/fs/lua.lua new file mode 100644 index 0000000..4016ddc --- /dev/null +++ b/src/luarocks/fs/lua.lua @@ -0,0 +1,1307 @@ + +--- Native Lua implementation of filesystem and platform abstractions, +-- using LuaFileSystem, LuaSocket, LuaSec, lua-zlib, LuaPosix, MD5. +-- module("luarocks.fs.lua") +local fs_lua = {} + +local fs = require("luarocks.fs") + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local vers = require("luarocks.core.vers") + +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +local socket_ok, zip_ok, lfs_ok, md5_ok, posix_ok, bz2_ok, _ +local http, ftp, zip, lfs, md5, posix, bz2 + +if cfg.fs_use_modules then + socket_ok, http = pcall(require, "socket.http") + _, ftp = pcall(require, "socket.ftp") + zip_ok, zip = pcall(require, "luarocks.tools.zip") + bz2_ok, bz2 = pcall(require, "bz2") + lfs_ok, lfs = pcall(require, "lfs") + md5_ok, md5 = pcall(require, "md5") + posix_ok, posix = pcall(require, "posix") +end + +local patch = require("luarocks.tools.patch") +local tar = require("luarocks.tools.tar") + +local dir_sep = package.config:sub(1, 1) + +local dir_stack = {} + +--- Test is file/dir is writable. +-- Warning: testing if a file/dir is writable does not guarantee +-- that it will remain writable and therefore it is no replacement +-- for checking the result of subsequent operations. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function fs_lua.is_writable(file) + assert(file) + file = dir.normalize(file) + local result + if fs.is_dir(file) then + local file2 = dir.path(file, '.tmpluarockstestwritable') + local fh = io.open(file2, 'wb') + result = fh ~= nil + if fh then fh:close() end + os.remove(file2) + else + local fh = io.open(file, 'r+b') + result = fh ~= nil + if fh then fh:close() end + end + return result +end + +function fs_lua.quote_args(command, ...) + local out = { command } + local args = pack(...) + for i=1, args.n do + local arg = args[i] + assert(type(arg) == "string") + out[#out+1] = fs.Q(arg) + end + return table.concat(out, " ") +end + +--- Run the given command, quoting its arguments. +-- The command is executed in the current directory in the dir stack. +-- @param command string: The command to be executed. No quoting/escaping +-- is applied. +-- @param ... Strings containing additional arguments, which are quoted. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function fs_lua.execute(command, ...) + assert(type(command) == "string") + return fs.execute_string(fs.quote_args(command, ...)) +end + +--- Run the given command, quoting its arguments, silencing its output. +-- The command is executed in the current directory in the dir stack. +-- Silencing is omitted if 'verbose' mode is enabled. +-- @param command string: The command to be executed. No quoting/escaping +-- is applied. +-- @param ... Strings containing additional arguments, which will be quoted. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function fs_lua.execute_quiet(command, ...) + assert(type(command) == "string") + if cfg.verbose then -- omit silencing output + return fs.execute_string(fs.quote_args(command, ...)) + else + return fs.execute_string(fs.quiet(fs.quote_args(command, ...))) + end +end + +function fs_lua.execute_env(env, command, ...) + assert(type(command) == "string") + local envstr = {} + for var, val in pairs(env) do + table.insert(envstr, fs.export_cmd(var, val)) + end + return fs.execute_string(table.concat(envstr, "\n") .. "\n" .. fs.quote_args(command, ...)) +end + +local tool_available_cache = {} + +function fs_lua.set_tool_available(tool_name, value) + assert(type(value) == "boolean") + tool_available_cache[tool_name] = value +end + +--- Checks if the given tool is available. +-- The tool is executed using a flag, usually just to ask its version. +-- @param tool_cmd string: The command to be used to check the tool's presence (e.g. hg in case of Mercurial) +-- @param tool_name string: The actual name of the tool (e.g. Mercurial) +function fs_lua.is_tool_available(tool_cmd, tool_name) + assert(type(tool_cmd) == "string") + assert(type(tool_name) == "string") + + local ok + if tool_available_cache[tool_name] ~= nil then + ok = tool_available_cache[tool_name] + else + local tool_cmd_no_args = tool_cmd:gsub(" [^\"]*$", "") + + -- if it looks like the tool has a pathname, try that first + if tool_cmd_no_args:match("[/\\]") then + local tool_cmd_no_args_normalized = dir.path(tool_cmd_no_args) + local fd = io.open(tool_cmd_no_args_normalized, "r") + if fd then + fd:close() + ok = true + end + end + + if not ok then + ok = fs.search_in_path(tool_cmd_no_args) + end + + tool_available_cache[tool_name] = (ok == true) + end + + if ok then + return true + else + local msg = "'%s' program not found. Make sure %s is installed and is available in your PATH " .. + "(or you may want to edit the 'variables.%s' value in file '%s')" + return nil, msg:format(tool_cmd, tool_name, tool_name:upper(), cfg.config_files.nearest) + end +end + +--- Check the MD5 checksum for a file. +-- @param file string: The file to be checked. +-- @param md5sum string: The string with the expected MD5 checksum. +-- @return boolean: true if the MD5 checksum for 'file' equals 'md5sum', false + msg if not +-- or if it could not perform the check for any reason. +function fs_lua.check_md5(file, md5sum) + file = dir.normalize(file) + local computed, msg = fs.get_md5(file) + if not computed then + return false, msg + end + if computed:match("^"..md5sum) then + return true + else + return false, "Mismatch MD5 hash for file "..file + end +end + +--- List the contents of a directory. +-- @param at string or nil: directory to list (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. +function fs_lua.list_dir(at) + local result = {} + for file in fs.dir(at) do + result[#result+1] = file + end + return result +end + +--- Iterate over the contents of a directory. +-- @param at string or nil: directory to list (will be the current +-- directory if none is given). +-- @return function: an iterator function suitable for use with +-- the for statement. +function fs_lua.dir(at) + if not at then + at = fs.current_dir() + end + at = dir.normalize(at) + if not fs.is_dir(at) then + return function() end + end + return coroutine.wrap(function() fs.dir_iterator(at) end) +end + +--- List the Lua modules at a specific require path. +-- eg. `modules("luarocks.cmd")` would return a list of all LuaRocks command +-- modules, in the current Lua path. +function fs_lua.modules(at) + at = at or "" + if #at > 0 then + -- turn require path into file path + at = at:gsub("%.", package.config:sub(1,1)) .. package.config:sub(1,1) + end + + local path = package.path:sub(-1, -1) == ";" and package.path or package.path .. ";" + local paths = {} + for location in path:gmatch("(.-);") do + if location:lower() == "?.lua" then + location = "./?.lua" + end + local _, q_count = location:gsub("%?", "") -- only use the ones with a single '?' + if location:match("%?%.[lL][uU][aA]$") and q_count == 1 then -- only use when ending with "?.lua" + location = location:gsub("%?%.[lL][uU][aA]$", at) + table.insert(paths, location) + end + end + + if #paths == 0 then + return {} + end + + local modules = {} + local is_duplicate = {} + for _, path in ipairs(paths) do -- luacheck: ignore 421 + local files = fs.list_dir(path) + for _, filename in ipairs(files or {}) do + if filename:match("%.[lL][uU][aA]$") then + filename = filename:sub(1,-5) -- drop the extension + if not is_duplicate[filename] then + is_duplicate[filename] = true + table.insert(modules, filename) + end + end + end + end + + return modules +end + +function fs_lua.filter_file(fn, input_filename, output_filename) + local fd, err = io.open(input_filename, "rb") + if not fd then + return nil, err + end + + local input, err = fd:read("*a") + fd:close() + if not input then + return nil, err + end + + local output, err = fn(input) + if not output then + return nil, err + end + + fd, err = io.open(output_filename, "wb") + if not fd then + return nil, err + end + + local ok, err = fd:write(output) + fd:close() + if not ok then + return nil, err + end + + return true +end + +function fs_lua.system_temp_dir() + return os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp" +end + +local function temp_dir_pattern(name_pattern) + return dir.path(fs.system_temp_dir(), + "luarocks_" .. dir.normalize(name_pattern):gsub("[/\\]", "_") .. "-") +end + +--------------------------------------------------------------------- +-- LuaFileSystem functions +--------------------------------------------------------------------- + +if lfs_ok then + +function fs_lua.file_age(filename) + local attr = lfs.attributes(filename) + if attr and attr.change then + return os.difftime(os.time(), attr.change) + end + return math.huge +end + +function fs_lua.lock_access(dirname, force) + fs.make_dir(dirname) + local lockfile = dir.path(dirname, "lockfile.lfs") + + -- drop stale lock, older than 1 hour + local age = fs.file_age(lockfile) + if age > 3600 and age < math.huge then + force = true + end + + if force then + os.remove(lockfile) + end + return lfs.lock_dir(dirname) +end + +function fs_lua.unlock_access(lock) + return lock:free() +end + +--- Run the given command. +-- The command is executed in the current directory in the dir stack. +-- @param cmd string: No quoting/escaping is applied to the command. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function fs_lua.execute_string(cmd) + local code = os.execute(cmd) + return (code == 0 or code == true) +end + +--- Obtain current directory. +-- Uses the module's internal dir stack. +-- @return string: the absolute pathname of the current directory. +function fs_lua.current_dir() + return lfs.currentdir() +end + +--- Change the current directory. +-- Uses the module's internal dir stack. This does not have exact +-- semantics of chdir, as it does not handle errors the same way, +-- but works well for our purposes for now. +-- @param d string: The directory to switch to. +function fs_lua.change_dir(d) + table.insert(dir_stack, lfs.currentdir()) + d = dir.normalize(d) + return lfs.chdir(d) +end + +--- Change directory to root. +-- Allows leaving a directory (e.g. for deleting it) in +-- a crossplatform way. +function fs_lua.change_dir_to_root() + local current = lfs.currentdir() + if not current or current == "" then + return false + end + table.insert(dir_stack, current) + lfs.chdir("/") -- works on Windows too + return true +end + +--- Change working directory to the previous in the dir stack. +-- @return true if a pop occurred, false if the stack was empty. +function fs_lua.pop_dir() + local d = table.remove(dir_stack) + if d then + lfs.chdir(d) + return true + else + return false + end +end + +--- Create a directory if it does not already exist. +-- If any of the higher levels in the path name do not exist +-- too, they are created as well. +-- @param directory string: pathname of directory to create. +-- @return boolean or (boolean, string): true on success or (false, error message) on failure. +function fs_lua.make_dir(directory) + assert(type(directory) == "string") + directory = dir.normalize(directory) + local path = nil + if directory:sub(2, 2) == ":" then + path = directory:sub(1, 2) + directory = directory:sub(4) + else + if directory:match("^" .. dir_sep) then + path = "" + end + end + for d in directory:gmatch("([^" .. dir_sep .. "]+)" .. dir_sep .. "*") do + path = path and path .. dir_sep .. d or d + local mode = lfs.attributes(path, "mode") + if not mode then + local ok, err = lfs.mkdir(path) + if not ok then + return false, err + end + if cfg.is_platform("unix") then + ok, err = fs.set_permissions(path, "exec", "all") + if not ok then + return false, err + end + end + elseif mode ~= "directory" then + return false, path.." is not a directory" + end + end + return true +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param d string: pathname of directory to remove. +function fs_lua.remove_dir_if_empty(d) + assert(d) + d = dir.normalize(d) + lfs.rmdir(d) +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param d string: pathname of directory to remove. +function fs_lua.remove_dir_tree_if_empty(d) + assert(d) + d = dir.normalize(d) + for i=1,10 do + lfs.rmdir(d) + d = dir.dir_name(d) + end +end + +local function are_the_same_file(f1, f2) + if f1 == f2 then + return true + end + if cfg.is_platform("unix") then + local i1 = lfs.attributes(f1, "ino") + local i2 = lfs.attributes(f2, "ino") + if i1 ~= nil and i1 == i2 then + return true + end + end + return false +end + +--- Copy a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Permissions for destination +-- file or nil to use the source file permissions +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.copy(src, dest, perms) + assert(src and dest) + src = dir.normalize(src) + dest = dir.normalize(dest) + local destmode = lfs.attributes(dest, "mode") + if destmode == "directory" then + dest = dir.path(dest, dir.base_name(src)) + end + if are_the_same_file(src, dest) then + return nil, "The source and destination are the same files" + end + local src_h, err = io.open(src, "rb") + if not src_h then return nil, err end + local dest_h, err = io.open(dest, "w+b") + if not dest_h then src_h:close() return nil, err end + while true do + local block = src_h:read(8192) + if not block then break end + local ok, err = dest_h:write(block) + if not ok then return nil, err end + end + src_h:close() + dest_h:close() + + local fullattrs + if not perms then + fullattrs = lfs.attributes(src, "permissions") + end + if fullattrs and posix_ok then + return posix.chmod(dest, fullattrs) + else + if cfg.is_platform("unix") then + if not perms then + perms = fullattrs:match("x") and "exec" or "read" + end + return fs.set_permissions(dest, perms, "all") + else + return true + end + end +end + +--- Implementation function for recursive copy of directory contents. +-- Assumes paths are normalized. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Optional permissions. +-- If not given, permissions of the source are copied over to the destination. +-- @return boolean or (boolean, string): true on success, false on failure +local function recursive_copy(src, dest, perms) + local srcmode = lfs.attributes(src, "mode") + + if srcmode == "file" then + local ok = fs.copy(src, dest, perms) + if not ok then return false end + elseif srcmode == "directory" then + local subdir = dir.path(dest, dir.base_name(src)) + local ok, err = fs.make_dir(subdir) + if not ok then return nil, err end + if pcall(lfs.dir, src) == false then + return false + end + for file in lfs.dir(src) do + if file ~= "." and file ~= ".." then + local ok = recursive_copy(dir.path(src, file), subdir, perms) + if not ok then return false end + end + end + end + return true +end + +--- Recursively copy the contents of a directory. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Optional permissions. +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.copy_contents(src, dest, perms) + assert(src) + assert(dest) + src = dir.normalize(src) + dest = dir.normalize(dest) + if not fs.is_dir(src) then + return false, src .. " is not a directory" + end + if pcall(lfs.dir, src) == false then + return false, "Permission denied" + end + for file in lfs.dir(src) do + if file ~= "." and file ~= ".." then + local ok = recursive_copy(dir.path(src, file), dest, perms) + if not ok then + return false, "Failed copying "..src.." to "..dest + end + end + end + return true +end + +--- Implementation function for recursive removal of directories. +-- Assumes paths are normalized. +-- @param name string: Pathname of file +-- @return boolean or (boolean, string): true on success, +-- or nil and an error message on failure. +local function recursive_delete(name) + local ok = os.remove(name) + if ok then return true end + local pok, ok, err = pcall(function() + for file in lfs.dir(name) do + if file ~= "." and file ~= ".." then + local ok, err = recursive_delete(dir.path(name, file)) + if not ok then return nil, err end + end + end + local ok, err = lfs.rmdir(name) + return ok, (not ok) and err + end) + if pok then + return ok, err + else + return pok, ok + end +end + +--- Delete a file or a directory and all its contents. +-- @param name string: Pathname of source +-- @return nil +function fs_lua.delete(name) + name = dir.normalize(name) + recursive_delete(name) +end + +--- Internal implementation function for fs.dir. +-- Yields a filename on each iteration. +-- @param at string: directory to list +-- @return nil or (nil and string): an error message on failure +function fs_lua.dir_iterator(at) + local pok, iter, arg = pcall(lfs.dir, at) + if not pok then + return nil, iter + end + for file in iter, arg do + if file ~= "." and file ~= ".." then + coroutine.yield(file) + end + end +end + +--- Implementation function for recursive find. +-- Assumes paths are normalized. +-- @param cwd string: Current working directory in recursion. +-- @param prefix string: Auxiliary prefix string to form pathname. +-- @param result table: Array of strings where results are collected. +local function recursive_find(cwd, prefix, result) + local pok, iter, arg = pcall(lfs.dir, cwd) + if not pok then + return nil + end + for file in iter, arg do + if file ~= "." and file ~= ".." then + local item = prefix .. file + table.insert(result, item) + local pathname = dir.path(cwd, file) + if lfs.attributes(pathname, "mode") == "directory" then + recursive_find(pathname, item .. dir_sep, result) + end + end + end +end + +--- Recursively scan the contents of a directory. +-- @param at string or nil: directory to scan (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. +function fs_lua.find(at) + assert(type(at) == "string" or not at) + if not at then + at = fs.current_dir() + end + at = dir.normalize(at) + local result = {} + recursive_find(at, "", result) + return result +end + +--- Test for existence of a file. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function fs_lua.exists(file) + assert(file) + file = dir.normalize(file) + return type(lfs.attributes(file)) == "table" +end + +--- Test is pathname is a directory. +-- @param file string: pathname to test +-- @return boolean: true if it is a directory, false otherwise. +function fs_lua.is_dir(file) + assert(file) + file = dir.normalize(file) + return lfs.attributes(file, "mode") == "directory" +end + +--- Test is pathname is a regular file. +-- @param file string: pathname to test +-- @return boolean: true if it is a file, false otherwise. +function fs_lua.is_file(file) + assert(file) + file = dir.normalize(file) + return lfs.attributes(file, "mode") == "file" +end + +-- Set access and modification times for a file. +-- @param filename File to set access and modification times for. +-- @param time may be a number containing the format returned +-- by os.time, or a table ready to be processed via os.time; if +-- nil, current time is assumed. +function fs_lua.set_time(file, time) + assert(time == nil or type(time) == "table" or type(time) == "number") + file = dir.normalize(file) + if type(time) == "table" then + time = os.time(time) + end + return lfs.touch(file, time) +end + +else -- if not lfs_ok + +function fs_lua.exists(file) + assert(file) + -- check if file exists by attempting to open it + return util.exists(fs.absolute_name(file)) +end + +function fs_lua.file_age(_) + return math.huge +end + +end + +--------------------------------------------------------------------- +-- lua-bz2 functions +--------------------------------------------------------------------- + +if bz2_ok then + +local function bunzip2_string(data) + local decompressor = bz2.initDecompress() + local output, err = decompressor:update(data) + if not output then + return nil, err + end + decompressor:close() + return output +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function fs_lua.bunzip2(infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + if not outfile then + outfile = infile:gsub("%.bz2$", "") + end + + return fs.filter_file(bunzip2_string, infile, outfile) +end + +end + +--------------------------------------------------------------------- +-- luarocks.tools.zip functions +--------------------------------------------------------------------- + +if zip_ok then + +function fs_lua.zip(zipfile, ...) + return zip.zip(zipfile, ...) +end + +function fs_lua.unzip(zipfile) + return zip.unzip(zipfile) +end + +function fs_lua.gunzip(infile, outfile) + return zip.gunzip(infile, outfile) +end + +end + +--------------------------------------------------------------------- +-- LuaSocket functions +--------------------------------------------------------------------- + +if socket_ok then + +local ltn12 = require("ltn12") +local luasec_ok, https = pcall(require, "ssl.https") + +if luasec_ok and not vers.compare_versions(https._VERSION, "1.0.3") then + luasec_ok = false + https = nil +end + +local redirect_protocols = { + http = http, + https = luasec_ok and https, +} + +local function request(url, method, http, loop_control) -- luacheck: ignore 431 + local result = {} + + if cfg.verbose then + print(method, url) + end + + local proxy = os.getenv("http_proxy") + if type(proxy) ~= "string" then proxy = nil end + -- LuaSocket's http.request crashes when given URLs missing the scheme part. + if proxy and not proxy:find("://") then + proxy = "http://" .. proxy + end + + if cfg.show_downloads then + io.write(method.." "..url.." ...\n") + end + local dots = 0 + if cfg.connection_timeout and cfg.connection_timeout > 0 then + http.TIMEOUT = cfg.connection_timeout + end + local res, status, headers, err = http.request { + url = url, + proxy = proxy, + method = method, + redirect = false, + sink = ltn12.sink.table(result), + step = cfg.show_downloads and function(...) + io.write(".") + io.flush() + dots = dots + 1 + if dots == 70 then + io.write("\n") + dots = 0 + end + return ltn12.pump.step(...) + end, + headers = { + ["user-agent"] = cfg.user_agent.." via LuaSocket" + }, + } + if cfg.show_downloads then + io.write("\n") + end + if not res then + return nil, status + elseif status == 301 or status == 302 then + local location = headers.location + if location then + local protocol, rest = dir.split_url(location) + if redirect_protocols[protocol] then + if not loop_control then + loop_control = {} + elseif loop_control[location] then + return nil, "Redirection loop -- broken URL?" + end + loop_control[url] = true + return request(location, method, redirect_protocols[protocol], loop_control) + else + return nil, "URL redirected to unsupported protocol - install luasec >= 1.1 to get HTTPS support.", "https" + end + end + return nil, err + elseif status ~= 200 then + return nil, err + else + return result, status, headers, err + end +end + +local function write_timestamp(filename, data) + local fd = io.open(filename, "w") + if fd then + fd:write(data) + fd:close() + end +end + +local function read_timestamp(filename) + local fd = io.open(filename, "r") + if fd then + local data = fd:read("*a") + fd:close() + return data + end +end + +local function fail_with_status(filename, status, headers) + write_timestamp(filename .. ".unixtime", os.time()) + write_timestamp(filename .. ".status", status) + return nil, status, headers +end + +-- @param url string: URL to fetch. +-- @param filename string: local filename of the file to fetch. +-- @param http table: The library to use (http from LuaSocket or LuaSec) +-- @param cache boolean: Whether to use a `.timestamp` file to check +-- via the HTTP Last-Modified header if the full download is needed. +-- @return (boolean | (nil, string, string?)): True if successful, or +-- nil, error message and optionally HTTPS error in case of errors. +local function http_request(url, filename, http, cache) -- luacheck: ignore 431 + if cache then + local status = read_timestamp(filename..".status") + local timestamp = read_timestamp(filename..".timestamp") + if status or timestamp then + local unixtime = read_timestamp(filename..".unixtime") + if tonumber(unixtime) then + local diff = os.time() - tonumber(unixtime) + if status then + if diff < cfg.cache_fail_timeout then + return nil, status, {} + end + else + if diff < cfg.cache_timeout then + return true, nil, nil, true + end + end + end + + local result, status, headers, err = request(url, "HEAD", http) -- luacheck: ignore 421 + if not result then + return fail_with_status(filename, status, headers) + end + if status == 200 and headers["last-modified"] == timestamp then + write_timestamp(filename .. ".unixtime", os.time()) + return true, nil, nil, true + end + end + end + local result, status, headers, err = request(url, "GET", http) + if not result then + if status then + return fail_with_status(filename, status, headers) + end + end + if cache and headers["last-modified"] then + write_timestamp(filename .. ".timestamp", headers["last-modified"]) + write_timestamp(filename .. ".unixtime", os.time()) + end + local file = io.open(filename, "wb") + if not file then return nil, 0, {} end + for _, data in ipairs(result) do + file:write(data) + end + file:close() + return true +end + +local function ftp_request(url, filename) + local content, err = ftp.get(url) + if not content then + return false, err + end + local file = io.open(filename, "wb") + if not file then return false, err end + file:write(content) + file:close() + return true +end + +local downloader_warning = false + +--- Download a remote file. +-- @param url string: URL to be fetched. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @return (boolean, string, boolean): +-- In case of success: +-- * true +-- * a string with the filename +-- * true if the file was retrieved from local cache +-- In case of failure: +-- * false +-- * error message +function fs_lua.download(url, filename, cache) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + filename = fs.absolute_name(filename or dir.base_name(url)) + + -- delegate to the configured downloader so we don't have to deal with whitelists + if os.getenv("no_proxy") then + return fs.use_downloader(url, filename, cache) + end + + local ok, err, https_err, from_cache + if util.starts_with(url, "http:") then + ok, err, https_err, from_cache = http_request(url, filename, http, cache) + elseif util.starts_with(url, "ftp:") then + ok, err = ftp_request(url, filename) + elseif util.starts_with(url, "https:") then + -- skip LuaSec when proxy is enabled since it is not supported + if luasec_ok and not os.getenv("https_proxy") then + local _ + ok, err, _, from_cache = http_request(url, filename, https, cache) + else + https_err = true + end + else + err = "Unsupported protocol" + end + if https_err then + local downloader, err = fs.which_tool("downloader") + if not downloader then + return nil, err + end + if not downloader_warning then + util.warning("falling back to "..downloader.." - install luasec >= 1.1 to get native HTTPS support") + downloader_warning = true + end + return fs.use_downloader(url, filename, cache) + elseif not ok then + return nil, err, "network" + end + return true, filename, from_cache +end + +else --...if socket_ok == false then + +function fs_lua.download(url, filename, cache) + return fs.use_downloader(url, filename, cache) +end + +end +--------------------------------------------------------------------- +-- MD5 functions +--------------------------------------------------------------------- + +if md5_ok then + +-- Support the interface of lmd5 by lhf in addition to md5 by Roberto +-- and the keplerproject. +if not md5.sumhexa and md5.digest then + md5.sumhexa = function(msg) + return md5.digest(msg) + end +end + +if md5.sumhexa then + +--- Get the MD5 checksum for a file. +-- @param file string: The file to be computed. +-- @return string: The MD5 checksum or nil + error +function fs_lua.get_md5(file) + file = fs.absolute_name(file) + local file_handler = io.open(file, "rb") + if not file_handler then return nil, "Failed to open file for reading: "..file end + local computed = md5.sumhexa(file_handler:read("*a")) + file_handler:close() + if computed then return computed end + return nil, "Failed to compute MD5 hash for file "..file +end + +end +end + +--------------------------------------------------------------------- +-- POSIX functions +--------------------------------------------------------------------- + +function fs_lua._unix_rwx_to_number(rwx, neg) + local num = 0 + neg = neg or false + for i = 1, 9 do + local c = rwx:sub(10 - i, 10 - i) == "-" + if neg == c then + num = num + 2^(i-1) + end + end + return math.floor(num) +end + +if posix_ok then + +local octal_to_rwx = { + ["0"] = "---", + ["1"] = "--x", + ["2"] = "-w-", + ["3"] = "-wx", + ["4"] = "r--", + ["5"] = "r-x", + ["6"] = "rw-", + ["7"] = "rwx", +} + +do + local umask_cache + function fs_lua._unix_umask() + if umask_cache then + return umask_cache + end + -- LuaPosix (as of 34.0.4) only returns the umask as rwx + local rwx = posix.umask() + local num = fs_lua._unix_rwx_to_number(rwx, true) + umask_cache = ("%03o"):format(num) + return umask_cache + end +end + +function fs_lua.set_permissions(filename, mode, scope) + local perms, err = fs._unix_mode_scope_to_perms(mode, scope) + if err then + return false, err + end + + -- LuaPosix (as of 5.1.15) does not support octal notation... + local new_perms = {} + for c in perms:sub(-3):gmatch(".") do + table.insert(new_perms, octal_to_rwx[c]) + end + perms = table.concat(new_perms) + local err = posix.chmod(filename, perms) + return err == 0 +end + +function fs_lua.current_user() + return posix.getpwuid(posix.geteuid()).pw_name +end + +function fs_lua.is_superuser() + return posix.geteuid() == 0 +end + +-- This call is not available on all systems, see #677 +if posix.mkdtemp then + +--- Create a temporary directory. +-- @param name_pattern string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure. +function fs_lua.make_temp_dir(name_pattern) + assert(type(name_pattern) == "string") + + return posix.mkdtemp(temp_dir_pattern(name_pattern) .. "-XXXXXX") +end + +end -- if posix.mkdtemp + +end + +--------------------------------------------------------------------- +-- Other functions +--------------------------------------------------------------------- + +if not fs_lua.make_temp_dir then + +function fs_lua.make_temp_dir(name_pattern) + assert(type(name_pattern) == "string") + + local ok, err + for _ = 1, 3 do + local name = temp_dir_pattern(name_pattern) .. tostring(math.random(10000000)) + ok, err = fs.make_dir(name) + if ok then + return name + end + end + + return nil, err +end + +end + +--- Apply a patch. +-- @param patchname string: The filename of the patch. +-- @param patchdata string or nil: The actual patch as a string. +-- @param create_delete boolean: Support creating and deleting files in a patch. +function fs_lua.apply_patch(patchname, patchdata, create_delete) + local p, all_ok = patch.read_patch(patchname, patchdata) + if not all_ok then + return nil, "Failed reading patch "..patchname + end + if p then + return patch.apply_patch(p, 1, create_delete) + end +end + +--- Move a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Permissions for destination +-- file or nil to use the source file permissions. +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.move(src, dest, perms) + assert(src and dest) + if fs.exists(dest) and not fs.is_dir(dest) then + return false, "File already exists: "..dest + end + local ok, err = fs.copy(src, dest, perms) + if not ok then + return false, err + end + fs.delete(src) + if fs.exists(src) then + return false, "Failed move: could not delete "..src.." after copy." + end + return true +end + +local function get_local_tree() + for _, tree in ipairs(cfg.rocks_trees) do + if type(tree) == "table" and tree.name == "user" then + return fs.absolute_name(tree.root) + end + end +end + +local function is_local_tree_in_env(local_tree) + local lua_path + if _VERSION == "Lua 5.1" then + lua_path = os.getenv("LUA_PATH") + else + lua_path = os.getenv("LUA_PATH_" .. _VERSION:sub(5):gsub("%.", "_")) + or os.getenv("LUA_PATH") + end + if lua_path and lua_path:match(local_tree, 1, true) then + return true + end +end + +--- Check if user has write permissions for the command. +-- Assumes the configuration variables under cfg have been previously set up. +-- @param args table: the args table passed to run() drivers. +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.check_command_permissions(args) + local ok = true + local err = "" + if args._command_permissions_checked then + return true + end + for _, directory in ipairs { cfg.rocks_dir, cfg.deploy_lua_dir, cfg.deploy_bin_dir, cfg.deploy_lua_dir } do + if fs.exists(directory) then + if not fs.is_writable(directory) then + ok = false + err = "Your user does not have write permissions in " .. directory + break + end + else + local root = fs.root_of(directory) + local parent = directory + repeat + parent = dir.dir_name(parent) + if parent == "" then + parent = root + end + until parent == root or fs.exists(parent) + if not fs.is_writable(parent) then + ok = false + err = directory.." does not exist\nand your user does not have write permissions in " .. parent + break + end + end + end + if ok then + args._command_permissions_checked = true + return true + else + if args["local"] or cfg.local_by_default then + err = err .. "\n\nPlease check your permissions.\n" + else + local local_tree = get_local_tree() + if local_tree then + err = err .. "\n\nYou may want to run as a privileged user," + .. "\nor use --local to install into your local tree at " .. local_tree + .. "\nor run 'luarocks config local_by_default true' to make --local the default.\n" + + if not is_local_tree_in_env(local_tree) then + err = err .. "\n(You may need to configure your Lua package paths\nto use the local tree, see 'luarocks path --help')\n" + end + else + err = err .. "\n\nYou may want to run as a privileged user.\n" + end + end + return nil, err + end +end + +--- Check whether a file is a Lua script +-- When the file can be successfully compiled by the configured +-- Lua interpreter, it's considered to be a valid Lua file. +-- @param filename filename of file to check +-- @return boolean true, if it is a Lua script, false otherwise +function fs_lua.is_lua(filename) + filename = filename:gsub([[%\]],"/") -- normalize on fw slash to prevent escaping issues + local lua = fs.Q(cfg.variables.LUA) -- get lua interpreter configured + -- execute on configured interpreter, might not be the same as the interpreter LR is run on + local result = fs.execute_string(lua..[[ -e "if loadfile(']]..filename..[[') then os.exit(0) else os.exit(1) end"]]) + return (result == true) +end + +--- Unpack an archive. +-- Extract the contents of an archive, detecting its format by +-- filename extension. +-- @param archive string: Filename of archive. +-- @return boolean or (boolean, string): true on success, false and an error message on failure. +function fs_lua.unpack_archive(archive) + assert(type(archive) == "string") + + local ok, err + archive = fs.absolute_name(archive) + if archive:match("%.tar%.gz$") then + local tar_filename = archive:gsub("%.gz$", "") + ok, err = fs.gunzip(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.tgz$") then + local tar_filename = archive:gsub("%.tgz$", ".tar") + ok, err = fs.gunzip(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.tar%.bz2$") then + local tar_filename = archive:gsub("%.bz2$", "") + ok, err = fs.bunzip2(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.zip$") then + ok, err = fs.unzip(archive) + elseif archive:match("%.lua$") or archive:match("%.c$") then + -- Ignore .lua and .c files; they don't need to be extracted. + return true + else + return false, "Couldn't extract archive "..archive..": unrecognized filename extension" + end + if not ok then + return false, "Failed extracting "..archive..": "..err + end + return true +end + +return fs_lua diff --git a/src/luarocks/fs/macosx.lua b/src/luarocks/fs/macosx.lua new file mode 100644 index 0000000..b71e7f1 --- /dev/null +++ b/src/luarocks/fs/macosx.lua @@ -0,0 +1,50 @@ +--- macOS-specific implementation of filesystem and platform abstractions. +local macosx = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") + +function macosx.is_dir(file) + file = fs.absolute_name(file) + file = dir.normalize(file) .. "/." + local fd, _, code = io.open(file, "r") + if code == 2 then -- "No such file or directory" + return false + end + if code == 20 then -- "Not a directory", regardless of permissions + return false + end + if code == 13 then -- "Permission denied", but is a directory + return true + end + if fd then + local _, _, ecode = fd:read(1) + fd:close() + if ecode == 21 then -- "Is a directory" + return true + end + end + return false +end + +function macosx.is_file(file) + file = fs.absolute_name(file) + if fs.is_dir(file) then + return false + end + file = dir.normalize(file) + local fd, _, code = io.open(file, "r") + if code == 2 then -- "No such file or directory" + return false + end + if code == 13 then -- "Permission denied", but it exists + return true + end + if fd then + fd:close() + return true + end + return false +end + +return macosx diff --git a/src/luarocks/fs/tools.lua b/src/luarocks/fs/tools.lua new file mode 100644 index 0000000..23f2561 --- /dev/null +++ b/src/luarocks/fs/tools.lua @@ -0,0 +1,222 @@ + +--- Common fs operations implemented with third-party tools. +local tools = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") + +local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end }) + +local dir_stack = {} + +do + local tool_cache = {} + + local tool_options = { + downloader = { + desc = "downloader", + { var = "WGET", name = "wget" }, + { var = "CURL", name = "curl" }, + }, + md5checker = { + desc = "MD5 checker", + { var = "MD5SUM", name = "md5sum" }, + { var = "OPENSSL", name = "openssl", cmdarg = "md5" }, + { var = "MD5", name = "md5" }, + }, + } + + function tools.which_tool(tooltype) + local tool = tool_cache[tooltype] + local names = {} + if not tool then + for _, opt in ipairs(tool_options[tooltype]) do + table.insert(names, opt.name) + if fs.is_tool_available(vars[opt.var], opt.name) then + tool = opt + tool_cache[tooltype] = opt + break + end + end + end + if not tool then + local tool_names = table.concat(names, ", ", 1, #names - 1) .. " or " .. names[#names] + return nil, "no " .. tool_options[tooltype].desc .. " tool available," .. " please install " .. tool_names .. " in your system" + end + return tool.name, vars[tool.var] .. (tool.cmdarg and " "..tool.cmdarg or "") + end +end + +local current_dir_with_cache +do + local cache_pwd + + current_dir_with_cache = function() + local current = cache_pwd + if not current then + local pipe = io.popen(fs.quiet_stderr(vars.PWD)) + current = pipe:read("*a"):gsub("^%s*", ""):gsub("%s*$", "") + pipe:close() + cache_pwd = current + end + for _, directory in ipairs(dir_stack) do + current = fs.absolute_name(directory, current) + end + return current, cache_pwd + end + + --- Obtain current directory. + -- Uses the module's internal directory stack. + -- @return string: the absolute pathname of the current directory. + function tools.current_dir() + return (current_dir_with_cache()) -- drop second return + end +end + +--- Change the current directory. +-- Uses the module's internal directory stack. This does not have exact +-- semantics of chdir, as it does not handle errors the same way, +-- but works well for our purposes for now. +-- @param directory string: The directory to switch to. +-- @return boolean or (nil, string): true if successful, (nil, error message) if failed. +function tools.change_dir(directory) + assert(type(directory) == "string") + if fs.is_dir(directory) then + table.insert(dir_stack, directory) + return true + end + return nil, "directory not found: "..directory +end + +--- Change directory to root. +-- Allows leaving a directory (e.g. for deleting it) in +-- a crossplatform way. +function tools.change_dir_to_root() + local curr_dir = fs.current_dir() + if not curr_dir or not fs.is_dir(curr_dir) then + return false + end + table.insert(dir_stack, "/") + return true +end + +--- Change working directory to the previous in the directory stack. +function tools.pop_dir() + local directory = table.remove(dir_stack) + return directory ~= nil +end + +--- Run the given command. +-- The command is executed in the current directory in the directory stack. +-- @param cmd string: No quoting/escaping is applied to the command. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function tools.execute_string(cmd) + local current, cache_pwd = current_dir_with_cache() + if not current then return false end + if current ~= cache_pwd then + cmd = fs.command_at(current, cmd) + end + local code = os.execute(cmd) + if code == 0 or code == true then + return true + else + return false + end +end + +--- Internal implementation function for fs.dir. +-- Yields a filename on each iteration. +-- @param at string: directory to list +-- @return nil +function tools.dir_iterator(at) + local pipe = io.popen(fs.command_at(at, vars.LS, true)) + for file in pipe:lines() do + if file ~= "." and file ~= ".." then + coroutine.yield(file) + end + end + pipe:close() +end + +--- Download a remote file. +-- @param url string: URL to be fetched. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to +-- re-downloading the file. +-- @return (boolean, string, string): true and the filename on success, +-- false and the error message and code on failure. +function tools.use_downloader(url, filename, cache) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + filename = fs.absolute_name(filename or dir.base_name(url)) + + local downloader, err = fs.which_tool("downloader") + if not downloader then + return nil, err, "downloader" + end + + local ok = false + if downloader == "wget" then + local wget_cmd = vars.WGET.." "..vars.WGETNOCERTFLAG.." --no-cache --user-agent=\""..cfg.user_agent.." via wget\" --quiet " + if cfg.connection_timeout and cfg.connection_timeout > 0 then + wget_cmd = wget_cmd .. "--timeout="..tostring(cfg.connection_timeout).." --tries=1 " + end + if cache then + -- --timestamping is incompatible with --output-document, + -- but that's not a problem for our use cases. + fs.delete(filename .. ".unixtime") + fs.change_dir(dir.dir_name(filename)) + ok = fs.execute_quiet(wget_cmd.." --timestamping ", url) + fs.pop_dir() + elseif filename then + ok = fs.execute_quiet(wget_cmd.." --output-document ", filename, url) + else + ok = fs.execute_quiet(wget_cmd, url) + end + elseif downloader == "curl" then + local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --user-agent \""..cfg.user_agent.." via curl\" " + if cfg.connection_timeout and cfg.connection_timeout > 0 then + curl_cmd = curl_cmd .. "--connect-timeout "..tostring(cfg.connection_timeout).." " + end + if cache then + curl_cmd = curl_cmd .. " -R -z \"" .. filename .. "\" " + end + ok = fs.execute_string(fs.quiet_stderr(curl_cmd..fs.Q(url).." --output "..fs.Q(filename))) + end + if ok then + return true, filename + else + os.remove(filename) + return false, "failed downloading " .. url, "network" + end +end + +--- Get the MD5 checksum for a file. +-- @param file string: The file to be computed. +-- @return string: The MD5 checksum or nil + message +function tools.get_md5(file) + local ok, md5checker = fs.which_tool("md5checker") + if not ok then + return false, md5checker + end + + local pipe = io.popen(md5checker.." "..fs.Q(fs.absolute_name(file))) + local computed = pipe:read("*l") + pipe:close() + if computed then + computed = computed:match("("..("%x"):rep(32)..")") + end + if computed then + return computed + else + return nil, "Failed to compute MD5 hash for file "..tostring(fs.absolute_name(file)) + end +end + +return tools diff --git a/src/luarocks/fs/unix.lua b/src/luarocks/fs/unix.lua new file mode 100644 index 0000000..41a9ba8 --- /dev/null +++ b/src/luarocks/fs/unix.lua @@ -0,0 +1,266 @@ + +--- Unix implementation of filesystem and platform abstractions. +local unix = {} + +local fs = require("luarocks.fs") + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local util = require("luarocks.util") + +--- Annotate command string for quiet execution. +-- @param cmd string: A command-line string. +-- @return string: The command-line, with silencing annotation. +function unix.quiet(cmd) + return cmd.." 1> /dev/null 2> /dev/null" +end + +--- Annotate command string for execution with quiet stderr. +-- @param cmd string: A command-line string. +-- @return string: The command-line, with stderr silencing annotation. +function unix.quiet_stderr(cmd) + return cmd.." 2> /dev/null" +end + +--- Quote argument for shell processing. +-- Adds single quotes and escapes. +-- @param arg string: Unquoted argument. +-- @return string: Quoted argument. +function unix.Q(arg) + assert(type(arg) == "string") + return "'" .. arg:gsub("'", "'\\''") .. "'" +end + +--- Return an absolute pathname from a potentially relative one. +-- @param pathname string: pathname to convert. +-- @param relative_to string or nil: path to prepend when making +-- pathname absolute, or the current dir in the dir stack if +-- not given. +-- @return string: The pathname converted to absolute. +function unix.absolute_name(pathname, relative_to) + assert(type(pathname) == "string") + assert(type(relative_to) == "string" or not relative_to) + + local unquoted = pathname:match("^['\"](.*)['\"]$") + if unquoted then + pathname = unquoted + end + + relative_to = relative_to or fs.current_dir() + if pathname:sub(1,1) == "/" then + return dir.normalize(pathname) + else + return dir.path(relative_to, pathname) + end +end + +--- Return the root directory for the given path. +-- In Unix, root is always "/". +-- @param pathname string: pathname to use. +-- @return string: The root of the given pathname. +function unix.root_of(_) + return "/" +end + +--- Create a wrapper to make a script executable from the command-line. +-- @param script string: Pathname of script to be made executable. +-- @param target string: wrapper target pathname (without wrapper suffix). +-- @param name string: rock name to be used in loader context. +-- @param version string: rock version to be used in loader context. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function unix.wrap_script(script, target, deps_mode, name, version, ...) + assert(type(script) == "string" or not script) + assert(type(target) == "string") + assert(type(deps_mode) == "string") + assert(type(name) == "string" or not name) + assert(type(version) == "string" or not version) + + local wrapper = io.open(target, "w") + if not wrapper then + return nil, "Could not open "..target.." for writing." + end + + local lpath, lcpath = path.package_paths(deps_mode) + + local luainit = { + "package.path="..util.LQ(lpath..";").."..package.path", + "package.cpath="..util.LQ(lcpath..";").."..package.cpath", + } + + local remove_interpreter = false + local base = dir.base_name(target):gsub("%..*$", "") + if base == "luarocks" or base == "luarocks-admin" then + if cfg.is_binary then + remove_interpreter = true + end + luainit = { + "package.path="..util.LQ(package.path), + "package.cpath="..util.LQ(package.cpath), + } + end + + if name and version then + local addctx = "local k,l,_=pcall(require,"..util.LQ("luarocks.loader")..") _=k " .. + "and l.add_context("..util.LQ(name)..","..util.LQ(version)..")" + table.insert(luainit, addctx) + end + + local argv = { + fs.Q(cfg.variables["LUA"]), + "-e", + fs.Q(table.concat(luainit, ";")), + script and fs.Q(script) or [[$([ "$*" ] || echo -i)]], + ... + } + if remove_interpreter then + table.remove(argv, 1) + table.remove(argv, 1) + table.remove(argv, 1) + end + + wrapper:write("#!/bin/sh\n\n") + wrapper:write("LUAROCKS_SYSCONFDIR="..fs.Q(cfg.sysconfdir) .. " ") + wrapper:write("exec "..table.concat(argv, " ")..' "$@"\n') + wrapper:close() + + if fs.set_permissions(target, "exec", "all") then + return true + else + return nil, "Could not make "..target.." executable." + end +end + +--- Check if a file (typically inside path.bin_dir) is an actual binary +-- or a Lua wrapper. +-- @param filename string: the file name with full path. +-- @return boolean: returns true if file is an actual binary +-- (or if it couldn't check) or false if it is a Lua wrapper. +function unix.is_actual_binary(filename) + if filename:match("%.lua$") then + return false + end + local file = io.open(filename) + if not file then + return true + end + local first = file:read(2) + file:close() + if not first then + util.warning("could not read "..filename) + return true + end + return first ~= "#!" +end + +function unix.copy_binary(filename, dest) + return fs.copy(filename, dest, "exec") +end + +--- Move a file on top of the other. +-- The new file ceases to exist under its original name, +-- and takes over the name of the old file. +-- On Unix this is done through a single rename operation. +-- @param old_file The name of the original file, +-- which will be the new name of new_file. +-- @param new_file The name of the new file, +-- which will replace old_file. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function unix.replace_file(old_file, new_file) + return os.rename(new_file, old_file) +end + +function unix.tmpname() + return os.tmpname() +end + +function unix.export_cmd(var, val) + return ("export %s='%s'"):format(var, val) +end + +local octal_to_rwx = { + ["0"] = "---", + ["1"] = "--x", + ["2"] = "-w-", + ["3"] = "-wx", + ["4"] = "r--", + ["5"] = "r-x", + ["6"] = "rw-", + ["7"] = "rwx", +} +local rwx_to_octal = {} +for octal, rwx in pairs(octal_to_rwx) do + rwx_to_octal[rwx] = octal +end +--- Moderate the given permissions based on the local umask +-- @param perms string: permissions to moderate +-- @return string: the moderated permissions +local function apply_umask(perms) + local umask = fs._unix_umask() + + local moderated_perms = "" + for i = 1, 3 do + local p_rwx = octal_to_rwx[perms:sub(i, i)] + local u_rwx = octal_to_rwx[umask:sub(i, i)] + local new_perm = "" + for j = 1, 3 do + local p_val = p_rwx:sub(j, j) + local u_val = u_rwx:sub(j, j) + if p_val == u_val then + new_perm = new_perm .. "-" + else + new_perm = new_perm .. p_val + end + end + moderated_perms = moderated_perms .. rwx_to_octal[new_perm] + end + return moderated_perms +end + +function unix._unix_mode_scope_to_perms(mode, scope) + local perms + if mode == "read" and scope == "user" then + perms = apply_umask("600") + elseif mode == "exec" and scope == "user" then + perms = apply_umask("700") + elseif mode == "read" and scope == "all" then + perms = apply_umask("666") + elseif mode == "exec" and scope == "all" then + perms = apply_umask("777") + else + return false, "Invalid permission " .. mode .. " for " .. scope + end + return perms +end + +function unix.system_cache_dir() + if fs.is_dir("/var/cache") then + return "/var/cache" + end + return dir.path(fs.system_temp_dir(), "cache") +end + +function unix.search_in_path(program) + if program:match("/") then + local fd = io.open(dir.path(program), "r") + if fd then + fd:close() + return true, program + end + + return false + end + + for d in (os.getenv("PATH") or ""):gmatch("([^:]+)") do + local fd = io.open(dir.path(d, program), "r") + if fd then + fd:close() + return true, d + end + end + return false +end + +return unix diff --git a/src/luarocks/fs/unix/tools.lua b/src/luarocks/fs/unix/tools.lua new file mode 100644 index 0000000..d733473 --- /dev/null +++ b/src/luarocks/fs/unix/tools.lua @@ -0,0 +1,353 @@ + +--- fs operations implemented with third-party tools for Unix platform abstractions. +local tools = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") + +local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end }) + +--- Adds prefix to command to make it run from a directory. +-- @param directory string: Path to a directory. +-- @param cmd string: A command-line string. +-- @return string: The command-line with prefix. +function tools.command_at(directory, cmd) + return "cd " .. fs.Q(fs.absolute_name(directory)) .. " && " .. cmd +end + +--- Create a directory if it does not already exist. +-- If any of the higher levels in the path name does not exist +-- too, they are created as well. +-- @param directory string: pathname of directory to create. +-- @return boolean: true on success, false on failure. +function tools.make_dir(directory) + assert(directory) + local ok, err = fs.execute(vars.MKDIR.." -p", directory) + if not ok then + err = "failed making directory "..directory + end + return ok, err +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param directory string: pathname of directory to remove. +function tools.remove_dir_if_empty(directory) + assert(directory) + fs.execute_quiet(vars.RMDIR, directory) +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param directory string: pathname of directory to remove. +function tools.remove_dir_tree_if_empty(directory) + assert(directory) + fs.execute_quiet(vars.RMDIR, "-p", directory) +end + +--- Copy a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perm string ("read" or "exec") or nil: Permissions for destination +-- file or nil to use the source permissions +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function tools.copy(src, dest, perm) + assert(src and dest) + if fs.execute(vars.CP, src, dest) then + if perm then + if fs.is_dir(dest) then + dest = dir.path(dest, dir.base_name(src)) + end + if fs.set_permissions(dest, perm, "all") then + return true + else + return false, "Failed setting permissions of "..dest + end + end + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Recursively copy the contents of a directory. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function tools.copy_contents(src, dest) + assert(src and dest) + if not fs.is_dir(src) then + return false, src .. " is not a directory" + end + if fs.make_dir(dest) and fs.execute_quiet(vars.CP.." -pPR "..fs.Q(src).."/* "..fs.Q(dest)) then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end +--- Delete a file or a directory and all its contents. +-- For safety, this only accepts absolute paths. +-- @param arg string: Pathname of source +-- @return nil +function tools.delete(arg) + assert(arg) + assert(arg:sub(1,1) == "/") + fs.execute_quiet(vars.RM, "-rf", arg) +end + +--- Recursively scan the contents of a directory. +-- @param at string or nil: directory to scan (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. +function tools.find(at) + assert(type(at) == "string" or not at) + if not at then + at = fs.current_dir() + end + if not fs.is_dir(at) then + return {} + end + local result = {} + local pipe = io.popen(fs.command_at(at, fs.quiet_stderr(vars.FIND.." *"))) + for file in pipe:lines() do + table.insert(result, file) + end + pipe:close() + return result +end + +--- Compress files in a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @param ... Filenames to be stored in the archive are given as +-- additional arguments. +-- @return boolean: true on success, nil and error message on failure. +function tools.zip(zipfile, ...) + local ok, err = fs.is_tool_available(vars.ZIP, "zip") + if not ok then + return nil, err + end + if fs.execute_quiet(vars.ZIP.." -r", zipfile, ...) then + return true + else + return nil, "failed compressing " .. zipfile + end +end + +--- Uncompress files from a .zip archive. +-- @param zipfile string: pathname of .zip archive to be extracted. +-- @return boolean: true on success, nil and error message on failure. +function tools.unzip(zipfile) + assert(zipfile) + local ok, err = fs.is_tool_available(vars.UNZIP, "unzip") + if not ok then + return nil, err + end + if fs.execute_quiet(vars.UNZIP, zipfile) then + return true + else + return nil, "failed extracting " .. zipfile + end +end + +local function uncompress(default_ext, program, infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + if not outfile then + outfile = infile:gsub("%."..default_ext.."$", "") + end + if fs.execute(fs.Q(program).." -c "..fs.Q(infile).." > "..fs.Q(outfile)) then + return true + else + return nil, "failed extracting " .. infile + end +end + +--- Uncompresses a .gz file. +-- @param infile string: pathname of .gz file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.gunzip(infile, outfile) + return uncompress("gz", "gunzip", infile, outfile) +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.bunzip2(infile, outfile) + return uncompress("bz2", "bunzip2", infile, outfile) +end + +do + local function rwx_to_octal(rwx) + return (rwx:match "r" and 4 or 0) + + (rwx:match "w" and 2 or 0) + + (rwx:match "x" and 1 or 0) + end + local umask_cache + function tools._unix_umask() + if umask_cache then + return umask_cache + end + local fd = assert(io.popen("umask -S")) + local umask = assert(fd:read("*a")) + fd:close() + local u, g, o = umask:match("u=([rwx]*),g=([rwx]*),o=([rwx]*)") + if not u then + error("invalid umask result") + end + umask_cache = string.format("%d%d%d", + 7 - rwx_to_octal(u), + 7 - rwx_to_octal(g), + 7 - rwx_to_octal(o)) + return umask_cache + end +end + +--- Set permissions for file or directory +-- @param filename string: filename whose permissions are to be modified +-- @param mode string ("read" or "exec"): permissions to set +-- @param scope string ("user" or "all"): the user(s) to whom the permission applies +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message +function tools.set_permissions(filename, mode, scope) + assert(filename and mode and scope) + + local perms, err = fs._unix_mode_scope_to_perms(mode, scope) + if err then + return false, err + end + + return fs.execute(vars.CHMOD, perms, filename) +end + +function tools.browser(url) + return fs.execute(cfg.web_browser, url) +end + +-- Set access and modification times for a file. +-- @param filename File to set access and modification times for. +-- @param time may be a string or number containing the format returned +-- by os.time, or a table ready to be processed via os.time; if +-- nil, current time is assumed. +function tools.set_time(file, time) + assert(time == nil or type(time) == "table" or type(time) == "number") + file = dir.normalize(file) + local flag = "" + if type(time) == "number" then + time = os.date("*t", time) + end + if type(time) == "table" then + flag = ("-t %04d%02d%02d%02d%02d.%02d"):format(time.year, time.month, time.day, time.hour, time.min, time.sec) + end + return fs.execute(vars.TOUCH .. " " .. flag, file) +end + +--- Create a temporary directory. +-- @param name_pattern string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure. +function tools.make_temp_dir(name_pattern) + assert(type(name_pattern) == "string") + name_pattern = dir.normalize(name_pattern) + + local template = (os.getenv("TMPDIR") or "/tmp") .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-XXXXXX" + local pipe = io.popen(vars.MKTEMP.." -d "..fs.Q(template)) + local dirname = pipe:read("*l") + pipe:close() + if dirname and dirname:match("^/") then + return dirname + end + return nil, "Failed to create temporary directory "..tostring(dirname) +end + +--- Test is file/directory exists +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function tools.exists(file) + assert(file) + return fs.execute(vars.TEST, "-e", file) +end + +--- Test is pathname is a directory. +-- @param file string: pathname to test +-- @return boolean: true if it is a directory, false otherwise. +function tools.is_dir(file) + assert(file) + return fs.execute(vars.TEST, "-d", file) +end + +--- Test is pathname is a regular file. +-- @param file string: pathname to test +-- @return boolean: true if it is a regular file, false otherwise. +function tools.is_file(file) + assert(file) + return fs.execute(vars.TEST, "-f", file) +end + +function tools.current_user() + local user = os.getenv("USER") + if user then + return user + end + local pd = io.popen("whoami", "r") + if not pd then + return "" + end + user = pd:read("*l") + pd:close() + return user +end + +function tools.is_superuser() + return fs.current_user() == "root" +end + +function tools.lock_access(dirname, force) + local ok, err = fs.make_dir(dirname) + if not ok then + return nil, err + end + + local tempfile = dir.path(dirname, ".lock.tmp." .. tostring(math.random(100000000))) + + local fd, fderr = io.open(tempfile, "w") + if not fd then + return nil, "failed opening temp file " .. tempfile .. " for locking: " .. fderr + end + + local ok, werr = fd:write("lock file for " .. dirname) + if not ok then + return nil, "failed writing temp file " .. tempfile .. " for locking: " .. werr + end + + fd:close() + + local lockfile = dir.path(dirname, "lockfile.lfs") + + local force_flag = force and " -f" or "" + + if fs.execute(vars.LN .. force_flag, tempfile, lockfile) then + return { + tempfile = tempfile, + lockfile = lockfile, + } + else + return nil, "File exists" -- same message as luafilesystem + end +end + +function tools.unlock_access(lock) + os.remove(lock.lockfile) + os.remove(lock.tempfile) +end + +return tools diff --git a/src/luarocks/fs/win32.lua b/src/luarocks/fs/win32.lua new file mode 100644 index 0000000..bba6873 --- /dev/null +++ b/src/luarocks/fs/win32.lua @@ -0,0 +1,384 @@ +--- Windows implementation of filesystem and platform abstractions. +-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities +-- used by this module. +local win32 = {} + +local fs = require("luarocks.fs") + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local util = require("luarocks.util") + +-- Monkey patch io.popen and os.execute to make sure quoting +-- works as expected. +-- See http://lua-users.org/lists/lua-l/2013-11/msg00367.html +local _prefix = "type NUL && " +local _popen, _execute = io.popen, os.execute + +-- luacheck: push globals io os +io.popen = function(cmd, ...) return _popen(_prefix..cmd, ...) end +os.execute = function(cmd, ...) return _execute(_prefix..cmd, ...) end +-- luacheck: pop + +--- Annotate command string for quiet execution. +-- @param cmd string: A command-line string. +-- @return string: The command-line, with silencing annotation. +function win32.quiet(cmd) + return cmd.." 2> NUL 1> NUL" +end + +--- Annotate command string for execution with quiet stderr. +-- @param cmd string: A command-line string. +-- @return string: The command-line, with stderr silencing annotation. +function win32.quiet_stderr(cmd) + return cmd.." 2> NUL" +end + +function win32.execute_env(env, command, ...) + assert(type(command) == "string") + local cmdstr = {} + for var, val in pairs(env) do + table.insert(cmdstr, fs.export_cmd(var, val)) + end + table.insert(cmdstr, fs.quote_args(command, ...)) + return fs.execute_string(table.concat(cmdstr, " & ")) +end + +-- Split path into drive, root and the rest. +-- Example: "c:\\hello\\world" becomes "c:" "\\" "hello\\world" +-- if any part is missing from input, it becomes an empty string. +local function split_root(pathname) + local drive = "" + local root = "" + local rest + + local unquoted = pathname:match("^['\"](.*)['\"]$") + if unquoted then + pathname = unquoted + end + + if pathname:match("^.:") then + drive = pathname:sub(1, 2) + pathname = pathname:sub(3) + end + + if pathname:match("^[\\/]") then + root = pathname:sub(1, 1) + rest = pathname:sub(2) + else + rest = pathname + end + + return drive, root, rest +end + +--- Quote argument for shell processing. Fixes paths on Windows. +-- Adds double quotes and escapes. +-- @param arg string: Unquoted argument. +-- @return string: Quoted argument. +function win32.Q(arg) + assert(type(arg) == "string") + -- Use Windows-specific directory separator for paths. + -- Paths should be converted to absolute by now. + local drive, root, rest = split_root(arg) + if root ~= "" then + arg = arg:gsub("/", "\\") + end + if arg == "\\" then + return '\\' -- CHDIR needs special handling for root dir + end + -- URLs and anything else + arg = arg:gsub('\\(\\*)"', '\\%1%1"') + arg = arg:gsub('\\+$', '%0%0') + arg = arg:gsub('"', '\\"') + arg = arg:gsub('(\\*)%%', '%1%1"%%"') + return '"' .. arg .. '"' +end + +--- Quote argument for shell processing in batch files. +-- Adds double quotes and escapes. +-- @param arg string: Unquoted argument. +-- @return string: Quoted argument. +function win32.Qb(arg) + assert(type(arg) == "string") + -- Use Windows-specific directory separator for paths. + -- Paths should be converted to absolute by now. + local drive, root, rest = split_root(arg) + if root ~= "" then + arg = arg:gsub("/", "\\") + end + if arg == "\\" then + return '\\' -- CHDIR needs special handling for root dir + end + -- URLs and anything else + arg = arg:gsub('\\(\\*)"', '\\%1%1"') + arg = arg:gsub('\\+$', '%0%0') + arg = arg:gsub('"', '\\"') + arg = arg:gsub('%%', '%%%%') + return '"' .. arg .. '"' +end + +--- Return an absolute pathname from a potentially relative one. +-- @param pathname string: pathname to convert. +-- @param relative_to string or nil: path to prepend when making +-- pathname absolute, or the current dir in the dir stack if +-- not given. +-- @return string: The pathname converted to absolute. +function win32.absolute_name(pathname, relative_to) + assert(type(pathname) == "string") + assert(type(relative_to) == "string" or not relative_to) + + relative_to = (relative_to or fs.current_dir()):gsub("[\\/]*$", "") + local drive, root, rest = split_root(pathname) + if root:match("[\\/]$") then + -- It's an absolute path already. Ensure is not quoted. + return dir.normalize(drive .. root .. rest) + else + -- It's a relative path, join it with base path. + -- This drops drive letter from paths like "C:foo". + return dir.path(relative_to, rest) + end +end + +--- Return the root directory for the given path. +-- For example, for "c:\hello", returns "c:\" +-- @param pathname string: pathname to use. +-- @return string: The root of the given pathname. +function win32.root_of(pathname) + local drive, root, rest = split_root(fs.absolute_name(pathname)) + return drive .. root +end + +--- Create a wrapper to make a script executable from the command-line. +-- @param script string: Pathname of script to be made executable. +-- @param target string: wrapper target pathname (without wrapper suffix). +-- @param name string: rock name to be used in loader context. +-- @param version string: rock version to be used in loader context. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function win32.wrap_script(script, target, deps_mode, name, version, ...) + assert(type(script) == "string" or not script) + assert(type(target) == "string") + assert(type(deps_mode) == "string") + assert(type(name) == "string" or not name) + assert(type(version) == "string" or not version) + + local wrapper = io.open(target, "wb") + if not wrapper then + return nil, "Could not open "..target.." for writing." + end + + local lpath, lcpath = path.package_paths(deps_mode) + + local luainit = { + "package.path="..util.LQ(lpath..";").."..package.path", + "package.cpath="..util.LQ(lcpath..";").."..package.cpath", + } + + local remove_interpreter = false + local base = dir.base_name(target):gsub("%..*$", "") + if base == "luarocks" or base == "luarocks-admin" then + if cfg.is_binary then + remove_interpreter = true + end + luainit = { + "package.path="..util.LQ(package.path), + "package.cpath="..util.LQ(package.cpath), + } + end + + if name and version then + local addctx = "local k,l,_=pcall(require,'luarocks.loader') _=k " .. + "and l.add_context('"..name.."','"..version.."')" + table.insert(luainit, addctx) + end + + local argv = { + fs.Qb(cfg.variables["LUA"]), + "-e", + fs.Qb(table.concat(luainit, ";")), + script and fs.Qb(script) or "%I%", + ... + } + if remove_interpreter then + table.remove(argv, 1) + table.remove(argv, 1) + table.remove(argv, 1) + end + + wrapper:write("@echo off\r\n") + wrapper:write("setlocal\r\n") + if not script then + wrapper:write([[IF "%*"=="" (set I=-i) ELSE (set I=)]] .. "\r\n") + end + wrapper:write("set "..fs.Qb("LUAROCKS_SYSCONFDIR="..cfg.sysconfdir) .. "\r\n") + wrapper:write(table.concat(argv, " ") .. " %*\r\n") + wrapper:write("exit /b %ERRORLEVEL%\r\n") + wrapper:close() + return true +end + +function win32.is_actual_binary(name) + name = name:lower() + if name:match("%.bat$") or name:match("%.exe$") then + return true + end + return false +end + +function win32.copy_binary(filename, dest) + local ok, err = fs.copy(filename, dest) + if not ok then + return nil, err + end + local exe_pattern = "%.[Ee][Xx][Ee]$" + local base = dir.base_name(filename) + dest = dir.dir_name(dest) + if base:match(exe_pattern) then + base = base:gsub(exe_pattern, ".lua") + local helpname = dest.."\\"..base + local helper = io.open(helpname, "w") + if not helper then + return nil, "Could not open "..helpname.." for writing." + end + helper:write('package.path=\"'..package.path:gsub("\\","\\\\")..';\"..package.path\n') + helper:write('package.cpath=\"'..package.path:gsub("\\","\\\\")..';\"..package.cpath\n') + helper:close() + end + return true +end + +--- Move a file on top of the other. +-- The new file ceases to exist under its original name, +-- and takes over the name of the old file. +-- On Windows this is done by removing the original file and +-- renaming the new file to its original name. +-- @param old_file The name of the original file, +-- which will be the new name of new_file. +-- @param new_file The name of the new file, +-- which will replace old_file. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function win32.replace_file(old_file, new_file) + os.remove(old_file) + return os.rename(new_file, old_file) +end + +function win32.is_dir(file) + file = fs.absolute_name(file) + file = dir.normalize(file) + local fd, _, code = io.open(file, "r") + if code == 13 then -- directories return "Permission denied" + fd, _, code = io.open(file .. "\\", "r") + if code == 2 then -- directories return 2, files return 22 + return true + end + end + if fd then + fd:close() + end + return false +end + +function win32.is_file(file) + file = fs.absolute_name(file) + file = dir.normalize(file) + local fd, _, code = io.open(file, "r") + if code == 13 then -- if "Permission denied" + fd, _, code = io.open(file .. "\\", "r") + if code == 2 then -- directories return 2, files return 22 + return false + elseif code == 22 then + return true + end + end + if fd then + fd:close() + return true + end + return false +end + +--- Test is file/dir is writable. +-- Warning: testing if a file/dir is writable does not guarantee +-- that it will remain writable and therefore it is no replacement +-- for checking the result of subsequent operations. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function win32.is_writable(file) + assert(file) + file = dir.normalize(file) + local result + local tmpname = 'tmpluarockstestwritable.deleteme' + if fs.is_dir(file) then + local file2 = dir.path(file, tmpname) + local fh = io.open(file2, 'wb') + result = fh ~= nil + if fh then fh:close() end + if result then + -- the above test might give a false positive when writing to + -- c:\program files\ because of VirtualStore redirection on Vista and up + -- So check whether it's really there + result = fs.exists(file2) + end + os.remove(file2) + else + local fh = io.open(file, 'r+b') + result = fh ~= nil + if fh then fh:close() end + end + return result +end + +function win32.tmpname() + local name = os.tmpname() + local tmp = os.getenv("TMP") + if tmp and name:sub(1, #tmp) ~= tmp then + name = (tmp .. "\\" .. name):gsub("\\+", "\\") + end + return name +end + +function win32.current_user() + return os.getenv("USERNAME") +end + +function win32.is_superuser() + return false +end + +function win32.export_cmd(var, val) + return ("SET %s"):format(fs.Q(var.."="..val)) +end + +function win32.system_cache_dir() + return dir.path(fs.system_temp_dir(), "cache") +end + +function win32.search_in_path(program) + if program:match("\\") then + local fd = io.open(dir.path(program), "r") + if fd then + fd:close() + return true, program + end + + return false + end + + if not program:lower():match("exe$") then + program = program .. ".exe" + end + + for d in (os.getenv("PATH") or ""):gmatch("([^;]+)") do + local fd = io.open(dir.path(d, program), "r") + if fd then + fd:close() + return true, d + end + end + return false +end + +return win32 diff --git a/src/luarocks/fs/win32/tools.lua b/src/luarocks/fs/win32/tools.lua new file mode 100644 index 0000000..86cbb45 --- /dev/null +++ b/src/luarocks/fs/win32/tools.lua @@ -0,0 +1,330 @@ + +--- fs operations implemented with third-party tools for Windows platform abstractions. +-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities +-- used by this module. +local tools = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") + +local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end }) + +local dir_sep = package.config:sub(1, 1) + +--- Adds prefix to command to make it run from a directory. +-- @param directory string: Path to a directory. +-- @param cmd string: A command-line string. +-- @param exit_on_error bool: Exits immediately if entering the directory failed. +-- @return string: The command-line with prefix. +function tools.command_at(directory, cmd, exit_on_error) + local drive = directory:match("^([A-Za-z]:)") + local op = " & " + if exit_on_error then + op = " && " + end + local cmd_prefixed = "cd " .. fs.Q(directory) .. op .. cmd + if drive then + cmd_prefixed = drive .. " & " .. cmd_prefixed + end + return cmd_prefixed +end + +--- Create a directory if it does not already exist. +-- If any of the higher levels in the path name does not exist +-- too, they are created as well. +-- @param directory string: pathname of directory to create. +-- @return boolean: true on success, false on failure. +function tools.make_dir(directory) + assert(directory) + directory = dir.normalize(directory) + fs.execute_quiet(vars.MKDIR, directory) + if not fs.is_dir(directory) then + return false, "failed making directory "..directory + end + return true +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param directory string: pathname of directory to remove. +function tools.remove_dir_if_empty(directory) + assert(directory) + fs.execute_quiet(vars.RMDIR, directory) +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param directory string: pathname of directory to remove. +function tools.remove_dir_tree_if_empty(directory) + assert(directory) + while true do + fs.execute_quiet(vars.RMDIR, directory) + local parent = dir.dir_name(directory) + if parent ~= directory then + directory = parent + else + break + end + end +end + +--- Copy a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function tools.copy(src, dest) + assert(src and dest) + if dest:match("[/\\]$") then dest = dest:sub(1, -2) end + local ok = fs.execute(vars.CP, src, dest) + if ok then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Recursively copy the contents of a directory. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function tools.copy_contents(src, dest) + assert(src and dest) + if not fs.is_dir(src) then + return false, src .. " is not a directory" + end + if fs.make_dir(dest) and fs.execute_quiet(vars.CP, "-dR", src.."\\*.*", dest) then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Delete a file or a directory and all its contents. +-- For safety, this only accepts absolute paths. +-- @param arg string: Pathname of source +-- @return nil +function tools.delete(arg) + assert(arg) + assert(arg:match("^[a-zA-Z]?:?[\\/]")) + fs.execute_quiet("if exist "..fs.Q(arg.."\\*").." ( RMDIR /S /Q "..fs.Q(arg).." ) else ( DEL /Q /F "..fs.Q(arg).." )") +end + +--- Recursively scan the contents of a directory. +-- @param at string or nil: directory to scan (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. Paths are returned with forward slashes. +function tools.find(at) + assert(type(at) == "string" or not at) + if not at then + at = fs.current_dir() + end + if not fs.is_dir(at) then + return {} + end + local result = {} + local pipe = io.popen(fs.command_at(at, fs.quiet_stderr(vars.FIND), true)) + for file in pipe:lines() do + -- Windows find is a bit different + local first_two = file:sub(1,2) + if first_two == ".\\" or first_two == "./" then file=file:sub(3) end + if file ~= "." then + table.insert(result, (file:gsub("[\\/]", dir_sep))) + end + end + pipe:close() + return result +end + +--- Compress files in a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @param ... Filenames to be stored in the archive are given as +-- additional arguments. +-- @return boolean: true on success, nil and error message on failure. +function tools.zip(zipfile, ...) + if fs.execute_quiet(vars.SEVENZ.." -aoa a -tzip", zipfile, ...) then + return true + else + return nil, "failed compressing " .. zipfile + end +end + +--- Uncompress files from a .zip archive. +-- @param zipfile string: pathname of .zip archive to be extracted. +-- @return boolean: true on success, nil and error message on failure. +function tools.unzip(zipfile) + assert(zipfile) + if fs.execute_quiet(vars.SEVENZ.." -aoa x", zipfile) then + return true + else + return nil, "failed extracting " .. zipfile + end +end + +local function sevenz(default_ext, infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + + local dropext = infile:gsub("%."..default_ext.."$", "") + local outdir = dir.dir_name(dropext) + + infile = fs.absolute_name(infile) + + local cmdline = vars.SEVENZ.." -aoa -t* -o"..fs.Q(outdir).." x "..fs.Q(infile) + local ok, err = fs.execute_quiet(cmdline) + if not ok then + return nil, "failed extracting " .. infile + end + + if outfile then + outfile = fs.absolute_name(outfile) + dropext = fs.absolute_name(dropext) + ok, err = os.rename(dropext, outfile) + if not ok then + return nil, "failed creating new file " .. outfile + end + end + + return true +end + +--- Uncompresses a .gz file. +-- @param infile string: pathname of .gz file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.gunzip(infile, outfile) + return sevenz("gz", infile, outfile) +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.bunzip2(infile, outfile) + return sevenz("bz2", infile, outfile) +end + +--- Helper function for fs.set_permissions +-- @return table: an array of all system users +local function get_system_users() + local exclude = { + [""] = true, + ["Name"] = true, + ["\128\164\172\168\173\168\225\226\224\160\226\174\224"] = true, -- Administrator in cp866 + ["Administrator"] = true, + } + local result = {} + local fd = assert(io.popen("wmic UserAccount get name")) + for user in fd:lines() do + user = user:gsub("%s+$", "") + if not exclude[user] then + table.insert(result, user) + end + end + return result +end + +--- Set permissions for file or directory +-- @param filename string: filename whose permissions are to be modified +-- @param mode string ("read" or "exec"): permission to set +-- @param scope string ("user" or "all"): the user(s) to whom the permission applies +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message +function tools.set_permissions(filename, mode, scope) + assert(filename and mode and scope) + + if scope == "user" then + local perms + if mode == "read" then + perms = "(R,W,M)" + elseif mode == "exec" then + perms = "(F)" + end + + local ok + -- Take ownership of the given file + ok = fs.execute_quiet("takeown /f " .. fs.Q(filename)) + if not ok then + return false, "Could not take ownership of the given file" + end + local username = os.getenv('USERNAME') + -- Grant the current user the proper rights + ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant:r " .. fs.Q(username) .. ":" .. perms) + if not ok then + return false, "Failed setting permission " .. mode .. " for " .. scope + end + -- Finally, remove all the other users from the ACL in order to deny them access to the file + for _, user in pairs(get_system_users()) do + if username ~= user then + local ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /remove " .. fs.Q(user)) + if not ok then + return false, "Failed setting permission " .. mode .. " for " .. scope + end + end + end + elseif scope == "all" then + local my_perms, others_perms + if mode == "read" then + my_perms = "(R,W,M)" + others_perms = "(R)" + elseif mode == "exec" then + my_perms = "(F)" + others_perms = "(RX)" + end + + local ok + -- Grant permissions available to all users + ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant:r *S-1-1-0:" .. others_perms) + if not ok then + return false, "Failed setting permission " .. mode .. " for " .. scope + end + + -- Grant permissions available only to the current user + ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant \"%USERNAME%\":" .. my_perms) + + -- This may not be necessary if the above syntax is correct, + -- but I couldn't really test the extra quotes above, so if that + -- fails we try again with the syntax used in previous releases + -- just to be on the safe side + if not ok then + ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant %USERNAME%:" .. my_perms) + end + + if not ok then + return false, "Failed setting permission " .. mode .. " for " .. scope + end + end + + return true +end + +function tools.browser(url) + return fs.execute(cfg.web_browser..' "Starting docs..." '..fs.Q(url)) +end + +-- Set access and modification times for a file. +-- @param filename File to set access and modification times for. +-- @param time may be a string or number containing the format returned +-- by os.time, or a table ready to be processed via os.time; if +-- nil, current time is assumed. +function tools.set_time(filename, time) + return true -- FIXME +end + +function tools.lock_access(dirname) + -- NYI + return {} +end + +function tools.unlock_access(lock) + -- NYI +end + +return tools diff --git a/src/luarocks/fun.lua b/src/luarocks/fun.lua new file mode 100644 index 0000000..80bf7c2 --- /dev/null +++ b/src/luarocks/fun.lua @@ -0,0 +1,143 @@ + +--- A set of basic functional utilities +local fun = {} + +local unpack = table.unpack or unpack + +function fun.concat(xs, ys) + local rs = {} + local n = #xs + for i = 1, n do + rs[i] = xs[i] + end + for i = 1, #ys do + rs[i + n] = ys[i] + end + return rs +end + +function fun.contains(xs, v) + for _, x in ipairs(xs) do + if v == x then + return true + end + end + return false +end + +function fun.map(xs, f) + local rs = {} + for i = 1, #xs do + rs[i] = f(xs[i]) + end + return rs +end + +function fun.filter(xs, f) + local rs = {} + for i = 1, #xs do + local v = xs[i] + if f(v) then + rs[#rs+1] = v + end + end + return rs +end + +function fun.traverse(t, f) + return fun.map(t, function(x) + return type(x) == "table" and fun.traverse(x, f) or f(x) + end) +end + +function fun.reverse_in(t) + for i = 1, math.floor(#t/2) do + local m, n = i, #t - i + 1 + local a, b = t[m], t[n] + t[m] = b + t[n] = a + end + return t +end + +function fun.sort_in(t, f) + table.sort(t, f) + return t +end + +function fun.flip(f) + return function(a, b) + return f(b, a) + end +end + +function fun.find(xs, f) + if type(xs) == "function" then + for v in xs do + local x = f(v) + if x then + return x + end + end + elseif type(xs) == "table" then + for _, v in ipairs(xs) do + local x = f(v) + if x then + return x + end + end + end +end + +function fun.partial(f, ...) + local n = select("#", ...) + if n == 1 then + local a = ... + return function(...) + return f(a, ...) + end + elseif n == 2 then + local a, b = ... + return function(...) + return f(a, b, ...) + end + else + local pargs = { n = n, ... } + return function(...) + local m = select("#", ...) + local fargs = { ... } + local args = {} + for i = 1, n do + args[i] = pargs[i] + end + for i = 1, m do + args[i+n] = fargs[i] + end + return f(unpack(args, 1, n+m)) + end + end +end + +function fun.memoize(fn) + local memory = setmetatable({}, { __mode = "k" }) + local errors = setmetatable({}, { __mode = "k" }) + local NIL = {} + return function(arg) + if memory[arg] then + if memory[arg] == NIL then + return nil, errors[arg] + end + return memory[arg] + end + local ret1, ret2 = fn(arg) + if ret1 ~= nil then + memory[arg] = ret1 + else + memory[arg] = NIL + errors[arg] = ret2 + end + return ret1, ret2 + end +end + +return fun diff --git a/src/luarocks/loader.lua b/src/luarocks/loader.lua new file mode 100644 index 0000000..772fdfc --- /dev/null +++ b/src/luarocks/loader.lua @@ -0,0 +1,269 @@ +--- A module which installs a Lua package loader that is LuaRocks-aware. +-- This loader uses dependency information from the LuaRocks tree to load +-- correct versions of modules. It does this by constructing a "context" +-- table in the environment, which records which versions of packages were +-- used to load previous modules, so that the loader chooses versions +-- that are declared to be compatible with the ones loaded earlier. + +-- luacheck: globals luarocks + +local loaders = package.loaders or package.searchers +local require, ipairs, table, type, next, tostring, error = + require, ipairs, table, type, next, tostring, error +local unpack = unpack or table.unpack + +local loader = {} + +local is_clean = not package.loaded["luarocks.core.cfg"] + +-- This loader module depends only on core modules. +local cfg = require("luarocks.core.cfg") +local cfg_ok, err = cfg.init() +if cfg_ok then + cfg.init_package_paths() +end + +local path = require("luarocks.core.path") +local manif = require("luarocks.core.manif") +local vers = require("luarocks.core.vers") +local require = nil -- luacheck: ignore 411 +-------------------------------------------------------------------------------- + +-- Workaround for wrappers produced by older versions of LuaRocks +local temporary_global = false +local status, luarocks_value = pcall(function() return luarocks end) +if status and luarocks_value then + -- The site_config.lua file generated by old versions uses module(), + -- so it produces a global `luarocks` table. Since we have the table, + -- add the `loader` field to make the old wrappers happy. + luarocks.loader = loader +else + -- When a new version is installed on top of an old version, + -- site_config.lua may be replaced, and then it no longer creates + -- a global. + -- Detect when being called via -lluarocks.loader; this is + -- most likely a wrapper. + local info = debug and debug.getinfo(2, "nS") + if info and info.what == "C" and not info.name then + luarocks = { loader = loader } + temporary_global = true + -- For the other half of this hack, + -- see the next use of `temporary_global` below. + end +end + +loader.context = {} + +--- Process the dependencies of a package to determine its dependency +-- chain for loading modules. +-- @param name string: The name of an installed rock. +-- @param version string: The version of the rock, in string format +function loader.add_context(name, version) + -- assert(type(name) == "string") + -- assert(type(version) == "string") + + if temporary_global then + -- The first thing a wrapper does is to call add_context. + -- From here on, it's safe to clean the global environment. + luarocks = nil + temporary_global = false + end + + local tree_manifests = manif.load_rocks_tree_manifests() + if not tree_manifests then + return nil + end + + return manif.scan_dependencies(name, version, tree_manifests, loader.context) +end + +--- Internal sorting function. +-- @param a table: A provider table. +-- @param b table: Another provider table. +-- @return boolean: True if the version of a is greater than that of b. +local function sort_versions(a,b) + return a.version > b.version +end + +--- Request module to be loaded through other loaders, +-- once the proper name of the module has been determined. +-- For example, in case the module "socket.core" has been requested +-- to the LuaRocks loader and it determined based on context that +-- the version 2.0.2 needs to be loaded and it is not the current +-- version, the module requested for the other loaders will be +-- "socket.core_2_0_2". +-- @param module The module name requested by the user, such as "socket.core" +-- @param name The rock name, such as "luasocket" +-- @param version The rock version, such as "2.0.2-1" +-- @param module_name The actual module name, such as "socket.core" or "socket.core_2_0_2". +-- @return table or (nil, string): The module table as returned by some other loader, +-- or nil followed by an error message if no other loader managed to load the module. +local function call_other_loaders(module, name, version, module_name) + for _, a_loader in ipairs(loaders) do + if a_loader ~= loader.luarocks_loader then + local results = { a_loader(module_name) } + if type(results[1]) == "function" then + return unpack(results) + end + end + end + return "Failed loading module "..module.." in LuaRocks rock "..name.." "..version +end + +local function add_providers(providers, entries, tree, module, filter_file_name) + for i, entry in ipairs(entries) do + local name, version = entry:match("^([^/]*)/(.*)$") + local file_name = tree.manifest.repository[name][version][1].modules[module] + if type(file_name) ~= "string" then + error("Invalid data in manifest file for module "..tostring(module).." (invalid data for "..tostring(name).." "..tostring(version)..")") + end + file_name = filter_file_name(file_name, name, version, tree.tree, i) + if loader.context[name] == version then + return name, version, file_name + end + version = vers.parse_version(version) + table.insert(providers, {name = name, version = version, module_name = file_name, tree = tree}) + end +end + +--- Search for a module in the rocks trees +-- @param module string: module name (eg. "socket.core") +-- @param filter_file_name function(string, string, string, string, number): +-- a function that takes the module file name (eg "socket/core.so"), the rock name +-- (eg "luasocket"), the version (eg "2.0.2-1"), the path of the rocks tree +-- (eg "/usr/local"), and the numeric index of the matching entry, so the +-- filter function can know if the matching module was the first entry or not. +-- @return string, string, string, (string or table): +-- * name of the rock containing the module (eg. "luasocket") +-- * version of the rock (eg. "2.0.2-1") +-- * return value of filter_file_name +-- * tree of the module (string or table in `tree_manifests` format) +local function select_module(module, filter_file_name) + --assert(type(module) == "string") + --assert(type(filter_module_name) == "function") + + local tree_manifests = manif.load_rocks_tree_manifests() + if not tree_manifests then + return nil + end + + local providers = {} + local initmodule + for _, tree in ipairs(tree_manifests) do + local entries = tree.manifest.modules[module] + if entries then + local n, v, f = add_providers(providers, entries, tree, module, filter_file_name) + if n then + return n, v, f + end + else + initmodule = initmodule or module .. ".init" + entries = tree.manifest.modules[initmodule] + if entries then + local n, v, f = add_providers(providers, entries, tree, initmodule, filter_file_name) + if n then + return n, v, f + end + end + end + end + + if next(providers) then + table.sort(providers, sort_versions) + local first = providers[1] + return first.name, first.version.string, first.module_name, first.tree + end +end + +--- Search for a module +-- @param module string: module name (eg. "socket.core") +-- @return string, string, string, (string or table): +-- * name of the rock containing the module (eg. "luasocket") +-- * version of the rock (eg. "2.0.2-1") +-- * name of the module (eg. "socket.core", or "socket.core_2_0_2" if file is stored versioned). +-- * tree of the module (string or table in `tree_manifests` format) +local function pick_module(module) + return + select_module(module, function(file_name, name, version, tree, i) + if i > 1 then + file_name = path.versioned_name(file_name, "", name, version) + end + return path.path_to_module(file_name) + end) +end + +--- Return the pathname of the file that would be loaded for a module. +-- @param module string: module name (eg. "socket.core") +-- @param where string: places to look for the module. If `where` contains +-- "l", it will search using the LuaRocks loader; if it contains "p", +-- it will look in the filesystem using package.path and package.cpath. +-- You can use both at the same time. +-- @return If successful, it will return four values. +-- * If found using the LuaRocks loader, it will return: +-- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"), +-- * rock name +-- * rock version +-- * "l" to indicate the match comes from the loader. +-- * If found scanning package.path and package.cpath, it will return: +-- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"), +-- * "path" or "cpath" +-- * nil +-- * "p" to indicate the match comes from scanning package.path and cpath. +-- If unsuccessful, nothing is returned. +function loader.which(module, where) + where = where or "l" + if where:match("l") then + local rock_name, rock_version, file_name = select_module(module, path.which_i) + if rock_name then + local fd = io.open(file_name) + if fd then + fd:close() + return file_name, rock_name, rock_version, "l" + end + end + end + if where:match("p") then + local modpath = module:gsub("%.", "/") + for _, v in ipairs({"path", "cpath"}) do + for p in package[v]:gmatch("([^;]+)") do + local file_name = p:gsub("%?", modpath) -- luacheck: ignore 421 + local fd = io.open(file_name) + if fd then + fd:close() + return file_name, v, nil, "p" + end + end + end + end +end + +--- Package loader for LuaRocks support. +-- A module is searched in installed rocks that match the +-- current LuaRocks context. If module is not part of the +-- context, or if a context has not yet been set, the module +-- in the package with the highest version is used. +-- @param module string: The module name, like in plain require(). +-- @return table: The module table (typically), like in plain +-- require(). See require() +-- in the Lua reference manual for details. +function loader.luarocks_loader(module) + local name, version, module_name = pick_module(module) + if not name then + return "No LuaRocks module found for "..module + else + loader.add_context(name, version) + return call_other_loaders(module, name, version, module_name) + end +end + +table.insert(loaders, 1, loader.luarocks_loader) + +if is_clean then + for modname, _ in pairs(package.loaded) do + if modname:match("^luarocks%.") then + package.loaded[modname] = nil + end + end +end + +return loader diff --git a/src/luarocks/manif.lua b/src/luarocks/manif.lua new file mode 100644 index 0000000..a4ddda1 --- /dev/null +++ b/src/luarocks/manif.lua @@ -0,0 +1,225 @@ +--- Module for handling manifest files and tables. +-- Manifest files describe the contents of a LuaRocks tree or server. +-- They are loaded into manifest tables, which are then used for +-- performing searches, matching dependencies, etc. +local manif = {} + +local core = require("luarocks.core.manif") +local persist = require("luarocks.persist") +local fetch = require("luarocks.fetch") +local dir = require("luarocks.dir") +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local path = require("luarocks.path") +local util = require("luarocks.util") +local queries = require("luarocks.queries") +local type_manifest = require("luarocks.type.manifest") + +manif.cache_manifest = core.cache_manifest +manif.load_rocks_tree_manifests = core.load_rocks_tree_manifests +manif.scan_dependencies = core.scan_dependencies + +manif.rock_manifest_cache = {} + +local function check_manifest(repo_url, manifest, globals) + local ok, err = type_manifest.check(manifest, globals) + if not ok then + core.cache_manifest(repo_url, cfg.lua_version, nil) + return nil, "Error checking manifest: "..err, "type" + end + return manifest +end + +local postprocess_dependencies +do + local postprocess_check = setmetatable({}, { __mode = "k" }) + postprocess_dependencies = function(manifest) + if postprocess_check[manifest] then + return + end + if manifest.dependencies then + for name, versions in pairs(manifest.dependencies) do + for version, entries in pairs(versions) do + for k, v in pairs(entries) do + entries[k] = queries.from_persisted_table(v) + end + end + end + end + postprocess_check[manifest] = true + end +end + +function manif.load_rock_manifest(name, version, root) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local name_version = name.."/"..version + if manif.rock_manifest_cache[name_version] then + return manif.rock_manifest_cache[name_version].rock_manifest + end + local pathname = path.rock_manifest_file(name, version, root) + local rock_manifest = persist.load_into_table(pathname) + if not rock_manifest then + return nil, "rock_manifest file not found for "..name.." "..version.." - not a LuaRocks tree?" + end + manif.rock_manifest_cache[name_version] = rock_manifest + return rock_manifest.rock_manifest +end + +--- Load a local or remote manifest describing a repository. +-- All functions that use manifest tables assume they were obtained +-- through this function. +-- @param repo_url string: URL or pathname for the repository. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @param versioned_only boolean: If true, do not fall back to the main manifest +-- if a versioned manifest was not found. +-- @return table or (nil, string, [string]): A table representing the manifest, +-- or nil followed by an error message and an optional error code. +function manif.load_manifest(repo_url, lua_version, versioned_only) + assert(type(repo_url) == "string") + assert(type(lua_version) == "string" or not lua_version) + lua_version = lua_version or cfg.lua_version + + local cached_manifest = core.get_cached_manifest(repo_url, lua_version) + if cached_manifest then + postprocess_dependencies(cached_manifest) + return cached_manifest + end + + local filenames = { + "manifest-"..lua_version..".zip", + "manifest-"..lua_version, + not versioned_only and "manifest" or nil, + } + + local protocol, repodir = dir.split_url(repo_url) + local pathname, from_cache + if protocol == "file" then + for _, filename in ipairs(filenames) do + pathname = dir.path(repodir, filename) + if fs.exists(pathname) then + break + end + end + else + local err, errcode + for _, filename in ipairs(filenames) do + pathname, err, errcode, from_cache = fetch.fetch_caching(dir.path(repo_url, filename), "no_mirror") + if pathname then + break + end + end + if not pathname then + return nil, err, errcode + end + end + if pathname:match(".*%.zip$") then + pathname = fs.absolute_name(pathname) + local nozip = pathname:match("(.*)%.zip$") + if not from_cache then + local dirname = dir.dir_name(pathname) + fs.change_dir(dirname) + fs.delete(nozip) + local ok, err = fs.unzip(pathname) + fs.pop_dir() + if not ok then + fs.delete(pathname) + fs.delete(pathname..".timestamp") + return nil, "Failed extracting manifest file: " .. err + end + end + pathname = nozip + end + local manifest, err, errcode = core.manifest_loader(pathname, repo_url, lua_version) + if not manifest then + return nil, err, errcode + end + + postprocess_dependencies(manifest) + return check_manifest(repo_url, manifest, err) +end + +--- Get type and name of an item (a module or a command) provided by a file. +-- @param deploy_type string: rock manifest subtree the file comes from ("bin", "lua", or "lib"). +-- @param file_path string: path to the file relatively to deploy_type subdirectory. +-- @return (string, string): item type ("module" or "command") and name. +function manif.get_provided_item(deploy_type, file_path) + assert(type(deploy_type) == "string") + assert(type(file_path) == "string") + local item_type = deploy_type == "bin" and "command" or "module" + local item_name = item_type == "command" and file_path or path.path_to_module(file_path) + return item_type, item_name +end + +local function get_providers(item_type, item_name, repo) + assert(type(item_type) == "string") + assert(type(item_name) == "string") + local rocks_dir = path.rocks_dir(repo or cfg.root_dir) + local manifest = manif.load_manifest(rocks_dir) + return manifest and manifest[item_type .. "s"][item_name] +end + +--- Given a name of a module or a command, figure out which rock name and version +-- correspond to it in the rock tree manifest. +-- @param item_type string: "module" or "command". +-- @param item_name string: module or command name. +-- @param root string or nil: A local root dir for a rocks tree. If not given, the default is used. +-- @return (string, string) or nil: name and version of the provider rock or nil if there +-- is no provider. +function manif.get_current_provider(item_type, item_name, repo) + local providers = get_providers(item_type, item_name, repo) + if providers then + return providers[1]:match("([^/]*)/([^/]*)") + end +end + +function manif.get_next_provider(item_type, item_name, repo) + local providers = get_providers(item_type, item_name, repo) + if providers and providers[2] then + return providers[2]:match("([^/]*)/([^/]*)") + end +end + +--- Get all versions of a package listed in a manifest file. +-- @param name string: a package name. +-- @param deps_mode string: "one", to use only the currently +-- configured tree; "order" to select trees based on order +-- (use the current tree and all trees below it on the list) +-- or "all", to use all trees. +-- @return table: An array of strings listing installed +-- versions of a package, and a table indicating where they are found. +function manif.get_versions(dep, deps_mode) + assert(type(dep) == "table") + assert(type(deps_mode) == "string") + + local name = dep.name + local namespace = dep.namespace + + local version_set = {} + path.map_trees(deps_mode, function(tree) + local manifest = manif.load_manifest(path.rocks_dir(tree)) + + if manifest and manifest.repository[name] then + for version in pairs(manifest.repository[name]) do + if dep.namespace then + local ns_file = path.rock_namespace_file(name, version, tree) + local fd = io.open(ns_file, "r") + if fd then + local ns = fd:read("*a") + fd:close() + if ns == namespace then + version_set[version] = tree + end + end + else + version_set[version] = tree + end + end + end + end) + + return util.keys(version_set), version_set +end + +return manif diff --git a/src/luarocks/manif/writer.lua b/src/luarocks/manif/writer.lua new file mode 100644 index 0000000..36f5f57 --- /dev/null +++ b/src/luarocks/manif/writer.lua @@ -0,0 +1,498 @@ + +local writer = {} + +local cfg = require("luarocks.core.cfg") +local search = require("luarocks.search") +local repos = require("luarocks.repos") +local deps = require("luarocks.deps") +local vers = require("luarocks.core.vers") +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local fetch = require("luarocks.fetch") +local path = require("luarocks.path") +local persist = require("luarocks.persist") +local manif = require("luarocks.manif") +local queries = require("luarocks.queries") + +--- Update storage table to account for items provided by a package. +-- @param storage table: a table storing items in the following format: +-- keys are item names and values are arrays of packages providing each item, +-- where a package is specified as string `name/version`. +-- @param items table: a table mapping item names to paths. +-- @param name string: package name. +-- @param version string: package version. +local function store_package_items(storage, name, version, items) + assert(type(storage) == "table") + assert(type(items) == "table") + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local package_identifier = name.."/"..version + + for item_name, path in pairs(items) do -- luacheck: ignore 431 + if not storage[item_name] then + storage[item_name] = {} + end + + table.insert(storage[item_name], package_identifier) + end +end + +--- Update storage table removing items provided by a package. +-- @param storage table: a table storing items in the following format: +-- keys are item names and values are arrays of packages providing each item, +-- where a package is specified as string `name/version`. +-- @param items table: a table mapping item names to paths. +-- @param name string: package name. +-- @param version string: package version. +local function remove_package_items(storage, name, version, items) + assert(type(storage) == "table") + assert(type(items) == "table") + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local package_identifier = name.."/"..version + + for item_name, path in pairs(items) do -- luacheck: ignore 431 + local key = item_name + local all_identifiers = storage[key] + if not all_identifiers then + key = key .. ".init" + all_identifiers = storage[key] + end + + if all_identifiers then + for i, identifier in ipairs(all_identifiers) do + if identifier == package_identifier then + table.remove(all_identifiers, i) + break + end + end + + if #all_identifiers == 0 then + storage[key] = nil + end + else + util.warning("Cannot find entry for " .. item_name .. " in manifest -- corrupted manifest?") + end + end +end + +--- Process the dependencies of a manifest table to determine its dependency +-- chains for loading modules. The manifest dependencies information is filled +-- and any dependency inconsistencies or missing dependencies are reported to +-- standard error. +-- @param manifest table: a manifest table. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for no trees. +local function update_dependencies(manifest, deps_mode) + assert(type(manifest) == "table") + assert(type(deps_mode) == "string") + + if not manifest.dependencies then manifest.dependencies = {} end + local mdeps = manifest.dependencies + + for pkg, versions in pairs(manifest.repository) do + for version, repositories in pairs(versions) do + for _, repo in ipairs(repositories) do + if repo.arch == "installed" then + local rd = {} + repo.dependencies = rd + deps.scan_deps(rd, mdeps, pkg, version, deps_mode) + rd[pkg] = nil + end + end + end + end +end + + + +--- Sort function for ordering rock identifiers in a manifest's +-- modules table. Rocks are ordered alphabetically by name, and then +-- by version which greater first. +-- @param a string: Version to compare. +-- @param b string: Version to compare. +-- @return boolean: The comparison result, according to the +-- rule outlined above. +local function sort_pkgs(a, b) + assert(type(a) == "string") + assert(type(b) == "string") + + local na, va = a:match("(.*)/(.*)$") + local nb, vb = b:match("(.*)/(.*)$") + + return (na == nb) and vers.compare_versions(va, vb) or na < nb +end + +--- Sort items of a package matching table by version number (higher versions first). +-- @param tbl table: the package matching table: keys should be strings +-- and values arrays of strings with packages names in "name/version" format. +local function sort_package_matching_table(tbl) + assert(type(tbl) == "table") + + if next(tbl) then + for item, pkgs in pairs(tbl) do + if #pkgs > 1 then + table.sort(pkgs, sort_pkgs) + -- Remove duplicates from the sorted array. + local prev = nil + local i = 1 + while pkgs[i] do + local curr = pkgs[i] + if curr == prev then + table.remove(pkgs, i) + else + prev = curr + i = i + 1 + end + end + end + end + end +end + +--- Filter manifest table by Lua version, removing rockspecs whose Lua version +-- does not match. +-- @param manifest table: a manifest table. +-- @param lua_version string or nil: filter by Lua version +-- @param repodir string: directory of repository being scanned +-- @param cache table: temporary rockspec cache table +local function filter_by_lua_version(manifest, lua_version, repodir, cache) + assert(type(manifest) == "table") + assert(type(repodir) == "string") + assert((not cache) or type(cache) == "table") + + cache = cache or {} + lua_version = vers.parse_version(lua_version) + for pkg, versions in pairs(manifest.repository) do + local to_remove = {} + for version, repositories in pairs(versions) do + for _, repo in ipairs(repositories) do + if repo.arch == "rockspec" then + local pathname = dir.path(repodir, pkg.."-"..version..".rockspec") + local rockspec, err = cache[pathname] + if not rockspec then + rockspec, err = fetch.load_local_rockspec(pathname, true) + end + if rockspec then + cache[pathname] = rockspec + for _, dep in ipairs(rockspec.dependencies) do + if dep.name == "lua" then + if not vers.match_constraints(lua_version, dep.constraints) then + table.insert(to_remove, version) + end + break + end + end + else + util.printerr("Error loading rockspec for "..pkg.." "..version..": "..err) + end + end + end + end + if next(to_remove) then + for _, incompat in ipairs(to_remove) do + versions[incompat] = nil + end + if not next(versions) then + manifest.repository[pkg] = nil + end + end + end +end + +--- Store search results in a manifest table. +-- @param results table: The search results as returned by search.disk_search. +-- @param manifest table: A manifest table (must contain repository, modules, commands tables). +-- It will be altered to include the search results. +-- @return boolean or (nil, string): true in case of success, or nil followed by an error message. +local function store_results(results, manifest) + assert(type(results) == "table") + assert(type(manifest) == "table") + + for name, versions in pairs(results) do + local pkgtable = manifest.repository[name] or {} + for version, entries in pairs(versions) do + local versiontable = {} + for _, entry in ipairs(entries) do + local entrytable = {} + entrytable.arch = entry.arch + if entry.arch == "installed" then + local rock_manifest, err = manif.load_rock_manifest(name, version) + if not rock_manifest then return nil, err end + + entrytable.modules = repos.package_modules(name, version) + store_package_items(manifest.modules, name, version, entrytable.modules) + entrytable.commands = repos.package_commands(name, version) + store_package_items(manifest.commands, name, version, entrytable.commands) + end + table.insert(versiontable, entrytable) + end + pkgtable[version] = versiontable + end + manifest.repository[name] = pkgtable + end + sort_package_matching_table(manifest.modules) + sort_package_matching_table(manifest.commands) + return true +end + +--- Commit a table to disk in given local path. +-- @param where string: The directory where the table should be saved. +-- @param name string: The filename. +-- @param tbl table: The table to be saved. +-- @return boolean or (nil, string): true if successful, or nil and a +-- message in case of errors. +local function save_table(where, name, tbl) + assert(type(where) == "string") + assert(type(name) == "string" and not name:match("/")) + assert(type(tbl) == "table") + + local filename = dir.path(where, name) + local ok, err = persist.save_from_table(filename..".tmp", tbl) + if ok then + ok, err = fs.replace_file(filename, filename..".tmp") + end + return ok, err +end + +function writer.make_rock_manifest(name, version) + local install_dir = path.install_dir(name, version) + local tree = {} + for _, file in ipairs(fs.find(install_dir)) do + local full_path = dir.path(install_dir, file) + local walk = tree + local last + local last_name + for filename in file:gmatch("[^\\/]+") do + local next = walk[filename] + if not next then + next = {} + walk[filename] = next + end + last = walk + last_name = filename + walk = next + end + if fs.is_file(full_path) then + local sum, err = fs.get_md5(full_path) + if not sum then + return nil, "Failed producing checksum: "..tostring(err) + end + last[last_name] = sum + end + end + local rock_manifest = { rock_manifest=tree } + manif.rock_manifest_cache[name.."/"..version] = rock_manifest + save_table(install_dir, "rock_manifest", rock_manifest ) + return true +end + +-- Writes a 'rock_namespace' file in a locally installed rock directory. +-- @param name string: the rock name, without a namespace +-- @param version string: the rock version +-- @param namespace string?: the namespace +-- @return true if successful (or unnecessary, if there is no namespace), +-- or nil and an error message. +function writer.make_namespace_file(name, version, namespace) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(namespace) == "string" or not namespace) + if not namespace then + return true + end + local fd, err = io.open(path.rock_namespace_file(name, version), "w") + if not fd then + return nil, err + end + local ok, err = fd:write(namespace) + if not ok then + return nil, err + end + fd:close() + return true +end + +--- Scan a LuaRocks repository and output a manifest file. +-- A file called 'manifest' will be written in the root of the given +-- repository directory. +-- @param repo A local repository directory. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for the default dependency mode from the configuration. +-- @param remote boolean: 'true' if making a manifest for a rocks server. +-- @return boolean or (nil, string): True if manifest was generated, +-- or nil and an error message. +function writer.make_manifest(repo, deps_mode, remote) + assert(type(repo) == "string") + assert(type(deps_mode) == "string") + + if deps_mode == "none" then deps_mode = cfg.deps_mode end + + if not fs.is_dir(repo) then + return nil, "Cannot access repository at "..repo + end + + local query = queries.all("any") + local results = search.disk_search(repo, query) + local manifest = { repository = {}, modules = {}, commands = {} } + + manif.cache_manifest(repo, nil, manifest) + + local ok, err = store_results(results, manifest) + if not ok then return nil, err end + + if remote then + local cache = {} + for luaver in util.lua_versions() do + local vmanifest = { repository = {}, modules = {}, commands = {} } + local ok, err = store_results(results, vmanifest) + filter_by_lua_version(vmanifest, luaver, repo, cache) + if not cfg.no_manifest then + save_table(repo, "manifest-"..luaver, vmanifest) + end + end + else + update_dependencies(manifest, deps_mode) + end + + if cfg.no_manifest then + -- We want to have cache updated; but exit before save_table is called + return true + end + return save_table(repo, "manifest", manifest) +end + +--- Update manifest file for a local repository +-- adding information about a version of a package installed in that repository. +-- @param name string: Name of a package from the repository. +-- @param version string: Version of a package from the repository. +-- @param repo string or nil: Pathname of a local repository. If not given, +-- the default local repository is used. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for using the default dependency mode from the configuration. +-- @return boolean or (nil, string): True if manifest was updated successfully, +-- or nil and an error message. +function writer.add_to_manifest(name, version, repo, deps_mode) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + local rocks_dir = path.rocks_dir(repo or cfg.root_dir) + assert(type(deps_mode) == "string") + + if deps_mode == "none" then deps_mode = cfg.deps_mode end + + local manifest, err = manif.load_manifest(rocks_dir) + if not manifest then + util.printerr("No existing manifest. Attempting to rebuild...") + -- Manifest built by `writer.make_manifest` should already + -- include information about given name and version, + -- no need to update it. + return writer.make_manifest(rocks_dir, deps_mode) + end + + local results = {[name] = {[version] = {{arch = "installed", repo = rocks_dir}}}} + + local ok, err = store_results(results, manifest) + if not ok then return nil, err end + + update_dependencies(manifest, deps_mode) + + if cfg.no_manifest then + return true + end + return save_table(rocks_dir, "manifest", manifest) +end + +--- Update manifest file for a local repository +-- removing information about a version of a package. +-- @param name string: Name of a package removed from the repository. +-- @param version string: Version of a package removed from the repository. +-- @param repo string or nil: Pathname of a local repository. If not given, +-- the default local repository is used. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for using the default dependency mode from the configuration. +-- @return boolean or (nil, string): True if manifest was updated successfully, +-- or nil and an error message. +function writer.remove_from_manifest(name, version, repo, deps_mode) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + local rocks_dir = path.rocks_dir(repo or cfg.root_dir) + assert(type(deps_mode) == "string") + + if deps_mode == "none" then deps_mode = cfg.deps_mode end + + local manifest, err = manif.load_manifest(rocks_dir) + if not manifest then + util.printerr("No existing manifest. Attempting to rebuild...") + -- Manifest built by `writer.make_manifest` should already + -- include up-to-date information, no need to update it. + return writer.make_manifest(rocks_dir, deps_mode) + end + + local package_entry = manifest.repository[name] + if package_entry == nil or package_entry[version] == nil then + -- entry is already missing from repository, no need to do anything + return true + end + + local version_entry = package_entry[version][1] + if not version_entry then + -- manifest looks corrupted, rebuild + return writer.make_manifest(rocks_dir, deps_mode) + end + + remove_package_items(manifest.modules, name, version, version_entry.modules) + remove_package_items(manifest.commands, name, version, version_entry.commands) + + package_entry[version] = nil + manifest.dependencies[name][version] = nil + + if not next(package_entry) then + -- No more versions of this package. + manifest.repository[name] = nil + manifest.dependencies[name] = nil + end + + update_dependencies(manifest, deps_mode) + + if cfg.no_manifest then + return true + end + return save_table(rocks_dir, "manifest", manifest) +end + +--- Report missing dependencies for all rocks installed in a repository. +-- @param repo string or nil: Pathname of a local repository. If not given, +-- the default local repository is used. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for using the default dependency mode from the configuration. +function writer.check_dependencies(repo, deps_mode) + local rocks_dir = path.rocks_dir(repo or cfg.root_dir) + assert(type(deps_mode) == "string") + if deps_mode == "none" then deps_mode = cfg.deps_mode end + + local manifest = manif.load_manifest(rocks_dir) + if not manifest then + return + end + + for name, versions in util.sortedpairs(manifest.repository) do + for version, version_entries in util.sortedpairs(versions, vers.compare_versions) do + for _, entry in ipairs(version_entries) do + if entry.arch == "installed" then + if manifest.dependencies[name] and manifest.dependencies[name][version] then + deps.report_missing_dependencies(name, version, manifest.dependencies[name][version], deps_mode, util.get_rocks_provided()) + end + end + end + end + end +end + +return writer diff --git a/src/luarocks/pack.lua b/src/luarocks/pack.lua new file mode 100644 index 0000000..731f49d --- /dev/null +++ b/src/luarocks/pack.lua @@ -0,0 +1,184 @@ + +-- Create rock files, packing sources or binaries. +local pack = {} + +local unpack = unpack or table.unpack + +local queries = require("luarocks.queries") +local path = require("luarocks.path") +local repos = require("luarocks.repos") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local manif = require("luarocks.manif") +local search = require("luarocks.search") +local signing = require("luarocks.signing") + +--- Create a source rock. +-- Packages a rockspec and its required source files in a rock +-- file with the .src.rock extension, which can later be built and +-- installed with the "build" command. +-- @param rockspec_file string: An URL or pathname for a rockspec file. +-- @return string or (nil, string): The filename of the resulting +-- .src.rock file; or nil and an error message. +function pack.pack_source_rock(rockspec_file) + assert(type(rockspec_file) == "string") + + local rockspec, err = fetch.load_rockspec(rockspec_file) + if err then + return nil, "Error loading rockspec: "..err + end + rockspec_file = rockspec.local_abs_filename + + local name_version = rockspec.name .. "-" .. rockspec.version + local rock_file = fs.absolute_name(name_version .. ".src.rock") + + local temp_dir, err = fs.make_temp_dir("pack-"..name_version) + if not temp_dir then + return nil, "Failed creating temporary directory: "..err + end + util.schedule_function(fs.delete, temp_dir) + + local source_file, source_dir = fetch.fetch_sources(rockspec, true, temp_dir) + if not source_file then + return nil, source_dir + end + local ok, err = fs.change_dir(source_dir) + if not ok then return nil, err end + + fs.delete(rock_file) + fs.copy(rockspec_file, source_dir, "read") + ok, err = fs.zip(rock_file, dir.base_name(rockspec_file), dir.base_name(source_file)) + if not ok then + return nil, "Failed packing "..rock_file.." - "..err + end + fs.pop_dir() + + return rock_file +end + +local function copy_back_files(name, version, file_tree, deploy_dir, pack_dir, perms) + local ok, err = fs.make_dir(pack_dir) + if not ok then return nil, err end + for file, sub in pairs(file_tree) do + local source = dir.path(deploy_dir, file) + local target = dir.path(pack_dir, file) + if type(sub) == "table" then + local ok, err = copy_back_files(name, version, sub, source, target) + if not ok then return nil, err end + else + local versioned = path.versioned_name(source, deploy_dir, name, version) + if fs.exists(versioned) then + fs.copy(versioned, target, perms) + else + fs.copy(source, target, perms) + end + end + end + return true +end + +-- @param name string: Name of package to pack. +-- @param version string or nil: A version number may also be passed. +-- @param tree string or nil: An optional tree to pick the package from. +-- @return string or (nil, string): The filename of the resulting +-- .src.rock file; or nil and an error message. +function pack.pack_installed_rock(query, tree) + + local name, version, repo, repo_url = search.pick_installed_rock(query, tree) + if not name then + return nil, version + end + + local root = path.root_from_rocks_dir(repo_url) + local prefix = path.install_dir(name, version, root) + if not fs.exists(prefix) then + return nil, "'"..name.." "..version.."' does not seem to be an installed rock." + end + + local rock_manifest, err = manif.load_rock_manifest(name, version, root) + if not rock_manifest then return nil, err end + + local name_version = name .. "-" .. version + local rock_file = fs.absolute_name(name_version .. "."..cfg.arch..".rock") + + local temp_dir = fs.make_temp_dir("pack") + fs.copy_contents(prefix, temp_dir) + + local is_binary = false + if rock_manifest.lib then + local ok, err = copy_back_files(name, version, rock_manifest.lib, path.deploy_lib_dir(repo), dir.path(temp_dir, "lib"), "exec") + if not ok then return nil, "Failed copying back files: " .. err end + is_binary = true + end + if rock_manifest.lua then + local ok, err = copy_back_files(name, version, rock_manifest.lua, path.deploy_lua_dir(repo), dir.path(temp_dir, "lua"), "read") + if not ok then return nil, "Failed copying back files: " .. err end + end + + local ok, err = fs.change_dir(temp_dir) + if not ok then return nil, err end + if not is_binary and not repos.has_binaries(name, version) then + rock_file = rock_file:gsub("%."..cfg.arch:gsub("%-","%%-").."%.", ".all.") + end + fs.delete(rock_file) + ok, err = fs.zip(rock_file, unpack(fs.list_dir())) + if not ok then + return nil, "Failed packing " .. rock_file .. " - " .. err + end + fs.pop_dir() + fs.delete(temp_dir) + return rock_file +end + +function pack.report_and_sign_local_file(file, err, sign) + if err then + return nil, err + end + local sigfile + if sign then + sigfile, err = signing.sign_file(file) + util.printout() + end + util.printout("Packed: "..file) + if sigfile then + util.printout("Signature stored in: "..sigfile) + end + if err then + return nil, err + end + return true +end + +function pack.pack_binary_rock(name, namespace, version, sign, cmd) + + -- The --pack-binary-rock option for "luarocks build" basically performs + -- "luarocks build" on a temporary tree and then "luarocks pack". The + -- alternative would require refactoring parts of luarocks.build and + -- luarocks.pack, which would save a few file operations: the idea would be + -- to shave off the final deploy steps from the build phase and the initial + -- collect steps from the pack phase. + + local temp_dir, err = fs.make_temp_dir("luarocks-build-pack-"..dir.base_name(name)) + if not temp_dir then + return nil, "Failed creating temporary directory: "..err + end + util.schedule_function(fs.delete, temp_dir) + + path.use_tree(temp_dir) + local ok, err = cmd() + if not ok then + return nil, err + end + local rname, rversion = path.parse_name(name) + if not rname then + rname, rversion = name, version + end + local query = queries.new(rname, namespace, rversion) + local file, err = pack.pack_installed_rock(query, temp_dir) + return pack.report_and_sign_local_file(file, err, sign) +end + +return pack diff --git a/src/luarocks/path.lua b/src/luarocks/path.lua new file mode 100644 index 0000000..19657c8 --- /dev/null +++ b/src/luarocks/path.lua @@ -0,0 +1,263 @@ + +--- LuaRocks-specific path handling functions. +-- All paths are configured in this module, making it a single +-- point where the layout of the local installation is defined in LuaRocks. +local path = {} + +local core = require("luarocks.core.path") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") + +path.rocks_dir = core.rocks_dir +path.versioned_name = core.versioned_name +path.path_to_module = core.path_to_module +path.deploy_lua_dir = core.deploy_lua_dir +path.deploy_lib_dir = core.deploy_lib_dir +path.map_trees = core.map_trees +path.rocks_tree_to_string = core.rocks_tree_to_string + +--- Infer rockspec filename from a rock filename. +-- @param rock_name string: Pathname of a rock file. +-- @return string: Filename of the rockspec, without path. +function path.rockspec_name_from_rock(rock_name) + assert(type(rock_name) == "string") + local base_name = dir.base_name(rock_name) + return base_name:match("(.*)%.[^.]*.rock") .. ".rockspec" +end + +function path.root_from_rocks_dir(rocks_dir) + assert(type(rocks_dir) == "string") + return rocks_dir:match("(.*)" .. util.matchquote(cfg.rocks_subdir) .. ".*$") +end + +function path.root_dir(tree) + if type(tree) == "string" then + return tree + else + assert(type(tree) == "table") + return tree.root + end +end + +function path.deploy_bin_dir(tree) + return dir.path(path.root_dir(tree), "bin") +end + +function path.manifest_file(tree) + return dir.path(path.rocks_dir(tree), "manifest") +end + +--- Get the directory for all versions of a package in a tree. +-- @param name string: The package name. +-- @return string: The resulting path -- does not guarantee that +-- @param tree string or nil: If given, specifies the local tree to use. +-- the package (and by extension, the path) exists. +function path.versions_dir(name, tree) + assert(type(name) == "string" and not name:match("/")) + return dir.path(path.rocks_dir(tree), name) +end + +--- Get the local installation directory (prefix) for a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.install_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version) +end + +--- Get the local filename of the rockspec of an installed rock. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the file) exists. +function path.rockspec_file(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, name.."-"..version..".rockspec") +end + +--- Get the local filename of the rock_manifest file of an installed rock. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the file) exists. +function path.rock_manifest_file(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "rock_manifest") +end + +--- Get the local filename of the rock_namespace file of an installed rock. +-- @param name string: The package name (without a namespace). +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the file) exists. +function path.rock_namespace_file(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "rock_namespace") +end + +--- Get the local installation directory for C libraries of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.lib_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "lib") +end + +--- Get the local installation directory for Lua modules of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.lua_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "lua") +end + +--- Get the local installation directory for documentation of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.doc_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "doc") +end + +--- Get the local installation directory for configuration files of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.conf_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "conf") +end + +--- Get the local installation directory for command-line scripts +-- of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.bin_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "bin") +end + +--- Extract name, version and arch of a rock filename, +-- or name, version and "rockspec" from a rockspec name. +-- @param file_name string: pathname of a rock or rockspec +-- @return (string, string, string) or nil: name, version and arch +-- or nil if name could not be parsed +function path.parse_name(file_name) + assert(type(file_name) == "string") + if file_name:match("%.rock$") then + return dir.base_name(file_name):match("(.*)-([^-]+-%d+)%.([^.]+)%.rock$") + else + return dir.base_name(file_name):match("(.*)-([^-]+-%d+)%.(rockspec)") + end +end + +--- Make a rockspec or rock URL. +-- @param pathname string: Base URL or pathname. +-- @param name string: Package name. +-- @param version string: Package version. +-- @param arch string: Architecture identifier, or "rockspec" or "installed". +-- @return string: A URL or pathname following LuaRocks naming conventions. +function path.make_url(pathname, name, version, arch) + assert(type(pathname) == "string") + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(arch) == "string") + + local filename = name.."-"..version + if arch == "installed" then + filename = dir.path(name, version, filename..".rockspec") + elseif arch == "rockspec" then + filename = filename..".rockspec" + else + filename = filename.."."..arch..".rock" + end + return dir.path(pathname, filename) +end + +--- Obtain the directory name where a module should be stored. +-- For example, on Unix, "foo.bar.baz" will return "foo/bar". +-- @param mod string: A module name in Lua dot-separated format. +-- @return string: A directory name using the platform's separator. +function path.module_to_path(mod) + assert(type(mod) == "string") + return (mod:gsub("[^.]*$", ""):gsub("%.", "/")) +end + +function path.use_tree(tree) + cfg.root_dir = tree + cfg.rocks_dir = path.rocks_dir(tree) + cfg.deploy_bin_dir = path.deploy_bin_dir(tree) + cfg.deploy_lua_dir = path.deploy_lua_dir(tree) + cfg.deploy_lib_dir = path.deploy_lib_dir(tree) +end + +function path.add_to_package_paths(tree) + package.path = dir.path(path.deploy_lua_dir(tree), "?.lua") .. ";" + .. dir.path(path.deploy_lua_dir(tree), "?/init.lua") .. ";" + .. package.path + package.cpath = dir.path(path.deploy_lib_dir(tree), "?." .. cfg.lib_extension) .. ";" + .. package.cpath +end + +--- Get the namespace of a locally-installed rock, if any. +-- @param name string: The rock name, without a namespace. +-- @param version string: The rock version. +-- @param tree string: The local tree to use. +-- @return string?: The namespace if it exists, or nil. +function path.read_namespace(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(tree) == "string") + + local namespace + local fd = io.open(path.rock_namespace_file(name, version, tree), "r") + if fd then + namespace = fd:read("*a") + fd:close() + end + return namespace +end + +function path.package_paths(deps_mode) + local lpaths = {} + local lcpaths = {} + path.map_trees(deps_mode, function(tree) + local root = path.root_dir(tree) + table.insert(lpaths, dir.path(root, cfg.lua_modules_path, "?.lua")) + table.insert(lpaths, dir.path(root, cfg.lua_modules_path, "?/init.lua")) + table.insert(lcpaths, dir.path(root, cfg.lib_modules_path, "?." .. cfg.lib_extension)) + end) + return table.concat(lpaths, ";"), table.concat(lcpaths, ";") +end + +return path diff --git a/src/luarocks/persist.lua b/src/luarocks/persist.lua new file mode 100644 index 0000000..4dcd930 --- /dev/null +++ b/src/luarocks/persist.lua @@ -0,0 +1,259 @@ + +--- Utility module for loading files into tables and +-- saving tables into files. +local persist = {} + +local core = require("luarocks.core.persist") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local fs = require("luarocks.fs") + +persist.run_file = core.run_file +persist.load_into_table = core.load_into_table + +local write_table + +--- Write a value as Lua code. +-- This function handles only numbers and strings, invoking write_table +-- to write tables. +-- @param out table or userdata: a writer object supporting :write() method. +-- @param v: the value to be written. +-- @param level number: the indentation level +-- @param sub_order table: optional prioritization table +-- @see write_table +function persist.write_value(out, v, level, sub_order) + if type(v) == "table" then + level = level or 0 + write_table(out, v, level + 1, sub_order) + elseif type(v) == "string" then + if v:match("[\r\n]") then + local open, close = "[[", "]]" + local equals = 0 + local v_with_bracket = v.."]" + while v_with_bracket:find(close, 1, true) do + equals = equals + 1 + local eqs = ("="):rep(equals) + open, close = "["..eqs.."[", "]"..eqs.."]" + end + out:write(open.."\n"..v..close) + else + out:write(("%q"):format(v)) + end + else + out:write(tostring(v)) + end +end + +local is_valid_plain_key +do + local keywords = { + ["and"] = true, + ["break"] = true, + ["do"] = true, + ["else"] = true, + ["elseif"] = true, + ["end"] = true, + ["false"] = true, + ["for"] = true, + ["function"] = true, + ["goto"] = true, + ["if"] = true, + ["in"] = true, + ["local"] = true, + ["nil"] = true, + ["not"] = true, + ["or"] = true, + ["repeat"] = true, + ["return"] = true, + ["then"] = true, + ["true"] = true, + ["until"] = true, + ["while"] = true, + } + function is_valid_plain_key(k) + return type(k) == "string" + and k:match("^[a-zA-Z_][a-zA-Z0-9_]*$") + and not keywords[k] + end +end + +local function write_table_key_assignment(out, k, level) + if is_valid_plain_key(k) then + out:write(k) + else + out:write("[") + persist.write_value(out, k, level) + out:write("]") + end + + out:write(" = ") +end + +--- Write a table as Lua code in curly brackets notation to a writer object. +-- Only numbers, strings and tables (containing numbers, strings +-- or other recursively processed tables) are supported. +-- @param out table or userdata: a writer object supporting :write() method. +-- @param tbl table: the table to be written. +-- @param level number: the indentation level +-- @param field_order table: optional prioritization table +write_table = function(out, tbl, level, field_order) + out:write("{") + local sep = "\n" + local indentation = " " + local indent = true + local i = 1 + for k, v, sub_order in util.sortedpairs(tbl, field_order) do + out:write(sep) + if indent then + for _ = 1, level do out:write(indentation) end + end + + if k == i then + i = i + 1 + else + write_table_key_assignment(out, k, level) + end + + persist.write_value(out, v, level, sub_order) + if type(v) == "number" then + sep = ", " + indent = false + else + sep = ",\n" + indent = true + end + end + if sep ~= "\n" then + out:write("\n") + for _ = 1, level - 1 do out:write(indentation) end + end + out:write("}") +end + +--- Write a table as series of assignments to a writer object. +-- @param out table or userdata: a writer object supporting :write() method. +-- @param tbl table: the table to be written. +-- @param field_order table: optional prioritization table +-- @return true if successful; nil and error message if failed. +local function write_table_as_assignments(out, tbl, field_order) + for k, v, sub_order in util.sortedpairs(tbl, field_order) do + if not is_valid_plain_key(k) then + return nil, "cannot store '"..tostring(k).."' as a plain key." + end + out:write(k.." = ") + persist.write_value(out, v, 0, sub_order) + out:write("\n") + end + return true +end + +--- Write a table using Lua table syntax to a writer object. +-- @param out table or userdata: a writer object supporting :write() method. +-- @param tbl table: the table to be written. +local function write_table_as_table(out, tbl) + out:write("return {\n") + for k, v, sub_order in util.sortedpairs(tbl) do + out:write(" ") + write_table_key_assignment(out, k, 1) + persist.write_value(out, v, 1, sub_order) + out:write(",\n") + end + out:write("}\n") +end + +--- Save the contents of a table to a string. +-- Each element of the table is saved as a global assignment. +-- Only numbers, strings and tables (containing numbers, strings +-- or other recursively processed tables) are supported. +-- @param tbl table: the table containing the data to be written +-- @param field_order table: an optional array indicating the order of top-level fields. +-- @return persisted data as string; or nil and an error message +function persist.save_from_table_to_string(tbl, field_order) + local out = {buffer = {}} + function out:write(data) table.insert(self.buffer, data) end + local ok, err = write_table_as_assignments(out, tbl, field_order) + if not ok then + return nil, err + end + return table.concat(out.buffer) +end + +--- Save the contents of a table in a file. +-- Each element of the table is saved as a global assignment. +-- Only numbers, strings and tables (containing numbers, strings +-- or other recursively processed tables) are supported. +-- @param filename string: the output filename +-- @param tbl table: the table containing the data to be written +-- @param field_order table: an optional array indicating the order of top-level fields. +-- @return boolean or (nil, string): true if successful, or nil and a +-- message in case of errors. +function persist.save_from_table(filename, tbl, field_order) + local prefix = dir.dir_name(filename) + fs.make_dir(prefix) + local out = io.open(filename, "w") + if not out then + return nil, "Cannot create file at "..filename + end + local ok, err = write_table_as_assignments(out, tbl, field_order) + out:close() + if not ok then + return nil, err + end + return true +end + +--- Save the contents of a table as a module. +-- The module contains a 'return' statement that returns the table. +-- Only numbers, strings and tables (containing numbers, strings +-- or other recursively processed tables) are supported. +-- @param filename string: the output filename +-- @param tbl table: the table containing the data to be written +-- @return boolean or (nil, string): true if successful, or nil and a +-- message in case of errors. +function persist.save_as_module(filename, tbl) + local out = io.open(filename, "w") + if not out then + return nil, "Cannot create file at "..filename + end + write_table_as_table(out, tbl) + out:close() + return true +end + +function persist.load_config_file_if_basic(filename, cfg) + local env = { + home = cfg.home + } + local result, err, errcode = persist.load_into_table(filename, env) + if errcode == "load" or errcode == "run" then + -- bad config file or depends on env, so error out + return nil, "Could not read existing config file " .. filename + end + + local tbl + if errcode == "open" then + -- could not open, maybe file does not exist + tbl = {} + else + tbl = result + tbl.home = nil + end + + return tbl +end + +function persist.save_default_lua_version(prefix, lua_version) + local ok, err = fs.make_dir(prefix) + if not ok then + return nil, err + end + local fd, err = io.open(dir.path(prefix, "default-lua-version.lua"), "w") + if not fd then + return nil, err + end + fd:write('return "' .. lua_version .. '"\n') + fd:close() + return true +end + +return persist diff --git a/src/luarocks/queries.lua b/src/luarocks/queries.lua new file mode 100644 index 0000000..0c8790f --- /dev/null +++ b/src/luarocks/queries.lua @@ -0,0 +1,217 @@ + +local queries = {} + +local vers = require("luarocks.core.vers") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + +local query_mt = {} + +query_mt.__index = query_mt + +function query_mt.type() + return "query" +end + +-- Fallback default value for the `arch` field, if not explicitly set. +query_mt.arch = { + src = true, + all = true, + rockspec = true, + installed = true, + -- [cfg.arch] = true, -- this is set later +} + +-- Fallback default value for the `substring` field, if not explicitly set. +query_mt.substring = false + +--- Convert the arch field of a query table to table format. +-- @param input string, table or nil +local function arch_to_table(input) + if type(input) == "table" then + return input + elseif type(input) == "string" then + local arch = {} + for a in input:gmatch("[%w_-]+") do + arch[a] = true + end + return arch + end +end + +--- Prepare a query in dependency table format. +-- @param name string: the package name. +-- @param namespace string?: the package namespace. +-- @param version string?: the package version. +-- @param substring boolean?: match substrings of the name +-- (default is false, match full name) +-- @param arch string?: a string with pipe-separated accepted arch values +-- @param operator string?: operator for version matching (default is "==") +-- @return table: A query in table format +function queries.new(name, namespace, version, substring, arch, operator) + assert(type(name) == "string") + assert(type(namespace) == "string" or not namespace) + assert(type(version) == "string" or not version) + assert(type(substring) == "boolean" or not substring) + assert(type(arch) == "string" or not arch) + assert(type(operator) == "string" or not operator) + + operator = operator or "==" + + local self = { + name = name, + namespace = namespace, + constraints = {}, + substring = substring, + arch = arch_to_table(arch), + } + if version then + table.insert(self.constraints, { op = operator, version = vers.parse_version(version)}) + end + + query_mt.arch[cfg.arch] = true + return setmetatable(self, query_mt) +end + +-- Query for all packages +-- @param arch string (optional) +function queries.all(arch) + assert(type(arch) == "string" or not arch) + + return queries.new("", nil, nil, true, arch) +end + +do + local parse_constraints + do + local parse_constraint + do + local operators = { + ["=="] = "==", + ["~="] = "~=", + [">"] = ">", + ["<"] = "<", + [">="] = ">=", + ["<="] = "<=", + ["~>"] = "~>", + -- plus some convenience translations + [""] = "==", + ["="] = "==", + ["!="] = "~=" + } + + --- Consumes a constraint from a string, converting it to table format. + -- For example, a string ">= 1.0, > 2.0" is converted to a table in the + -- format {op = ">=", version={1,0}} and the rest, "> 2.0", is returned + -- back to the caller. + -- @param input string: A list of constraints in string format. + -- @return (table, string) or nil: A table representing the same + -- constraints and the string with the unused input, or nil if the + -- input string is invalid. + parse_constraint = function(input) + assert(type(input) == "string") + + local no_upgrade, op, version, rest = input:match("^(@?)([<>=~!]*)%s*([%w%.%_%-]+)[%s,]*(.*)") + local _op = operators[op] + version = vers.parse_version(version) + if not _op then + return nil, "Encountered bad constraint operator: '"..tostring(op).."' in '"..input.."'" + end + if not version then + return nil, "Could not parse version from constraint: '"..input.."'" + end + return { op = _op, version = version, no_upgrade = no_upgrade=="@" and true or nil }, rest + end + end + + --- Convert a list of constraints from string to table format. + -- For example, a string ">= 1.0, < 2.0" is converted to a table in the format + -- {{op = ">=", version={1,0}}, {op = "<", version={2,0}}}. + -- Version tables use a metatable allowing later comparison through + -- relational operators. + -- @param input string: A list of constraints in string format. + -- @return table or nil: A table representing the same constraints, + -- or nil if the input string is invalid. + parse_constraints = function(input) + assert(type(input) == "string") + + local constraints, oinput, constraint = {}, input + while #input > 0 do + constraint, input = parse_constraint(input) + if constraint then + table.insert(constraints, constraint) + else + return nil, "Failed to parse constraint '"..tostring(oinput).."' with error: ".. input + end + end + return constraints + end + end + + --- Prepare a query in dependency table format. + -- @param depstr string: A dependency in string format + -- as entered in rockspec files. + -- @return table: A query in table format, or nil and an error message in case of errors. + function queries.from_dep_string(depstr) + assert(type(depstr) == "string") + + local ns_name, rest = depstr:match("^%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)") + if not ns_name then + return nil, "failed to extract dependency name from '"..depstr.."'" + end + + ns_name = ns_name:lower() + + local constraints, err = parse_constraints(rest) + if not constraints then + return nil, err + end + + local name, namespace = util.split_namespace(ns_name) + + local self = { + name = name, + namespace = namespace, + constraints = constraints, + } + + query_mt.arch[cfg.arch] = true + return setmetatable(self, query_mt) + end +end + +function queries.from_persisted_table(tbl) + query_mt.arch[cfg.arch] = true + return setmetatable(tbl, query_mt) +end + +--- Build a string representation of a query package name. +-- Includes namespace, name and version, but not arch or constraints. +-- @param query table: a query table +-- @return string: a result such as `my_user/my_rock 1.0` or `my_rock`. +function query_mt:__tostring() + local out = {} + if self.namespace then + table.insert(out, self.namespace) + table.insert(out, "/") + end + table.insert(out, self.name) + + if #self.constraints > 0 then + local pretty = {} + for _, c in ipairs(self.constraints) do + local v = c.version.string + if c.op == "==" then + table.insert(pretty, v) + else + table.insert(pretty, c.op .. " " .. v) + end + end + table.insert(out, " ") + table.insert(out, table.concat(pretty, ", ")) + end + + return table.concat(out) +end + +return queries diff --git a/src/luarocks/remove.lua b/src/luarocks/remove.lua new file mode 100644 index 0000000..a24b54b --- /dev/null +++ b/src/luarocks/remove.lua @@ -0,0 +1,135 @@ +local remove = {} + +local search = require("luarocks.search") +local deps = require("luarocks.deps") +local fetch = require("luarocks.fetch") +local repos = require("luarocks.repos") +local path = require("luarocks.path") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local manif = require("luarocks.manif") +local queries = require("luarocks.queries") + +--- Obtain a list of packages that depend on the given set of packages +-- (where all packages of the set are versions of one program). +-- @param name string: the name of a program +-- @param versions array of string: the versions to be deleted. +-- @return array of string: an empty table if no packages depend on any +-- of the given list, or an array of strings in "name/version" format. +local function check_dependents(name, versions, deps_mode) + local dependents = {} + + local skip_set = {} + skip_set[name] = {} + for version, _ in pairs(versions) do + skip_set[name][version] = true + end + + local local_rocks = {} + local query_all = queries.all() + search.local_manifest_search(local_rocks, cfg.rocks_dir, query_all) + local_rocks[name] = nil + for rock_name, rock_versions in pairs(local_rocks) do + for rock_version, _ in pairs(rock_versions) do + local rockspec, err = fetch.load_rockspec(path.rockspec_file(rock_name, rock_version)) + if rockspec then + local _, missing = deps.match_deps(rockspec.dependencies, rockspec.rocks_provided, skip_set, deps_mode) + if missing[name] then + table.insert(dependents, { name = rock_name, version = rock_version }) + end + end + end + end + + return dependents +end + +--- Delete given versions of a program. +-- @param name string: the name of a program +-- @param versions array of string: the versions to be deleted. +-- @param deps_mode: string: Which trees to check dependencies for: +-- "one" for the current default tree, "all" for all trees, +-- "order" for all trees with priority >= the current default, "none" for no trees. +-- @return boolean or (nil, string): true on success or nil and an error message. +local function delete_versions(name, versions, deps_mode) + + for version, _ in pairs(versions) do + util.printout("Removing "..name.." "..version.."...") + local ok, err = repos.delete_version(name, version, deps_mode) + if not ok then return nil, err end + end + + return true +end + +function remove.remove_search_results(results, name, deps_mode, force, fast) + local versions = results[name] + + local version = next(versions) + local second = next(versions, version) + + local dependents = {} + if not fast then + util.printout("Checking stability of dependencies in the absence of") + util.printout(name.." "..table.concat(util.keys(versions), ", ").."...") + util.printout() + dependents = check_dependents(name, versions, deps_mode) + end + + if #dependents > 0 then + if force or fast then + util.printerr("The following packages may be broken by this forced removal:") + for _, dependent in ipairs(dependents) do + util.printerr(dependent.name.." "..dependent.version) + end + util.printerr() + else + if not second then + util.printerr("Will not remove "..name.." "..version..".") + util.printerr("Removing it would break dependencies for: ") + else + util.printerr("Will not remove installed versions of "..name..".") + util.printerr("Removing them would break dependencies for: ") + end + for _, dependent in ipairs(dependents) do + util.printerr(dependent.name.." "..dependent.version) + end + util.printerr() + util.printerr("Use --force to force removal (warning: this may break modules).") + return nil, "Failed removing." + end + end + + local ok, err = delete_versions(name, versions, deps_mode) + if not ok then return nil, err end + + util.printout("Removal successful.") + return true +end + +function remove.remove_other_versions(name, version, force, fast) + local results = {} + local query = queries.new(name, nil, version, false, nil, "~=") + search.local_manifest_search(results, cfg.rocks_dir, query) + local warn + if results[name] then + local ok, err = remove.remove_search_results(results, name, cfg.deps_mode, force, fast) + if not ok then -- downgrade failure to a warning + warn = err + end + end + + if not fast then + -- since we're not using --keep, this means that all files of the rock being installed + -- should be available as non-versioned variants. Double-check that: + local rock_manifest, load_err = manif.load_rock_manifest(name, version) + local ok, err = repos.check_everything_is_installed(name, version, rock_manifest, cfg.root_dir, false) + if not ok then + return nil, err + end + end + + return true, nil, warn +end + +return remove diff --git a/src/luarocks/repos.lua b/src/luarocks/repos.lua new file mode 100644 index 0000000..764fe3a --- /dev/null +++ b/src/luarocks/repos.lua @@ -0,0 +1,697 @@ + +--- Functions for managing the repository on disk. +local repos = {} + +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local manif = require("luarocks.manif") +local vers = require("luarocks.core.vers") + +local unpack = unpack or table.unpack -- luacheck: ignore 211 + +--- Get type and name of an item (a module or a command) provided by a file. +-- @param deploy_type string: rock manifest subtree the file comes from ("bin", "lua", or "lib"). +-- @param file_path string: path to the file relatively to deploy_type subdirectory. +-- @return (string, string): item type ("module" or "command") and name. +local function get_provided_item(deploy_type, file_path) + assert(type(deploy_type) == "string") + assert(type(file_path) == "string") + local item_type = deploy_type == "bin" and "command" or "module" + local item_name = item_type == "command" and file_path or path.path_to_module(file_path) + return item_type, item_name +end + +-- Tree of files installed by a package are stored +-- in its rock manifest. Some of these files have to +-- be deployed to locations where Lua can load them as +-- modules or where they can be used as commands. +-- These files are characterised by pair +-- (deploy_type, file_path), where deploy_type is the first +-- component of the file path and file_path is the rest of the +-- path. Only files with deploy_type in {"lua", "lib", "bin"} +-- are deployed somewhere. +-- Each deployed file provides an "item". An item is +-- characterised by pair (item_type, item_name). +-- item_type is "command" for files with deploy_type +-- "bin" and "module" for deploy_type in {"lua", "lib"}. +-- item_name is same as file_path for commands +-- and is produced using path.path_to_module(file_path) +-- for modules. + +--- Get all installed versions of a package. +-- @param name string: a package name. +-- @return table or nil: An array of strings listing installed +-- versions of a package, or nil if none is available. +local function get_installed_versions(name) + assert(type(name) == "string" and not name:match("/")) + + local dirs = fs.list_dir(path.versions_dir(name)) + return (dirs and #dirs > 0) and dirs or nil +end + +--- Check if a package exists in a local repository. +-- Version numbers are compared as exact string comparison. +-- @param name string: name of package +-- @param version string: package version in string format +-- @return boolean: true if a package is installed, +-- false otherwise. +function repos.is_installed(name, version) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + return fs.is_dir(path.install_dir(name, version)) +end + +function repos.recurse_rock_manifest_entry(entry, action) + assert(type(action) == "function") + + if entry == nil then + return true + end + + local function do_recurse_rock_manifest_entry(tree, parent_path) + + for file, sub in pairs(tree) do + local sub_path = (parent_path and (parent_path .. "/") or "") .. file + local ok, err -- luacheck: ignore 231 + + if type(sub) == "table" then + ok, err = do_recurse_rock_manifest_entry(sub, sub_path) + else + ok, err = action(sub_path) + end + + if err then return nil, err end + end + return true + end + return do_recurse_rock_manifest_entry(entry) +end + +local function store_package_data(result, rock_manifest, deploy_type) + if rock_manifest[deploy_type] then + repos.recurse_rock_manifest_entry(rock_manifest[deploy_type], function(file_path) + local _, item_name = get_provided_item(deploy_type, file_path) + result[item_name] = file_path + return true + end) + end +end + +--- Obtain a table of modules within an installed package. +-- @param name string: The package name; for example "luasocket" +-- @param version string: The exact version number including revision; +-- for example "2.0.1-1". +-- @return table: A table of modules where keys are module names +-- and values are file paths of files providing modules +-- relative to "lib" or "lua" rock manifest subtree. +-- If no modules are found or if package name or version +-- are invalid, an empty table is returned. +function repos.package_modules(name, version) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local result = {} + local rock_manifest = manif.load_rock_manifest(name, version) + if not rock_manifest then return result end + store_package_data(result, rock_manifest, "lib") + store_package_data(result, rock_manifest, "lua") + return result +end + +--- Obtain a table of command-line scripts within an installed package. +-- @param name string: The package name; for example "luasocket" +-- @param version string: The exact version number including revision; +-- for example "2.0.1-1". +-- @return table: A table of commands where keys and values are command names +-- as strings - file paths of files providing commands +-- relative to "bin" rock manifest subtree. +-- If no commands are found or if package name or version +-- are invalid, an empty table is returned. +function repos.package_commands(name, version) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local result = {} + local rock_manifest = manif.load_rock_manifest(name, version) + if not rock_manifest then return result end + store_package_data(result, rock_manifest, "bin") + return result +end + + +--- Check if a rock contains binary executables. +-- @param name string: name of an installed rock +-- @param version string: version of an installed rock +-- @return boolean: returns true if rock contains platform-specific +-- binary executables, or false if it is a pure-Lua rock. +function repos.has_binaries(name, version) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local rock_manifest = manif.load_rock_manifest(name, version) + if rock_manifest and rock_manifest.bin then + for bin_name, md5 in pairs(rock_manifest.bin) do + -- TODO verify that it is the same file. If it isn't, find the actual command. + if fs.is_actual_binary(dir.path(cfg.deploy_bin_dir, bin_name)) then + return true + end + end + end + return false +end + +function repos.run_hook(rockspec, hook_name) + assert(rockspec:type() == "rockspec") + assert(type(hook_name) == "string") + + local hooks = rockspec.hooks + if not hooks then + return true + end + + if cfg.hooks_enabled == false then + return nil, "This rockspec contains hooks, which are blocked by the 'hooks_enabled' setting in your LuaRocks configuration." + end + + if not hooks.substituted_variables then + util.variable_substitutions(hooks, rockspec.variables) + hooks.substituted_variables = true + end + local hook = hooks[hook_name] + if hook then + util.printout(hook) + if not fs.execute(hook) then + return nil, "Failed running "..hook_name.." hook." + end + end + return true +end + +function repos.should_wrap_bin_scripts(rockspec) + assert(rockspec:type() == "rockspec") + + if cfg.wrap_bin_scripts ~= nil then + return cfg.wrap_bin_scripts + end + if rockspec.deploy and rockspec.deploy.wrap_bin_scripts == false then + return false + end + return true +end + +local function find_suffixed(file, suffix) + local filenames = {file} + if suffix and suffix ~= "" then + table.insert(filenames, 1, file .. suffix) + end + + for _, filename in ipairs(filenames) do + if fs.exists(filename) then + return filename + end + end + + return nil, table.concat(filenames, ", ") .. " not found" +end + +local function check_suffix(filename, suffix) + local suffixed_filename, err = find_suffixed(filename, suffix) + if not suffixed_filename then + return "" + end + return suffixed_filename:sub(#filename + 1) +end + +-- Files can be deployed using versioned and non-versioned names. +-- Several items with same type and name can exist if they are +-- provided by different packages or versions. In any case +-- item from the newest version of lexicographically smallest package +-- is deployed using non-versioned name and others use versioned names. + +local function get_deploy_paths(name, version, deploy_type, file_path, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(type(deploy_type) == "string") + assert(type(file_path) == "string") + + repo = repo or cfg.root_dir + local deploy_dir = path["deploy_" .. deploy_type .. "_dir"](repo) + local non_versioned = dir.path(deploy_dir, file_path) + local versioned = path.versioned_name(non_versioned, deploy_dir, name, version) + return { nv = non_versioned, v = versioned } +end + +local function check_spot_if_available(name, version, deploy_type, file_path) + local item_type, item_name = get_provided_item(deploy_type, file_path) + local cur_name, cur_version = manif.get_current_provider(item_type, item_name) + + -- older versions of LuaRocks (< 3) registered "foo.init" files as "foo" + -- (which caused problems, so that behavior was changed). But look for that + -- in the manifest anyway for backward compatibility. + if not cur_name and deploy_type == "lua" and item_name:match("%.init$") then + cur_name, cur_version = manif.get_current_provider(item_type, (item_name:gsub("%.init$", ""))) + end + + if (not cur_name) + or (name < cur_name) + or (name == cur_name and (version == cur_version + or vers.compare_versions(version, cur_version))) then + return "nv", cur_name, cur_version, item_name + else + -- Existing version has priority, deploy new version using versioned name. + return "v", cur_name, cur_version, item_name + end +end + +local function backup_existing(should_backup, target) + if not should_backup then + fs.delete(target) + return + end + if fs.exists(target) then + local backup = target + repeat + backup = backup.."~" + until not fs.exists(backup) -- Slight race condition here, but shouldn't be a problem. + + util.warning(target.." is not tracked by this installation of LuaRocks. Moving it to "..backup) + local move_ok, move_err = os.rename(target, backup) + if not move_ok then + return nil, move_err + end + return backup + end +end + +local function prepare_op_install() + local mkdirs = {} + local rmdirs = {} + + local function memoize_mkdir(d) + if mkdirs[d] then + return true + end + local ok, err = fs.make_dir(d) + if not ok then + return nil, err + end + mkdirs[d] = true + return true + end + + local function op_install(op) + local ok, err = memoize_mkdir(dir.dir_name(op.dst)) + if not ok then + return nil, err + end + + local backup, err = backup_existing(op.backup, op.dst) + if err then + return nil, err + end + if backup then + op.backup_file = backup + end + + ok, err = op.fn(op.src, op.dst, op.backup) + if not ok then + return nil, err + end + + rmdirs[dir.dir_name(op.src)] = true + return true + end + + local function done_op_install() + for d, _ in pairs(rmdirs) do + fs.remove_dir_tree_if_empty(d) + end + end + + return op_install, done_op_install +end + +local function rollback_install(op) + fs.delete(op.dst) + if op.backup_file then + os.rename(op.backup_file, op.dst) + end + fs.remove_dir_tree_if_empty(dir.dir_name(op.dst)) + return true +end + +local function op_rename(op) + if op.suffix then + local suffix = check_suffix(op.src, op.suffix) + op.src = op.src .. suffix + op.dst = op.dst .. suffix + end + + if fs.exists(op.src) then + fs.make_dir(dir.dir_name(op.dst)) + fs.delete(op.dst) + local ok, err = os.rename(op.src, op.dst) + fs.remove_dir_tree_if_empty(dir.dir_name(op.src)) + return ok, err + else + return true + end +end + +local function rollback_rename(op) + return op_rename({ src = op.dst, dst = op.src }) +end + +local function prepare_op_delete() + local deletes = {} + local rmdirs = {} + + local function done_op_delete() + for _, f in ipairs(deletes) do + os.remove(f) + end + + for d, _ in pairs(rmdirs) do + fs.remove_dir_tree_if_empty(d) + end + end + + local function op_delete(op) + if op.suffix then + local suffix = check_suffix(op.name, op.suffix) + op.name = op.name .. suffix + end + + table.insert(deletes, op.name) + + rmdirs[dir.dir_name(op.name)] = true + end + + return op_delete, done_op_delete +end + +local function rollback_ops(ops, op_fn, n) + for i = 1, n do + op_fn(ops[i]) + end +end + +--- Double check that all files referenced in `rock_manifest` are installed in `repo`. +function repos.check_everything_is_installed(name, version, rock_manifest, repo, accept_versioned) + local missing = {} + local suffix = cfg.wrapper_suffix or "" + for _, category in ipairs({"bin", "lua", "lib"}) do + if rock_manifest[category] then + repos.recurse_rock_manifest_entry(rock_manifest[category], function(file_path) + local paths = get_deploy_paths(name, version, category, file_path, repo) + if category == "bin" then + if (fs.exists(paths.nv) or fs.exists(paths.nv .. suffix)) + or (accept_versioned and (fs.exists(paths.v) or fs.exists(paths.v .. suffix))) then + return + end + else + if fs.exists(paths.nv) or (accept_versioned and fs.exists(paths.v)) then + return + end + end + table.insert(missing, paths.nv) + end) + end + end + if #missing > 0 then + return nil, "failed deploying files. " .. + "The following files were not installed:\n" .. + table.concat(missing, "\n") + end + return true +end + +--- Deploy a package from the rocks subdirectory. +-- @param name string: name of package +-- @param version string: exact package version in string format +-- @param wrap_bin_scripts bool: whether commands written in Lua should be wrapped. +-- @param deps_mode: string: Which trees to check dependencies for: +-- "one" for the current default tree, "all" for all trees, +-- "order" for all trees with priority >= the current default, "none" for no trees. +function repos.deploy_files(name, version, wrap_bin_scripts, deps_mode) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(wrap_bin_scripts) == "boolean") + + local rock_manifest, load_err = manif.load_rock_manifest(name, version) + if not rock_manifest then return nil, load_err end + + local repo = cfg.root_dir + local renames = {} + local installs = {} + + local function install_binary(source, target) + if wrap_bin_scripts and fs.is_lua(source) then + return fs.wrap_script(source, target, deps_mode, name, version) + else + return fs.copy_binary(source, target) + end + end + + local function move_lua(source, target) + return fs.move(source, target, "read") + end + + local function move_lib(source, target) + return fs.move(source, target, "exec") + end + + if rock_manifest.bin then + local source_dir = path.bin_dir(name, version) + repos.recurse_rock_manifest_entry(rock_manifest.bin, function(file_path) + local source = dir.path(source_dir, file_path) + local paths = get_deploy_paths(name, version, "bin", file_path, repo) + local mode, cur_name, cur_version = check_spot_if_available(name, version, "bin", file_path) + + if mode == "nv" and cur_name then + local cur_paths = get_deploy_paths(cur_name, cur_version, "bin", file_path, repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v, suffix = cfg.wrapper_suffix }) + end + local target = mode == "nv" and paths.nv or paths.v + local backup = name ~= cur_name or version ~= cur_version + if wrap_bin_scripts and fs.is_lua(source) then + target = target .. (cfg.wrapper_suffix or "") + end + table.insert(installs, { fn = install_binary, src = source, dst = target, backup = backup }) + end) + end + + if rock_manifest.lua then + local source_dir = path.lua_dir(name, version) + repos.recurse_rock_manifest_entry(rock_manifest.lua, function(file_path) + local source = dir.path(source_dir, file_path) + local paths = get_deploy_paths(name, version, "lua", file_path, repo) + local mode, cur_name, cur_version = check_spot_if_available(name, version, "lua", file_path) + + if mode == "nv" and cur_name then + local cur_paths = get_deploy_paths(cur_name, cur_version, "lua", file_path, repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v }) + cur_paths = get_deploy_paths(cur_name, cur_version, "lib", file_path:gsub("%.lua$", "." .. cfg.lib_extension), repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v }) + end + local target = mode == "nv" and paths.nv or paths.v + local backup = name ~= cur_name or version ~= cur_version + table.insert(installs, { fn = move_lua, src = source, dst = target, backup = backup }) + end) + end + + if rock_manifest.lib then + local source_dir = path.lib_dir(name, version) + repos.recurse_rock_manifest_entry(rock_manifest.lib, function(file_path) + local source = dir.path(source_dir, file_path) + local paths = get_deploy_paths(name, version, "lib", file_path, repo) + local mode, cur_name, cur_version = check_spot_if_available(name, version, "lib", file_path) + + if mode == "nv" and cur_name then + local cur_paths = get_deploy_paths(cur_name, cur_version, "lua", file_path:gsub("%.[^.]+$", ".lua"), repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v }) + cur_paths = get_deploy_paths(cur_name, cur_version, "lib", file_path, repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v }) + end + local target = mode == "nv" and paths.nv or paths.v + local backup = name ~= cur_name or version ~= cur_version + table.insert(installs, { fn = move_lib, src = source, dst = target, backup = backup }) + end) + end + + for i, op in ipairs(renames) do + local ok, err = op_rename(op) + if not ok then + rollback_ops(renames, rollback_rename, i - 1) + return nil, err + end + end + local op_install, done_op_install = prepare_op_install() + for i, op in ipairs(installs) do + local ok, err = op_install(op) + if not ok then + rollback_ops(installs, rollback_install, i - 1) + rollback_ops(renames, rollback_rename, #renames) + return nil, err + end + end + done_op_install() + + local ok, err = repos.check_everything_is_installed(name, version, rock_manifest, repo, true) + if not ok then + return nil, err + end + + local writer = require("luarocks.manif.writer") + return writer.add_to_manifest(name, version, nil, deps_mode) +end + +local function add_to_double_checks(double_checks, name, version) + double_checks[name] = double_checks[name] or {} + double_checks[name][version] = true +end + +local function double_check_all(double_checks, repo) + local errs = {} + for next_name, versions in pairs(double_checks) do + for next_version in pairs(versions) do + local rock_manifest, load_err = manif.load_rock_manifest(next_name, next_version) + local ok, err = repos.check_everything_is_installed(next_name, next_version, rock_manifest, repo, true) + if not ok then + table.insert(errs, err) + end + end + end + if next(errs) then + return nil, table.concat(errs, "\n") + end + return true +end + +--- Delete a package from the local repository. +-- @param name string: name of package +-- @param version string: exact package version in string format +-- @param deps_mode: string: Which trees to check dependencies for: +-- "one" for the current default tree, "all" for all trees, +-- "order" for all trees with priority >= the current default, "none" for no trees. +-- @param quick boolean: do not try to fix the versioned name +-- of another version that provides the same module that +-- was deleted. This is used during 'purge', as every module +-- will be eventually deleted. +function repos.delete_version(name, version, deps_mode, quick) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(deps_mode) == "string") + + local rock_manifest, load_err = manif.load_rock_manifest(name, version) + if not rock_manifest then + if not quick then + local writer = require("luarocks.manif.writer") + writer.remove_from_manifest(name, version, nil, deps_mode) + return nil, "rock_manifest file not found for "..name.." "..version.." - removed entry from the manifest" + end + return nil, load_err + end + + local repo = cfg.root_dir + local renames = {} + local deletes = {} + + local double_checks = {} + + if rock_manifest.bin then + repos.recurse_rock_manifest_entry(rock_manifest.bin, function(file_path) + local paths = get_deploy_paths(name, version, "bin", file_path, repo) + local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "bin", file_path) + if mode == "v" then + table.insert(deletes, { name = paths.v, suffix = cfg.wrapper_suffix }) + else + table.insert(deletes, { name = paths.nv, suffix = cfg.wrapper_suffix }) + + local next_name, next_version = manif.get_next_provider("command", item_name) + if next_name then + add_to_double_checks(double_checks, next_name, next_version) + local next_paths = get_deploy_paths(next_name, next_version, "bin", file_path, repo) + table.insert(renames, { src = next_paths.v, dst = next_paths.nv, suffix = cfg.wrapper_suffix }) + end + end + end) + end + + if rock_manifest.lua then + repos.recurse_rock_manifest_entry(rock_manifest.lua, function(file_path) + local paths = get_deploy_paths(name, version, "lua", file_path, repo) + local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "lua", file_path) + if mode == "v" then + table.insert(deletes, { name = paths.v }) + else + table.insert(deletes, { name = paths.nv }) + + local next_name, next_version = manif.get_next_provider("module", item_name) + if next_name then + add_to_double_checks(double_checks, next_name, next_version) + local next_lua_paths = get_deploy_paths(next_name, next_version, "lua", file_path, repo) + table.insert(renames, { src = next_lua_paths.v, dst = next_lua_paths.nv }) + local next_lib_paths = get_deploy_paths(next_name, next_version, "lib", file_path:gsub("%.[^.]+$", ".lua"), repo) + table.insert(renames, { src = next_lib_paths.v, dst = next_lib_paths.nv }) + end + end + end) + end + + if rock_manifest.lib then + repos.recurse_rock_manifest_entry(rock_manifest.lib, function(file_path) + local paths = get_deploy_paths(name, version, "lib", file_path, repo) + local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "lib", file_path) + if mode == "v" then + table.insert(deletes, { name = paths.v }) + else + table.insert(deletes, { name = paths.nv }) + + local next_name, next_version = manif.get_next_provider("module", item_name) + if next_name then + add_to_double_checks(double_checks, next_name, next_version) + local next_lua_paths = get_deploy_paths(next_name, next_version, "lua", file_path:gsub("%.[^.]+$", ".lua"), repo) + table.insert(renames, { src = next_lua_paths.v, dst = next_lua_paths.nv }) + local next_lib_paths = get_deploy_paths(next_name, next_version, "lib", file_path, repo) + table.insert(renames, { src = next_lib_paths.v, dst = next_lib_paths.nv }) + end + end + end) + end + + local op_delete, done_op_delete = prepare_op_delete() + for _, op in ipairs(deletes) do + op_delete(op) + end + done_op_delete() + + if not quick then + for _, op in ipairs(renames) do + op_rename(op) + end + + local ok, err = double_check_all(double_checks, repo) + if not ok then + return nil, err + end + end + + fs.delete(path.install_dir(name, version)) + if not get_installed_versions(name) then + fs.delete(dir.path(cfg.rocks_dir, name)) + end + + if quick then + return true + end + + local writer = require("luarocks.manif.writer") + return writer.remove_from_manifest(name, version, nil, deps_mode) +end + +return repos diff --git a/src/luarocks/require.lua b/src/luarocks/require.lua new file mode 100644 index 0000000..902bd1a --- /dev/null +++ b/src/luarocks/require.lua @@ -0,0 +1,2 @@ +--- Retained for compatibility reasons only. Use luarocks.loader instead. +return require("luarocks.loader") diff --git a/src/luarocks/results.lua b/src/luarocks/results.lua new file mode 100644 index 0000000..c14862d --- /dev/null +++ b/src/luarocks/results.lua @@ -0,0 +1,62 @@ +local results = {} + +local vers = require("luarocks.core.vers") +local util = require("luarocks.util") + +local result_mt = {} + +result_mt.__index = result_mt + +function result_mt.type() + return "result" +end + +function results.new(name, version, repo, arch, namespace) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(repo) == "string") + assert(type(arch) == "string" or not arch) + assert(type(namespace) == "string" or not namespace) + + if not namespace then + name, namespace = util.split_namespace(name) + end + + local self = { + name = name, + version = version, + namespace = namespace, + arch = arch, + repo = repo, + } + + return setmetatable(self, result_mt) +end + +--- Test the name field of a query. +-- If query has a boolean field substring set to true, +-- then substring match is performed; otherwise, exact string +-- comparison is done. +-- @param query table: A query in dependency table format. +-- @param name string: A package name. +-- @return boolean: True if names match, false otherwise. +local function match_name(query, name) + if query.substring then + return name:find(query.name, 0, true) and true or false + else + return name == query.name + end +end + +--- Returns true if the result satisfies a given query. +-- @param query: a query. +-- @return boolean. +function result_mt:satisfies(query) + assert(query:type() == "query") + return match_name(query, self.name) + and (query.arch[self.arch] or query.arch["any"]) + and ((not query.namespace) or (query.namespace == self.namespace)) + and vers.match_constraints(vers.parse_version(self.version), query.constraints) +end + +return results diff --git a/src/luarocks/rockspecs.lua b/src/luarocks/rockspecs.lua new file mode 100644 index 0000000..454bab7 --- /dev/null +++ b/src/luarocks/rockspecs.lua @@ -0,0 +1,183 @@ +local rockspecs = {} + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local queries = require("luarocks.queries") +local type_rockspec = require("luarocks.type.rockspec") +local util = require("luarocks.util") +local vers = require("luarocks.core.vers") + +local vendored_build_type_set = { + ["builtin"] = true, + ["cmake"] = true, + ["command"] = true, + ["make"] = true, + ["module"] = true, -- compatibility alias + ["none"] = true, +} + +local rockspec_mt = {} + +rockspec_mt.__index = rockspec_mt + +function rockspec_mt.type() + return "rockspec" +end + +--- Perform platform-specific overrides on a table. +-- Overrides values of table with the contents of the appropriate +-- subset of its "platforms" field. The "platforms" field should +-- be a table containing subtables keyed with strings representing +-- platform names. Names that match the contents of the global +-- detected platforms setting are used. For example, if +-- platform "unix" is detected, then the fields of +-- tbl.platforms.unix will overwrite those of tbl with the same +-- names. For table values, the operation is performed recursively +-- (tbl.platforms.foo.x.y.z overrides tbl.x.y.z; other contents of +-- tbl.x are preserved). +-- @param tbl table or nil: Table which may contain a "platforms" field; +-- if it doesn't (or if nil is passed), this function does nothing. +local function platform_overrides(tbl) + assert(type(tbl) == "table" or not tbl) + + if not tbl then return end + + if tbl.platforms then + for platform in cfg.each_platform() do + local platform_tbl = tbl.platforms[platform] + if platform_tbl then + util.deep_merge(tbl, platform_tbl) + end + end + end + tbl.platforms = nil +end + +local function convert_dependencies(rockspec, key) + if rockspec[key] then + for i = 1, #rockspec[key] do + local parsed, err = queries.from_dep_string(rockspec[key][i]) + if not parsed then + return nil, "Parse error processing dependency '"..rockspec[key][i].."': "..tostring(err) + end + rockspec[key][i] = parsed + end + else + rockspec[key] = {} + end + return true +end + +--- Set up path-related variables for a given rock. +-- Create a "variables" table in the rockspec table, containing +-- adjusted variables according to the configuration file. +-- @param rockspec table: The rockspec table. +local function configure_paths(rockspec) + local vars = {} + for k,v in pairs(cfg.variables) do + vars[k] = v + end + local name, version = rockspec.name, rockspec.version + vars.PREFIX = path.install_dir(name, version) + vars.LUADIR = path.lua_dir(name, version) + vars.LIBDIR = path.lib_dir(name, version) + vars.CONFDIR = path.conf_dir(name, version) + vars.BINDIR = path.bin_dir(name, version) + vars.DOCDIR = path.doc_dir(name, version) + rockspec.variables = vars +end + +function rockspecs.from_persisted_table(filename, rockspec, globals, quick) + assert(type(rockspec) == "table") + assert(type(globals) == "table" or globals == nil) + assert(type(filename) == "string") + assert(type(quick) == "boolean" or quick == nil) + + if rockspec.rockspec_format then + if vers.compare_versions(rockspec.rockspec_format, type_rockspec.rockspec_format) then + return nil, "Rockspec format "..rockspec.rockspec_format.." is not supported, please upgrade LuaRocks." + end + end + + if not quick then + local ok, err = type_rockspec.check(rockspec, globals or {}) + if not ok then + return nil, err + end + end + + --- Check if rockspec format version satisfies version requirement. + -- @param rockspec table: The rockspec table. + -- @param version string: required version. + -- @return boolean: true if rockspec format matches version or is newer, false otherwise. + do + local parsed_format = vers.parse_version(rockspec.rockspec_format or "1.0") + rockspec.format_is_at_least = function(self, version) + return parsed_format >= vers.parse_version(version) + end + end + + platform_overrides(rockspec.build) + platform_overrides(rockspec.dependencies) + platform_overrides(rockspec.build_dependencies) + platform_overrides(rockspec.test_dependencies) + platform_overrides(rockspec.external_dependencies) + platform_overrides(rockspec.source) + platform_overrides(rockspec.hooks) + platform_overrides(rockspec.test) + + rockspec.name = rockspec.package:lower() + + local protocol, pathname = dir.split_url(rockspec.source.url) + if dir.is_basic_protocol(protocol) then + rockspec.source.file = rockspec.source.file or dir.base_name(rockspec.source.url) + end + rockspec.source.protocol, rockspec.source.pathname = protocol, pathname + + -- Temporary compatibility + if rockspec.source.cvs_module then rockspec.source.module = rockspec.source.cvs_module end + if rockspec.source.cvs_tag then rockspec.source.tag = rockspec.source.cvs_tag end + + rockspec.local_abs_filename = filename + rockspec.source.dir_set = rockspec.source.dir ~= nil + rockspec.source.dir = rockspec.source.dir or rockspec.source.module + + rockspec.rocks_provided = util.get_rocks_provided(rockspec) + + for _, key in ipairs({"dependencies", "build_dependencies", "test_dependencies"}) do + local ok, err = convert_dependencies(rockspec, key) + if not ok then + return nil, err + end + end + + if rockspec.build + and rockspec.build.type + and not vendored_build_type_set[rockspec.build.type] then + local build_pkg_name = "luarocks-build-" .. rockspec.build.type + if not rockspec.build_dependencies then + rockspec.build_dependencies = {} + end + + local found = false + for _, dep in ipairs(rockspec.build_dependencies) do + if dep.name == build_pkg_name then + found = true + break + end + end + + if not found then + table.insert(rockspec.build_dependencies, queries.from_dep_string(build_pkg_name)) + end + end + + if not quick then + configure_paths(rockspec) + end + + return setmetatable(rockspec, rockspec_mt) +end + +return rockspecs diff --git a/src/luarocks/search.lua b/src/luarocks/search.lua new file mode 100644 index 0000000..180f8f4 --- /dev/null +++ b/src/luarocks/search.lua @@ -0,0 +1,393 @@ +local search = {} + +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local manif = require("luarocks.manif") +local vers = require("luarocks.core.vers") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local queries = require("luarocks.queries") +local results = require("luarocks.results") + +--- Store a search result (a rock or rockspec) in the result tree. +-- @param result_tree table: The result tree, where keys are package names and +-- values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +-- @param result table: A result. +function search.store_result(result_tree, result) + assert(type(result_tree) == "table") + assert(result:type() == "result") + + local name = result.name + local version = result.version + + if not result_tree[name] then result_tree[name] = {} end + if not result_tree[name][version] then result_tree[name][version] = {} end + table.insert(result_tree[name][version], { + arch = result.arch, + repo = result.repo, + namespace = result.namespace, + }) +end + +--- Store a match in a result tree if version matches query. +-- Name, version, arch and repository path are stored in a given +-- table, optionally checking if version and arch (if given) match +-- a query. +-- @param result_tree table: The result tree, where keys are package names and +-- values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +-- @param result table: a result object. +-- @param query table: a query object. +local function store_if_match(result_tree, result, query) + assert(result:type() == "result") + assert(query:type() == "query") + + if result:satisfies(query) then + search.store_result(result_tree, result) + end +end + +--- Perform search on a local repository. +-- @param repo string: The pathname of the local repository. +-- @param query table: a query object. +-- @param result_tree table or nil: If given, this table will store the +-- result tree; if not given, a new table will be created. +-- @return table: The result tree, where keys are package names and +-- values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +-- If a table was given in the "result_tree" parameter, that is the result value. +function search.disk_search(repo, query, result_tree) + assert(type(repo) == "string") + assert(query:type() == "query") + assert(type(result_tree) == "table" or not result_tree) + + local fs = require("luarocks.fs") + + if not result_tree then + result_tree = {} + end + + for name in fs.dir(repo) do + local pathname = dir.path(repo, name) + local rname, rversion, rarch = path.parse_name(name) + + if rname and (pathname:match(".rockspec$") or pathname:match(".rock$")) then + local result = results.new(rname, rversion, repo, rarch) + store_if_match(result_tree, result, query) + elseif fs.is_dir(pathname) then + for version in fs.dir(pathname) do + if version:match("-%d+$") then + local namespace = path.read_namespace(name, version, repo) + local result = results.new(name, version, repo, "installed", namespace) + store_if_match(result_tree, result, query) + end + end + end + end + return result_tree +end + +--- Perform search on a rocks server or tree. +-- @param result_tree table: The result tree, where keys are package names and +-- values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +-- @param repo string: The URL of a rocks server or +-- the pathname of a rocks tree (as returned by path.rocks_dir()). +-- @param query table: a query object. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @param is_local boolean +-- @return true or, in case of errors, nil, an error message and an optional error code. +local function manifest_search(result_tree, repo, query, lua_version, is_local) + assert(type(result_tree) == "table") + assert(type(repo) == "string") + assert(query:type() == "query") + + -- FIXME do not add this in local repos + if (not is_local) and query.namespace then + repo = repo .. "/manifests/" .. query.namespace + end + + local manifest, err, errcode = manif.load_manifest(repo, lua_version, not is_local) + if not manifest then + return nil, err, errcode + end + for name, versions in pairs(manifest.repository) do + for version, items in pairs(versions) do + local namespace = is_local and path.read_namespace(name, version, repo) or query.namespace + for _, item in ipairs(items) do + local result = results.new(name, version, repo, item.arch, namespace) + store_if_match(result_tree, result, query) + end + end + end + return true +end + +local function remote_manifest_search(result_tree, repo, query, lua_version) + return manifest_search(result_tree, repo, query, lua_version, false) +end + +function search.local_manifest_search(result_tree, repo, query, lua_version) + return manifest_search(result_tree, repo, query, lua_version, true) +end + +--- Search on all configured rocks servers. +-- @param query table: a query object. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @return table: A table where keys are package names +-- and values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +function search.search_repos(query, lua_version) + assert(query:type() == "query") + + local result_tree = {} + for _, repo in ipairs(cfg.rocks_servers) do + if type(repo) == "string" then + repo = { repo } + end + for _, mirror in ipairs(repo) do + if not cfg.disabled_servers[mirror] then + local protocol, pathname = dir.split_url(mirror) + if protocol == "file" then + mirror = pathname + end + local ok, err, errcode = remote_manifest_search(result_tree, mirror, query, lua_version) + if errcode == "network" then + cfg.disabled_servers[mirror] = true + end + if ok then + break + else + util.warning("Failed searching manifest: "..err) + if errcode == "downloader" then + break + end + end + end + end + end + -- search through rocks in rocks_provided + local provided_repo = "provided by VM or rocks_provided" + for name, version in pairs(util.get_rocks_provided()) do + local result = results.new(name, version, provided_repo, "installed") + store_if_match(result_tree, result, query) + end + return result_tree +end + +--- Get the URL for the latest in a set of versions. +-- @param name string: The package name to be used in the URL. +-- @param versions table: An array of version informations, as stored +-- in search result trees. +-- @return string or nil: the URL for the latest version if one could +-- be picked, or nil. +local function pick_latest_version(name, versions) + assert(type(name) == "string" and not name:match("/")) + assert(type(versions) == "table") + + local vtables = {} + for v, _ in pairs(versions) do + table.insert(vtables, vers.parse_version(v)) + end + table.sort(vtables) + local version = vtables[#vtables].string + local items = versions[version] + if items then + local pick = 1 + for i, item in ipairs(items) do + if (item.arch == 'src' and items[pick].arch == 'rockspec') + or (item.arch ~= 'src' and item.arch ~= 'rockspec') then + pick = i + end + end + return path.make_url(items[pick].repo, name, version, items[pick].arch) + end + return nil +end + +-- Find out which other Lua versions provide rock versions matching a query, +-- @param query table: a query object. +-- @return table: array of Lua versions supported, in "5.x" format. +local function supported_lua_versions(query) + assert(query:type() == "query") + local result_tree = {} + + for lua_version in util.lua_versions() do + if lua_version ~= cfg.lua_version then + util.printout("Checking for Lua " .. lua_version .. "...") + if search.search_repos(query, lua_version)[query.name] then + table.insert(result_tree, lua_version) + end + end + end + + return result_tree +end + +--- Attempt to get a single URL for a given search for a rock. +-- @param query table: a query object. +-- @return string or (nil, string, string): URL for latest matching version +-- of the rock if it was found, or nil followed by an error message +-- and an error code. +function search.find_suitable_rock(query) + assert(query:type() == "query") + + local rocks_provided = util.get_rocks_provided() + + if rocks_provided[query.name] ~= nil then + -- Do not install versions listed in rocks_provided. + return nil, "Rock "..query.name.." "..rocks_provided[query.name].. + " is already provided by VM or via 'rocks_provided' in the config file.", "provided" + end + + local result_tree = search.search_repos(query) + local first_rock = next(result_tree) + if not first_rock then + return nil, "No results matching query were found for Lua " .. cfg.lua_version .. ".", "notfound" + elseif next(result_tree, first_rock) then + -- Shouldn't happen as query must match only one package. + return nil, "Several rocks matched query.", "manyfound" + else + return pick_latest_version(query.name, result_tree[first_rock]) + end +end + +function search.find_src_or_rockspec(name, namespace, version, check_lua_versions) + local query = queries.new(name, namespace, version, false, "src|rockspec") + local url, err = search.find_rock_checking_lua_versions(query, check_lua_versions) + if not url then + return nil, "Could not find a result named "..tostring(query)..": "..err + end + return url +end + +function search.find_rock_checking_lua_versions(query, check_lua_versions) + local url, err, errcode = search.find_suitable_rock(query) + if url then + return url + end + + if errcode == "notfound" then + local add + if check_lua_versions then + util.printout(query.name .. " not found for Lua " .. cfg.lua_version .. ".") + util.printout("Checking if available for other Lua versions...") + + -- Check if constraints are satisfiable with other Lua versions. + local lua_versions = supported_lua_versions(query) + + if #lua_versions ~= 0 then + -- Build a nice message in "only Lua 5.x and 5.y but not 5.z." format + for i, lua_version in ipairs(lua_versions) do + lua_versions[i] = "Lua "..lua_version + end + + local versions_message = "only "..table.concat(lua_versions, " and ").. + " but not Lua "..cfg.lua_version.."." + + if #query.constraints == 0 then + add = query.name.." supports "..versions_message + elseif #query.constraints == 1 and query.constraints[1].op == "==" then + add = query.name.." "..query.constraints[1].version.string.." supports "..versions_message + else + add = "Matching "..query.name.." versions support "..versions_message + end + else + add = query.name.." is not available for any Lua versions." + end + else + add = "To check if it is available for other Lua versions, use --check-lua-versions." + end + err = err .. "\n" .. add + end + + return nil, err +end + +--- Print a list of rocks/rockspecs on standard output. +-- @param result_tree table: A result tree. +-- @param porcelain boolean or nil: A flag to force machine-friendly output. +function search.print_result_tree(result_tree, porcelain) + assert(type(result_tree) == "table") + assert(type(porcelain) == "boolean" or not porcelain) + + if porcelain then + for package, versions in util.sortedpairs(result_tree) do + for version, repos in util.sortedpairs(versions, vers.compare_versions) do + for _, repo in ipairs(repos) do + local nrepo = dir.normalize(repo.repo) + util.printout(package, version, repo.arch, nrepo, repo.namespace) + end + end + end + return + end + + for package, versions in util.sortedpairs(result_tree) do + local namespaces = {} + for version, repos in util.sortedpairs(versions, vers.compare_versions) do + for _, repo in ipairs(repos) do + local key = repo.namespace or "" + local list = namespaces[key] or {} + namespaces[key] = list + + repo.repo = dir.normalize(repo.repo) + table.insert(list, " "..version.." ("..repo.arch..") - "..path.root_dir(repo.repo)) + end + end + for key, list in util.sortedpairs(namespaces) do + util.printout(key == "" and package or key .. "/" .. package) + for _, line in ipairs(list) do + util.printout(line) + end + util.printout() + end + end +end + +function search.pick_installed_rock(query, given_tree) + assert(query:type() == "query") + + local result_tree = {} + local tree_map = {} + local trees = cfg.rocks_trees + if given_tree then + trees = { given_tree } + end + for _, tree in ipairs(trees) do + local rocks_dir = path.rocks_dir(tree) + tree_map[rocks_dir] = tree + search.local_manifest_search(result_tree, rocks_dir, query) + end + if not next(result_tree) then + return nil, "cannot find package "..tostring(query).."\nUse 'list' to find installed rocks." + end + + if not result_tree[query.name] and next(result_tree, next(result_tree)) then + local out = { "multiple installed packages match the name '"..tostring(query).."':\n\n" } + for name, _ in util.sortedpairs(result_tree) do + table.insert(out, " " .. name .. "\n") + end + table.insert(out, "\nPlease specify a single rock.\n") + return nil, table.concat(out) + end + + local repo_url + + local name, versions + if result_tree[query.name] then + name, versions = query.name, result_tree[query.name] + else + name, versions = util.sortedpairs(result_tree)() + end + + local version, repositories = util.sortedpairs(versions, vers.compare_versions)() + for _, rp in ipairs(repositories) do repo_url = rp.repo end + + local repo = tree_map[repo_url] + return name, version, repo, repo_url +end + +return search + diff --git a/src/luarocks/signing.lua b/src/luarocks/signing.lua new file mode 100644 index 0000000..cb91643 --- /dev/null +++ b/src/luarocks/signing.lua @@ -0,0 +1,48 @@ +local signing = {} + +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") + +local function get_gpg() + local vars = cfg.variables + local gpg = vars.GPG + local gpg_ok, err = fs.is_tool_available(gpg, "gpg") + if not gpg_ok then + return nil, err + end + return gpg +end + +function signing.signature_url(url) + return url .. ".asc" +end + +function signing.sign_file(file) + local gpg, err = get_gpg() + if not gpg then + return nil, err + end + + local sigfile = file .. ".asc" + if fs.execute(gpg, "--armor", "--output", sigfile, "--detach-sign", file) then + return sigfile + else + return nil, "failed running " .. gpg .. " to sign " .. file + end +end + +function signing.verify_signature(file, sigfile) + local gpg, err = get_gpg() + if not gpg then + return nil, err + end + + if fs.execute(gpg, "--verify", sigfile, file) then + return true + else + return nil, "GPG returned a verification error" + end + +end + +return signing diff --git a/src/luarocks/test.lua b/src/luarocks/test.lua new file mode 100644 index 0000000..d074b95 --- /dev/null +++ b/src/luarocks/test.lua @@ -0,0 +1,100 @@ + +local test = {} + +local fetch = require("luarocks.fetch") +local deps = require("luarocks.deps") +local util = require("luarocks.util") + +local test_types = { + "busted", + "command", +} + +local test_modules = {} + +for _, test_type in ipairs(test_types) do + local mod = require("luarocks.test." .. test_type) + table.insert(test_modules, mod) + test_modules[test_type] = mod + test_modules[mod] = test_type +end + +local function get_test_type(rockspec) + if rockspec.test and rockspec.test.type then + return rockspec.test.type + end + + for _, test_module in ipairs(test_modules) do + if test_module.detect_type() then + return test_modules[test_module] + end + end + + return nil, "could not detect test type -- no test suite for " .. rockspec.package .. "?" +end + +-- Run test suite as configured in rockspec in the current directory. +function test.run_test_suite(rockspec_arg, test_type, args, prepare) + local rockspec + if type(rockspec_arg) == "string" then + local err, errcode + rockspec, err, errcode = fetch.load_rockspec(rockspec_arg) + if err then + return nil, err, errcode + end + else + assert(type(rockspec_arg) == "table") + rockspec = rockspec_arg + end + + if not test_type then + local err + test_type, err = get_test_type(rockspec, test_type) + if not test_type then + return nil, err + end + end + assert(test_type) + + local all_deps = { + "dependencies", + "build_dependencies", + "test_dependencies", + } + for _, dep_kind in ipairs(all_deps) do + if rockspec[dep_kind] and next(rockspec[dep_kind]) then + local ok, err, errcode = deps.fulfill_dependencies(rockspec, dep_kind, "all") + if err then + return nil, err, errcode + end + end + end + + local mod_name = "luarocks.test." .. test_type + local pok, test_mod = pcall(require, mod_name) + if not pok then + return nil, "failed loading test execution module " .. mod_name + end + + if prepare then + if test_type == "busted" then + return test_mod.run_tests(rockspec_arg, {"--version"}) + else + return true + end + else + local flags = rockspec.test and rockspec.test.flags + if type(flags) == "table" then + util.variable_substitutions(flags, rockspec.variables) + + -- insert any flags given in test.flags at the front of args + for i = 1, #flags do + table.insert(args, i, flags[i]) + end + end + + return test_mod.run_tests(rockspec.test, args) + end +end + +return test diff --git a/src/luarocks/test/busted.lua b/src/luarocks/test/busted.lua new file mode 100644 index 0000000..c73909c --- /dev/null +++ b/src/luarocks/test/busted.lua @@ -0,0 +1,53 @@ + +local busted = {} + +local fs = require("luarocks.fs") +local deps = require("luarocks.deps") +local path = require("luarocks.path") +local dir = require("luarocks.dir") +local queries = require("luarocks.queries") + +local unpack = table.unpack or unpack + +function busted.detect_type() + if fs.exists(".busted") then + return true + end + return false +end + +function busted.run_tests(test, args) + if not test then + test = {} + end + + local ok, bustedver, where = deps.fulfill_dependency(queries.new("busted"), nil, nil, nil, "test_dependencies") + if not ok then + return nil, bustedver + end + + local busted_exe + if test.busted_executable then + busted_exe = test.busted_executable + else + busted_exe = dir.path(path.root_dir(where), "bin", "busted") + + -- Windows fallback + local busted_bat = dir.path(path.root_dir(where), "bin", "busted.bat") + + if not fs.exists(busted_exe) and not fs.exists(busted_bat) then + return nil, "'busted' executable failed to be installed" + end + end + + local err + ok, err = fs.execute(busted_exe, unpack(args)) + if ok then + return true + else + return nil, err or "test suite failed." + end +end + + +return busted diff --git a/src/luarocks/test/command.lua b/src/luarocks/test/command.lua new file mode 100644 index 0000000..bed6744 --- /dev/null +++ b/src/luarocks/test/command.lua @@ -0,0 +1,52 @@ + +local command = {} + +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") + +local unpack = table.unpack or unpack + +function command.detect_type() + if fs.exists("test.lua") then + return true + end + return false +end + +function command.run_tests(test, args) + if not test then + test = { + script = "test.lua" + } + end + + if not test.script and not test.command then + test.script = "test.lua" + end + + local ok + + if test.script then + if type(test.script) ~= "string" then + return nil, "Malformed rockspec: 'script' expects a string" + end + if not fs.exists(test.script) then + return nil, "Test script " .. test.script .. " does not exist" + end + local lua = fs.Q(cfg.variables["LUA"]) -- get lua interpreter configured + ok = fs.execute(lua, test.script, unpack(args)) + elseif test.command then + if type(test.command) ~= "string" then + return nil, "Malformed rockspec: 'command' expects a string" + end + ok = fs.execute(test.command, unpack(args)) + end + + if ok then + return true + else + return nil, "tests failed with non-zero exit code" + end +end + +return command diff --git a/src/luarocks/tools/patch.lua b/src/luarocks/tools/patch.lua new file mode 100644 index 0000000..6f36d71 --- /dev/null +++ b/src/luarocks/tools/patch.lua @@ -0,0 +1,716 @@ +--- Patch utility to apply unified diffs. +-- +-- http://lua-users.org/wiki/LuaPatch +-- +-- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license). +-- Code is heavily based on the Python-based patch.py version 8.06-1 +-- Copyright (c) 2008 rainforce.org, MIT License +-- Project home: http://code.google.com/p/python-patch/ . +-- Version 0.1 + +local patch = {} + +local fs = require("luarocks.fs") +local fun = require("luarocks.fun") + +local io = io +local os = os +local string = string +local table = table +local format = string.format + +-- logging +local debugmode = false +local function debug(_) end +local function info(_) end +local function warning(s) io.stderr:write(s .. '\n') end + +-- Returns boolean whether string s2 starts with string s. +local function startswith(s, s2) + return s:sub(1, #s2) == s2 +end + +-- Returns boolean whether string s2 ends with string s. +local function endswith(s, s2) + return #s >= #s2 and s:sub(#s-#s2+1) == s2 +end + +-- Returns string s after filtering out any new-line characters from end. +local function endlstrip(s) + return s:gsub('[\r\n]+$', '') +end + +-- Returns shallow copy of table t. +local function table_copy(t) + local t2 = {} + for k,v in pairs(t) do t2[k] = v end + return t2 +end + +local function exists(filename) + local fh = io.open(filename) + local result = fh ~= nil + if fh then fh:close() end + return result +end +local function isfile() return true end --FIX? + +local function string_as_file(s) + return { + at = 0, + str = s, + len = #s, + eof = false, + read = function(self, n) + if self.eof then return nil end + local chunk = self.str:sub(self.at, self.at + n - 1) + self.at = self.at + n + if self.at > self.len then + self.eof = true + end + return chunk + end, + close = function(self) + self.eof = true + end, + } +end + +-- +-- file_lines(f) is similar to f:lines() for file f. +-- The main difference is that read_lines includes +-- new-line character sequences ("\n", "\r\n", "\r"), +-- if any, at the end of each line. Embedded "\0" are also handled. +-- Caution: The newline behavior can depend on whether f is opened +-- in binary or ASCII mode. +-- (file_lines - version 20080913) +-- +local function file_lines(f) + local CHUNK_SIZE = 1024 + local buffer = "" + local pos_beg = 1 + return function() + local pos, chars + while 1 do + pos, chars = buffer:match('()([\r\n].)', pos_beg) + if pos or not f then + break + elseif f then + local chunk = f:read(CHUNK_SIZE) + if chunk then + buffer = buffer:sub(pos_beg) .. chunk + pos_beg = 1 + else + f = nil + end + end + end + if not pos then + pos = #buffer + elseif chars == '\r\n' then + pos = pos + 1 + end + local line = buffer:sub(pos_beg, pos) + pos_beg = pos + 1 + if #line > 0 then + return line + end + end +end + +local function match_linerange(line) + local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)") + if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end + if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end + if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end + return m1, m2, m3, m4 +end + +local function match_epoch(str) + return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]") +end + +function patch.read_patch(filename, data) + -- define possible file regions that will direct the parser flow + local state = 'header' + -- 'header' - comments before the patch body + -- 'filenames' - lines starting with --- and +++ + -- 'hunkhead' - @@ -R +R @@ sequence + -- 'hunkbody' + -- 'hunkskip' - skipping invalid hunk mode + + local all_ok = true + local lineends = {lf=0, crlf=0, cr=0} + local files = {source={}, target={}, epoch={}, hunks={}, fileends={}, hunkends={}} + local nextfileno = 0 + local nexthunkno = 0 --: even if index starts with 0 user messages + -- number hunks from 1 + + -- hunkinfo holds parsed values, hunkactual - calculated + local hunkinfo = { + startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil, + invalid=false, text={} + } + local hunkactual = {linessrc=nil, linestgt=nil} + + info(format("reading patch %s", filename)) + + local fp + if data then + fp = string_as_file(data) + else + fp = filename == '-' and io.stdin or assert(io.open(filename, "rb")) + end + local lineno = 0 + + for line in file_lines(fp) do + lineno = lineno + 1 + if state == 'header' then + if startswith(line, "--- ") then + state = 'filenames' + end + -- state is 'header' or 'filenames' + end + if state == 'hunkbody' then + -- skip hunkskip and hunkbody code until definition of hunkhead read + + if line:match"^[\r\n]*$" then + -- prepend space to empty lines to interpret them as context properly + line = " " .. line + end + + -- process line first + if line:match"^[- +\\]" then + -- gather stats about line endings + local he = files.hunkends[nextfileno] + if endswith(line, "\r\n") then + he.crlf = he.crlf + 1 + elseif endswith(line, "\n") then + he.lf = he.lf + 1 + elseif endswith(line, "\r") then + he.cr = he.cr + 1 + end + if startswith(line, "-") then + hunkactual.linessrc = hunkactual.linessrc + 1 + elseif startswith(line, "+") then + hunkactual.linestgt = hunkactual.linestgt + 1 + elseif startswith(line, "\\") then + -- nothing + else + hunkactual.linessrc = hunkactual.linessrc + 1 + hunkactual.linestgt = hunkactual.linestgt + 1 + end + table.insert(hunkinfo.text, line) + -- todo: handle \ No newline cases + else + warning(format("invalid hunk no.%d at %d for target file %s", + nexthunkno, lineno, files.target[nextfileno])) + -- add hunk status node + table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) + files.hunks[nextfileno][nexthunkno].invalid = true + all_ok = false + state = 'hunkskip' + end + + -- check exit conditions + if hunkactual.linessrc > hunkinfo.linessrc or + hunkactual.linestgt > hunkinfo.linestgt + then + warning(format("extra hunk no.%d lines at %d for target %s", + nexthunkno, lineno, files.target[nextfileno])) + -- add hunk status node + table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) + files.hunks[nextfileno][nexthunkno].invalid = true + state = 'hunkskip' + elseif hunkinfo.linessrc == hunkactual.linessrc and + hunkinfo.linestgt == hunkactual.linestgt + then + table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) + state = 'hunkskip' + + -- detect mixed window/unix line ends + local ends = files.hunkends[nextfileno] + if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) + + (ends.lf~=0 and 1 or 0) > 1 + then + warning(format("inconsistent line ends in patch hunks for %s", + files.source[nextfileno])) + end + end + -- state is 'hunkbody' or 'hunkskip' + end + + if state == 'hunkskip' then + if match_linerange(line) then + state = 'hunkhead' + elseif startswith(line, "--- ") then + state = 'filenames' + if debugmode and #files.source > 0 then + debug(format("- %2d hunks for %s", #files.hunks[nextfileno], + files.source[nextfileno])) + end + end + -- state is 'hunkskip', 'hunkhead', or 'filenames' + end + local advance + if state == 'filenames' then + if startswith(line, "--- ") then + if fun.contains(files.source, nextfileno) then + all_ok = false + warning(format("skipping invalid patch for %s", + files.source[nextfileno+1])) + table.remove(files.source, nextfileno+1) + -- double source filename line is encountered + -- attempt to restart from this second line + end + -- Accept a space as a terminator, like GNU patch does. + -- Breaks patches containing filenames with spaces... + -- FIXME Figure out what does GNU patch do in those cases. + local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)") + if not match then + all_ok = false + warning(format("skipping invalid filename at line %d", lineno+1)) + state = 'header' + else + if match_epoch(rest) then + files.epoch[nextfileno + 1] = true + end + table.insert(files.source, match) + end + elseif not startswith(line, "+++ ") then + if fun.contains(files.source, nextfileno) then + all_ok = false + warning(format("skipping invalid patch with no target for %s", + files.source[nextfileno+1])) + table.remove(files.source, nextfileno+1) + else + -- this should be unreachable + warning("skipping invalid target patch") + end + state = 'header' + else + if fun.contains(files.target, nextfileno) then + all_ok = false + warning(format("skipping invalid patch - double target at line %d", + lineno+1)) + table.remove(files.source, nextfileno+1) + table.remove(files.target, nextfileno+1) + nextfileno = nextfileno - 1 + -- double target filename line is encountered + -- switch back to header state + state = 'header' + else + -- Accept a space as a terminator, like GNU patch does. + -- Breaks patches containing filenames with spaces... + -- FIXME Figure out what does GNU patch do in those cases. + local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$" + local match, rest = line:match(re_filename) + if not match then + all_ok = false + warning(format( + "skipping invalid patch - no target filename at line %d", + lineno+1)) + state = 'header' + else + table.insert(files.target, match) + nextfileno = nextfileno + 1 + if match_epoch(rest) then + files.epoch[nextfileno] = true + end + nexthunkno = 0 + table.insert(files.hunks, {}) + table.insert(files.hunkends, table_copy(lineends)) + table.insert(files.fileends, table_copy(lineends)) + state = 'hunkhead' + advance = true + end + end + end + -- state is 'filenames', 'header', or ('hunkhead' with advance) + end + if not advance and state == 'hunkhead' then + local m1, m2, m3, m4 = match_linerange(line) + if not m1 then + if not fun.contains(files.hunks, nextfileno-1) then + all_ok = false + warning(format("skipping invalid patch with no hunks for file %s", + files.target[nextfileno])) + end + state = 'header' + else + hunkinfo.startsrc = tonumber(m1) + hunkinfo.linessrc = tonumber(m2 or 1) + hunkinfo.starttgt = tonumber(m3) + hunkinfo.linestgt = tonumber(m4 or 1) + hunkinfo.invalid = false + hunkinfo.text = {} + + hunkactual.linessrc = 0 + hunkactual.linestgt = 0 + + state = 'hunkbody' + nexthunkno = nexthunkno + 1 + end + -- state is 'header' or 'hunkbody' + end + end + if state ~= 'hunkskip' then + warning(format("patch file incomplete - %s", filename)) + all_ok = false + -- os.exit(?) + else + -- duplicated message when an eof is reached + if debugmode and #files.source > 0 then + debug(format("- %2d hunks for %s", #files.hunks[nextfileno], + files.source[nextfileno])) + end + end + + local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end + info(format("total files: %d total hunks: %d", #files.source, sum)) + fp:close() + return files, all_ok +end + +local function find_hunk(file, h, hno) + for fuzz=0,2 do + local lineno = h.startsrc + for i=0,#file do + local found = true + local location = lineno + for l, hline in ipairs(h.text) do + if l > fuzz then + -- todo: \ No newline at the end of file + if startswith(hline, " ") or startswith(hline, "-") then + local line = file[lineno] + lineno = lineno + 1 + if not line or #line == 0 then + found = false + break + end + if endlstrip(line) ~= endlstrip(hline:sub(2)) then + found = false + break + end + end + end + end + if found then + local offset = location - h.startsrc - fuzz + if offset ~= 0 then + warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz))) + end + h.startsrc = location + h.starttgt = h.starttgt + offset + for _=1,fuzz do + table.remove(h.text, 1) + table.remove(h.text, #h.text) + end + return true + end + lineno = i + end + end + return false +end + +local function load_file(filename) + local fp = assert(io.open(filename)) + local file = {} + local readline = file_lines(fp) + while true do + local line = readline() + if not line then break end + table.insert(file, line) + end + fp:close() + return file +end + +local function find_hunks(file, hunks) + for hno, h in ipairs(hunks) do + find_hunk(file, h, hno) + end +end + +local function check_patched(file, hunks) + local lineno = 1 + local ok, err = pcall(function() + if #file == 0 then + error('nomatch', 0) + end + for hno, h in ipairs(hunks) do + -- skip to line just before hunk starts + if #file < h.starttgt then + error('nomatch', 0) + end + lineno = h.starttgt + for _, hline in ipairs(h.text) do + -- todo: \ No newline at the end of file + if not startswith(hline, "-") and not startswith(hline, "\\") then + local line = file[lineno] + lineno = lineno + 1 + if #line == 0 then + error('nomatch', 0) + end + if endlstrip(line) ~= endlstrip(hline:sub(2)) then + warning(format("file is not patched - failed hunk: %d", hno)) + error('nomatch', 0) + end + end + end + end + end) + -- todo: display failed hunk, i.e. expected/found + return err ~= 'nomatch' +end + +local function patch_hunks(srcname, tgtname, hunks) + local src = assert(io.open(srcname, "rb")) + local tgt = assert(io.open(tgtname, "wb")) + + local src_readline = file_lines(src) + + -- todo: detect linefeeds early - in apply_files routine + -- to handle cases when patch starts right from the first + -- line and no lines are processed. At the moment substituted + -- lineends may not be the same at the start and at the end + -- of patching. Also issue a warning about mixed lineends + + local srclineno = 1 + local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0} + for hno, h in ipairs(hunks) do + debug(format("processing hunk %d for file %s", hno, tgtname)) + -- skip to line just before hunk starts + while srclineno < h.startsrc do + local line = src_readline() + -- Python 'U' mode works only with text files + if endswith(line, "\r\n") then + lineends["\r\n"] = lineends["\r\n"] + 1 + elseif endswith(line, "\n") then + lineends["\n"] = lineends["\n"] + 1 + elseif endswith(line, "\r") then + lineends["\r"] = lineends["\r"] + 1 + end + tgt:write(line) + srclineno = srclineno + 1 + end + + for _,hline in ipairs(h.text) do + -- todo: check \ No newline at the end of file + if startswith(hline, "-") or startswith(hline, "\\") then + src_readline() + srclineno = srclineno + 1 + else + if not startswith(hline, "+") then + src_readline() + srclineno = srclineno + 1 + end + local line2write = hline:sub(2) + -- detect if line ends are consistent in source file + local sum = 0 + for _,v in pairs(lineends) do if v > 0 then sum=sum+1 end end + if sum == 1 then + local newline + for k,v in pairs(lineends) do if v ~= 0 then newline = k end end + tgt:write(endlstrip(line2write) .. newline) + else -- newlines are mixed or unknown + tgt:write(line2write) + end + end + end + end + for line in src_readline do + tgt:write(line) + end + tgt:close() + src:close() + return true +end + +local function strip_dirs(filename, strip) + if strip == nil then return filename end + for _=1,strip do + filename=filename:gsub("^[^/]*/", "") + end + return filename +end + +local function write_new_file(filename, hunk) + local fh = io.open(filename, "wb") + if not fh then return false end + for _, hline in ipairs(hunk.text) do + local c = hline:sub(1,1) + if c ~= "+" and c ~= "-" and c ~= " " then + return false, "malformed patch" + end + fh:write(hline:sub(2)) + end + fh:close() + return true +end + +local function patch_file(source, target, epoch, hunks, strip, create_delete) + local create_file = false + if create_delete then + local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0 + if is_src_epoch or source == "/dev/null" then + info(format("will create %s", target)) + create_file = true + end + end + if create_file then + return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1]) + end + source = strip_dirs(source, strip) + local f2patch = source + if not exists(f2patch) then + f2patch = strip_dirs(target, strip) + f2patch = fs.absolute_name(f2patch) + if not exists(f2patch) then --FIX:if f2patch nil + warning(format("source/target file does not exist\n--- %s\n+++ %s", + source, f2patch)) + return false + end + end + if not isfile(f2patch) then + warning(format("not a file - %s", f2patch)) + return false + end + + source = f2patch + + -- validate before patching + local file = load_file(source) + local hunkno = 1 + local hunk = hunks[hunkno] + local hunkfind = {} + local validhunks = 0 + local canpatch = false + local hunklineno + if not file then + return nil, "failed reading file " .. source + end + + if create_delete then + if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then + local ok = os.remove(source) + if not ok then + return false + end + info(format("successfully removed %s", source)) + return true + end + end + + find_hunks(file, hunks) + + local function process_line(line, lineno) + if not hunk or lineno < hunk.startsrc then + return false + end + if lineno == hunk.startsrc then + hunkfind = {} + for _,x in ipairs(hunk.text) do + if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then + hunkfind[#hunkfind+1] = endlstrip(x:sub(2)) + end + end + hunklineno = 1 + + -- todo \ No newline at end of file + end + -- check hunks in source file + if lineno < hunk.startsrc + #hunkfind - 1 then + if endlstrip(line) == hunkfind[hunklineno] then + hunklineno = hunklineno + 1 + else + debug(format("hunk no.%d doesn't match source file %s", + hunkno, source)) + -- file may be already patched, but check other hunks anyway + hunkno = hunkno + 1 + if hunkno <= #hunks then + hunk = hunks[hunkno] + return false + else + return true + end + end + end + -- check if processed line is the last line + if lineno == hunk.startsrc + #hunkfind - 1 then + debug(format("file %s hunk no.%d -- is ready to be patched", + source, hunkno)) + hunkno = hunkno + 1 + validhunks = validhunks + 1 + if hunkno <= #hunks then + hunk = hunks[hunkno] + else + if validhunks == #hunks then + -- patch file + canpatch = true + return true + end + end + end + return false + end + + local done = false + for lineno, line in ipairs(file) do + done = process_line(line, lineno) + if done then + break + end + end + if not done then + if hunkno <= #hunks and not create_file then + warning(format("premature end of source file %s at hunk %d", + source, hunkno)) + return false + end + end + if validhunks < #hunks then + if check_patched(file, hunks) then + warning(format("already patched %s", source)) + elseif not create_file then + warning(format("source file is different - %s", source)) + return false + end + end + if not canpatch then + return true + end + local backupname = source .. ".orig" + if exists(backupname) then + warning(format("can't backup original file to %s - aborting", + backupname)) + return false + end + local ok = os.rename(source, backupname) + if not ok then + warning(format("failed backing up %s when patching", source)) + return false + end + patch_hunks(backupname, source, hunks) + info(format("successfully patched %s", source)) + os.remove(backupname) + return true +end + +function patch.apply_patch(the_patch, strip, create_delete) + local all_ok = true + local total = #the_patch.source + for fileno, source in ipairs(the_patch.source) do + local target = the_patch.target[fileno] + local hunks = the_patch.hunks[fileno] + local epoch = the_patch.epoch[fileno] + info(format("processing %d/%d:\t %s", fileno, total, source)) + local ok = patch_file(source, target, epoch, hunks, strip, create_delete) + all_ok = all_ok and ok + end + -- todo: check for premature eof + return all_ok +end + +return patch diff --git a/src/luarocks/tools/tar.lua b/src/luarocks/tools/tar.lua new file mode 100644 index 0000000..bac7b2a --- /dev/null +++ b/src/luarocks/tools/tar.lua @@ -0,0 +1,191 @@ + +--- A pure-Lua implementation of untar (unpacking .tar archives) +local tar = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local fun = require("luarocks.fun") + +local blocksize = 512 + +local function get_typeflag(flag) + if flag == "0" or flag == "\0" then return "file" + elseif flag == "1" then return "link" + elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU + elseif flag == "3" then return "character" + elseif flag == "4" then return "block" + elseif flag == "5" then return "directory" + elseif flag == "6" then return "fifo" + elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU + elseif flag == "x" then return "next file" + elseif flag == "g" then return "global extended header" + elseif flag == "L" then return "long name" + elseif flag == "K" then return "long link name" + end + return "unknown" +end + +local function octal_to_number(octal) + local exp = 0 + local number = 0 + octal = octal:gsub("%s", "") + for i = #octal,1,-1 do + local digit = tonumber(octal:sub(i,i)) + if not digit then + break + end + number = number + (digit * 8^exp) + exp = exp + 1 + end + return number +end + +local function checksum_header(block) + local sum = 256 + + if block:byte(1) == 0 then + return 0 + end + + for i = 1,148 do + local b = block:byte(i) or 0 + sum = sum + b + end + for i = 157,500 do + local b = block:byte(i) or 0 + sum = sum + b + end + + return sum +end + +local function nullterm(s) + return s:match("^[^%z]*") +end + +local function read_header_block(block) + local header = {} + header.name = nullterm(block:sub(1,100)) + header.mode = nullterm(block:sub(101,108)):gsub(" ", "") + header.uid = octal_to_number(nullterm(block:sub(109,116))) + header.gid = octal_to_number(nullterm(block:sub(117,124))) + header.size = octal_to_number(nullterm(block:sub(125,136))) + header.mtime = octal_to_number(nullterm(block:sub(137,148))) + header.chksum = octal_to_number(nullterm(block:sub(149,156))) + header.typeflag = get_typeflag(block:sub(157,157)) + header.linkname = nullterm(block:sub(158,257)) + header.magic = block:sub(258,263) + header.version = block:sub(264,265) + header.uname = nullterm(block:sub(266,297)) + header.gname = nullterm(block:sub(298,329)) + header.devmajor = octal_to_number(nullterm(block:sub(330,337))) + header.devminor = octal_to_number(nullterm(block:sub(338,345))) + header.prefix = block:sub(346,500) + + -- if header.magic ~= "ustar " and header.magic ~= "ustar\0" then + -- return false, ("Invalid header magic %6x"):format(bestring_to_number(header.magic)) + -- end + -- if header.version ~= "00" and header.version ~= " \0" then + -- return false, "Unknown version "..header.version + -- end + if header.typeflag == "unknown" then + if checksum_header(block) ~= header.chksum then + return false, "Failed header checksum" + end + end + return header +end + +function tar.untar(filename, destdir) + assert(type(filename) == "string") + assert(type(destdir) == "string") + + local tar_handle = io.open(filename, "rb") + if not tar_handle then return nil, "Error opening file "..filename end + + local long_name, long_link_name + local ok, err + local make_dir = fun.memoize(fs.make_dir) + while true do + local block + repeat + block = tar_handle:read(blocksize) + until (not block) or block:byte(1) > 0 + if not block then break end + if #block < blocksize then + ok, err = nil, "Invalid block size -- corrupted file?" + break + end + + local header + header, err = read_header_block(block) + if not header then + ok = false + break + end + + local file_data = "" + if header.size > 0 then + local nread = math.ceil(header.size / blocksize) * blocksize + file_data = tar_handle:read(header.size) + if nread > header.size then + tar_handle:seek("cur", nread - header.size) + end + end + + if header.typeflag == "long name" then + long_name = nullterm(file_data) + elseif header.typeflag == "long link name" then + long_link_name = nullterm(file_data) + else + if long_name then + header.name = long_name + long_name = nil + end + if long_link_name then + header.name = long_link_name + long_link_name = nil + end + end + local pathname = dir.path(destdir, header.name) + pathname = fs.absolute_name(pathname) + if header.typeflag == "directory" then + ok, err = make_dir(pathname) + if not ok then + break + end + elseif header.typeflag == "file" then + local dirname = dir.dir_name(pathname) + if dirname ~= "" then + ok, err = make_dir(dirname) + if not ok then + break + end + end + local file_handle + file_handle, err = io.open(pathname, "wb") + if not file_handle then + ok = nil + break + end + file_handle:write(file_data) + file_handle:close() + fs.set_time(pathname, header.mtime) + if header.mode:match("[75]") then + fs.set_permissions(pathname, "exec", "all") + else + fs.set_permissions(pathname, "read", "all") + end + end + --[[ + for k,v in pairs(header) do + util.printout("[\""..tostring(k).."\"] = "..(type(v)=="number" and v or "\""..v:gsub("%z", "\\0").."\"")) + end + util.printout() + --]] + end + tar_handle:close() + return ok, err +end + +return tar diff --git a/src/luarocks/tools/zip.lua b/src/luarocks/tools/zip.lua new file mode 100644 index 0000000..82d582f --- /dev/null +++ b/src/luarocks/tools/zip.lua @@ -0,0 +1,531 @@ + +--- A Lua implementation of .zip and .gz file compression and decompression, +-- using only lzlib or lua-lzib. +local zip = {} + +local zlib = require("zlib") +local fs = require("luarocks.fs") +local fun = require("luarocks.fun") +local dir = require("luarocks.dir") + +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +local function shr(n, m) + return math.floor(n / 2^m) +end + +local function shl(n, m) + return n * 2^m +end +local function lowbits(n, m) + return n % 2^m +end + +local function mode_to_windowbits(mode) + if mode == "gzip" then + return 31 + elseif mode == "zlib" then + return 0 + elseif mode == "raw" then + return -15 + end +end + +-- zlib module can be provided by both lzlib and lua-lzib packages. +-- Create a compatibility layer. +local zlib_compress, zlib_uncompress, zlib_crc32 +if zlib._VERSION:match "^lua%-zlib" then + function zlib_compress(data, mode) + return (zlib.deflate(6, mode_to_windowbits(mode))(data, "finish")) + end + + function zlib_uncompress(data, mode) + return (zlib.inflate(mode_to_windowbits(mode))(data)) + end + + function zlib_crc32(data) + return zlib.crc32()(data) + end +elseif zlib._VERSION:match "^lzlib" then + function zlib_compress(data, mode) + return zlib.compress(data, -1, nil, mode_to_windowbits(mode)) + end + + function zlib_uncompress(data, mode) + return zlib.decompress(data, mode_to_windowbits(mode)) + end + + function zlib_crc32(data) + return zlib.crc32(zlib.crc32(), data) + end +else + error("unknown zlib library", 0) +end + +local function number_to_lestring(number, nbytes) + local out = {} + for _ = 1, nbytes do + local byte = number % 256 + table.insert(out, string.char(byte)) + number = (number - byte) / 256 + end + return table.concat(out) +end + +local function lestring_to_number(str) + local n = 0 + local bytes = { string.byte(str, 1, #str) } + for b = 1, #str do + n = n + shl(bytes[b], (b-1)*8) + end + return math.floor(n) +end + +local LOCAL_FILE_HEADER_SIGNATURE = number_to_lestring(0x04034b50, 4) +local DATA_DESCRIPTOR_SIGNATURE = number_to_lestring(0x08074b50, 4) +local CENTRAL_DIRECTORY_SIGNATURE = number_to_lestring(0x02014b50, 4) +local END_OF_CENTRAL_DIR_SIGNATURE = number_to_lestring(0x06054b50, 4) + +--- Begin a new file to be stored inside the zipfile. +-- @param self handle of the zipfile being written. +-- @param filename filenome of the file to be added to the zipfile. +-- @return true if succeeded, nil in case of failure. +local function zipwriter_open_new_file_in_zip(self, filename) + if self.in_open_file then + self:close_file_in_zip() + return nil + end + local lfh = {} + self.local_file_header = lfh + lfh.last_mod_file_time = 0 -- TODO + lfh.last_mod_file_date = 0 -- TODO + lfh.file_name_length = #filename + lfh.extra_field_length = 0 + lfh.file_name = filename:gsub("\\", "/") + lfh.external_attr = shl(493, 16) -- TODO proper permissions + self.in_open_file = true + return true +end + +--- Write data to the file currently being stored in the zipfile. +-- @param self handle of the zipfile being written. +-- @param data string containing full contents of the file. +-- @return true if succeeded, nil in case of failure. +local function zipwriter_write_file_in_zip(self, data) + if not self.in_open_file then + return nil + end + local lfh = self.local_file_header + local compressed = zlib_compress(data, "raw") + lfh.crc32 = zlib_crc32(data) + lfh.compressed_size = #compressed + lfh.uncompressed_size = #data + self.data = compressed + return true +end + +--- Complete the writing of a file stored in the zipfile. +-- @param self handle of the zipfile being written. +-- @return true if succeeded, nil in case of failure. +local function zipwriter_close_file_in_zip(self) + local zh = self.ziphandle + + if not self.in_open_file then + return nil + end + + -- Local file header + local lfh = self.local_file_header + lfh.offset = zh:seek() + zh:write(LOCAL_FILE_HEADER_SIGNATURE) + zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0 + zh:write(number_to_lestring(4, 2)) -- general purpose bit flag + zh:write(number_to_lestring(8, 2)) -- compression method: deflate + zh:write(number_to_lestring(lfh.last_mod_file_time, 2)) + zh:write(number_to_lestring(lfh.last_mod_file_date, 2)) + zh:write(number_to_lestring(lfh.crc32, 4)) + zh:write(number_to_lestring(lfh.compressed_size, 4)) + zh:write(number_to_lestring(lfh.uncompressed_size, 4)) + zh:write(number_to_lestring(lfh.file_name_length, 2)) + zh:write(number_to_lestring(lfh.extra_field_length, 2)) + zh:write(lfh.file_name) + + -- File data + zh:write(self.data) + + -- Data descriptor + zh:write(DATA_DESCRIPTOR_SIGNATURE) + zh:write(number_to_lestring(lfh.crc32, 4)) + zh:write(number_to_lestring(lfh.compressed_size, 4)) + zh:write(number_to_lestring(lfh.uncompressed_size, 4)) + + table.insert(self.files, lfh) + self.in_open_file = false + + return true +end + +-- @return boolean or (boolean, string): true on success, +-- false and an error message on failure. +local function zipwriter_add(self, file) + local fin + local ok, err = self:open_new_file_in_zip(file) + if not ok then + err = "error in opening "..file.." in zipfile" + else + fin = io.open(fs.absolute_name(file), "rb") + if not fin then + ok = false + err = "error opening "..file.." for reading" + end + end + if ok then + local data = fin:read("*a") + if not data then + err = "error reading "..file + ok = false + else + ok = self:write_file_in_zip(data) + if not ok then + err = "error in writing "..file.." in the zipfile" + end + end + end + if fin then + fin:close() + end + if ok then + ok = self:close_file_in_zip() + if not ok then + err = "error in writing "..file.." in the zipfile" + end + end + return ok == true, err +end + +--- Complete the writing of the zipfile. +-- @param self handle of the zipfile being written. +-- @return true if succeeded, nil in case of failure. +local function zipwriter_close(self) + local zh = self.ziphandle + + local central_directory_offset = zh:seek() + + local size_of_central_directory = 0 + -- Central directory structure + for _, lfh in ipairs(self.files) do + zh:write(CENTRAL_DIRECTORY_SIGNATURE) -- signature + zh:write(number_to_lestring(3, 2)) -- version made by: UNIX + zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0 + zh:write(number_to_lestring(0, 2)) -- general purpose bit flag + zh:write(number_to_lestring(8, 2)) -- compression method: deflate + zh:write(number_to_lestring(lfh.last_mod_file_time, 2)) + zh:write(number_to_lestring(lfh.last_mod_file_date, 2)) + zh:write(number_to_lestring(lfh.crc32, 4)) + zh:write(number_to_lestring(lfh.compressed_size, 4)) + zh:write(number_to_lestring(lfh.uncompressed_size, 4)) + zh:write(number_to_lestring(lfh.file_name_length, 2)) + zh:write(number_to_lestring(lfh.extra_field_length, 2)) + zh:write(number_to_lestring(0, 2)) -- file comment length + zh:write(number_to_lestring(0, 2)) -- disk number start + zh:write(number_to_lestring(0, 2)) -- internal file attributes + zh:write(number_to_lestring(lfh.external_attr, 4)) -- external file attributes + zh:write(number_to_lestring(lfh.offset, 4)) -- relative offset of local header + zh:write(lfh.file_name) + size_of_central_directory = size_of_central_directory + 46 + lfh.file_name_length + end + + -- End of central directory record + zh:write(END_OF_CENTRAL_DIR_SIGNATURE) -- signature + zh:write(number_to_lestring(0, 2)) -- number of this disk + zh:write(number_to_lestring(0, 2)) -- number of disk with start of central directory + zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir on this disk + zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir + zh:write(number_to_lestring(size_of_central_directory, 4)) + zh:write(number_to_lestring(central_directory_offset, 4)) + zh:write(number_to_lestring(0, 2)) -- zip file comment length + zh:close() + + return true +end + +--- Return a zip handle open for writing. +-- @param name filename of the zipfile to be created. +-- @return a zip handle, or nil in case of error. +function zip.new_zipwriter(name) + + local zw = {} + + zw.ziphandle = io.open(fs.absolute_name(name), "wb") + if not zw.ziphandle then + return nil + end + zw.files = {} + zw.in_open_file = false + + zw.add = zipwriter_add + zw.close = zipwriter_close + zw.open_new_file_in_zip = zipwriter_open_new_file_in_zip + zw.write_file_in_zip = zipwriter_write_file_in_zip + zw.close_file_in_zip = zipwriter_close_file_in_zip + + return zw +end + +--- Compress files in a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @param ... Filenames to be stored in the archive are given as +-- additional arguments. +-- @return boolean or (boolean, string): true on success, +-- false and an error message on failure. +function zip.zip(zipfile, ...) + local zw = zip.new_zipwriter(zipfile) + if not zw then + return nil, "error opening "..zipfile + end + + local args = pack(...) + local ok, err + for i=1, args.n do + local file = args[i] + if fs.is_dir(file) then + for _, entry in pairs(fs.find(file)) do + local fullname = dir.path(file, entry) + if fs.is_file(fullname) then + ok, err = zw:add(fullname) + if not ok then break end + end + end + else + ok, err = zw:add(file) + if not ok then break end + end + end + + zw:close() + return ok, err +end + + +local function ziptime_to_luatime(ztime, zdate) + local date = { + year = shr(zdate, 9) + 1980, + month = shr(lowbits(zdate, 9), 5), + day = lowbits(zdate, 5), + hour = shr(ztime, 11), + min = shr(lowbits(ztime, 11), 5), + sec = lowbits(ztime, 5) * 2, + } + + if date.month == 0 then date.month = 1 end + if date.day == 0 then date.day = 1 end + + return date +end + +local function read_file_in_zip(zh, cdr) + local sig = zh:read(4) + if sig ~= LOCAL_FILE_HEADER_SIGNATURE then + return nil, "failed reading Local File Header signature" + end + + -- Skip over the rest of the zip file header. See + -- zipwriter_close_file_in_zip for the format. + zh:seek("cur", 22) + local file_name_length = lestring_to_number(zh:read(2)) + local extra_field_length = lestring_to_number(zh:read(2)) + zh:read(file_name_length) + zh:read(extra_field_length) + + local data = zh:read(cdr.compressed_size) + + local uncompressed + if cdr.compression_method == 8 then + uncompressed = zlib_uncompress(data, "raw") + elseif cdr.compression_method == 0 then + uncompressed = data + else + return nil, "unknown compression method " .. cdr.compression_method + end + + if #uncompressed ~= cdr.uncompressed_size then + return nil, "uncompressed size doesn't match" + end + if cdr.crc32 ~= zlib_crc32(uncompressed) then + return nil, "crc32 failed (expected " .. cdr.crc32 .. ") - data: " .. uncompressed + end + + return uncompressed +end + +local function process_end_of_central_dir(zh) + local at, err = zh:seek("end", -22) + if not at then + return nil, err + end + + while true do + local sig = zh:read(4) + if sig == END_OF_CENTRAL_DIR_SIGNATURE then + break + end + at = at - 1 + local at1, err = zh:seek("set", at) + if at1 ~= at then + return nil, "Could not find End of Central Directory signature" + end + end + + -- number of this disk (2 bytes) + -- number of the disk with the start of the central directory (2 bytes) + -- total number of entries in the central directory on this disk (2 bytes) + -- total number of entries in the central directory (2 bytes) + zh:seek("cur", 6) + + local central_directory_entries = lestring_to_number(zh:read(2)) + + -- central directory size (4 bytes) + zh:seek("cur", 4) + + local central_directory_offset = lestring_to_number(zh:read(4)) + + return central_directory_entries, central_directory_offset +end + +local function process_central_dir(zh, cd_entries) + + local files = {} + + for i = 1, cd_entries do + local sig = zh:read(4) + if sig ~= CENTRAL_DIRECTORY_SIGNATURE then + return nil, "failed reading Central Directory signature" + end + + local cdr = {} + files[i] = cdr + + cdr.version_made_by = lestring_to_number(zh:read(2)) + cdr.version_needed = lestring_to_number(zh:read(2)) + cdr.bitflag = lestring_to_number(zh:read(2)) + cdr.compression_method = lestring_to_number(zh:read(2)) + cdr.last_mod_file_time = lestring_to_number(zh:read(2)) + cdr.last_mod_file_date = lestring_to_number(zh:read(2)) + cdr.last_mod_luatime = ziptime_to_luatime(cdr.last_mod_file_time, cdr.last_mod_file_date) + cdr.crc32 = lestring_to_number(zh:read(4)) + cdr.compressed_size = lestring_to_number(zh:read(4)) + cdr.uncompressed_size = lestring_to_number(zh:read(4)) + cdr.file_name_length = lestring_to_number(zh:read(2)) + cdr.extra_field_length = lestring_to_number(zh:read(2)) + cdr.file_comment_length = lestring_to_number(zh:read(2)) + cdr.disk_number_start = lestring_to_number(zh:read(2)) + cdr.internal_attr = lestring_to_number(zh:read(2)) + cdr.external_attr = lestring_to_number(zh:read(4)) + cdr.offset = lestring_to_number(zh:read(4)) + cdr.file_name = zh:read(cdr.file_name_length) + cdr.extra_field = zh:read(cdr.extra_field_length) + cdr.file_comment = zh:read(cdr.file_comment_length) + end + return files +end + +--- Uncompress files from a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @return boolean or (boolean, string): true on success, +-- false and an error message on failure. +function zip.unzip(zipfile) + zipfile = fs.absolute_name(zipfile) + local zh, err = io.open(zipfile, "rb") + if not zh then + return nil, err + end + + local cd_entries, cd_offset = process_end_of_central_dir(zh) + if not cd_entries then + return nil, cd_offset + end + + local ok, err = zh:seek("set", cd_offset) + if not ok then + return nil, err + end + + local files, err = process_central_dir(zh, cd_entries) + if not files then + return nil, err + end + + for _, cdr in ipairs(files) do + local file = cdr.file_name + if file:sub(#file) == "/" then + local ok, err = fs.make_dir(dir.path(fs.current_dir(), file)) + if not ok then + return nil, err + end + else + local base = dir.dir_name(file) + if base ~= "" then + base = dir.path(fs.current_dir(), base) + if not fs.is_dir(base) then + local ok, err = fs.make_dir(base) + if not ok then + return nil, err + end + end + end + + local ok, err = zh:seek("set", cdr.offset) + if not ok then + return nil, err + end + + local contents, err = read_file_in_zip(zh, cdr) + if not contents then + return nil, err + end + local pathname = dir.path(fs.current_dir(), file) + local wf, err = io.open(pathname, "wb") + if not wf then + zh:close() + return nil, err + end + wf:write(contents) + wf:close() + + if cdr.external_attr > 0 then + fs.set_permissions(pathname, "exec", "all") + else + fs.set_permissions(pathname, "read", "all") + end + fs.set_time(pathname, cdr.last_mod_luatime) + end + end + zh:close() + return true +end + +function zip.gzip(input_filename, output_filename) + assert(type(input_filename) == "string") + assert(output_filename == nil or type(output_filename) == "string") + + if not output_filename then + output_filename = input_filename .. ".gz" + end + + local fn = fun.partial(fun.flip(zlib_compress), "gzip") + return fs.filter_file(fn, input_filename, output_filename) +end + +function zip.gunzip(input_filename, output_filename) + assert(type(input_filename) == "string") + assert(output_filename == nil or type(output_filename) == "string") + + if not output_filename then + output_filename = input_filename:gsub("%.gz$", "") + end + + local fn = fun.partial(fun.flip(zlib_uncompress), "gzip") + return fs.filter_file(fn, input_filename, output_filename) +end + +return zip diff --git a/src/luarocks/type/manifest.lua b/src/luarocks/type/manifest.lua new file mode 100644 index 0000000..043366e --- /dev/null +++ b/src/luarocks/type/manifest.lua @@ -0,0 +1,80 @@ +local type_manifest = {} + +local type_check = require("luarocks.type_check") + +local manifest_formats = type_check.declare_schemas({ + ["3.0"] = { + repository = { + _mandatory = true, + -- packages + _any = { + -- versions + _any = { + -- items + _any = { + arch = { _type = "string", _mandatory = true }, + modules = { _any = { _type = "string" } }, + commands = { _any = { _type = "string" } }, + dependencies = { _any = { _type = "string" } }, + -- TODO: to be extended with more metadata. + } + } + } + }, + modules = { + _mandatory = true, + -- modules + _any = { + -- providers + _any = { _type = "string" } + } + }, + commands = { + _mandatory = true, + -- modules + _any = { + -- commands + _any = { _type = "string" } + } + }, + dependencies = { + -- each module + _any = { + -- each version + _any = { + -- each dependency + _any = { + name = { _type = "string" }, + namespace = { _type = "string" }, + constraints = { + _any = { + no_upgrade = { _type = "boolean" }, + op = { _type = "string" }, + version = { + string = { _type = "string" }, + _any = { _type = "number" }, + } + } + } + } + } + } + } + } +}) + +--- Type check a manifest table. +-- Verify the correctness of elements from a +-- manifest table, reporting on unknown fields and type +-- mismatches. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +function type_manifest.check(manifest, globals) + assert(type(manifest) == "table") + local format = manifest_formats["3.0"] + local ok, err = type_check.check_undeclared_globals(globals, format) + if not ok then return nil, err end + return type_check.type_check_table("3.0", manifest, format, "") +end + +return type_manifest diff --git a/src/luarocks/type/rockspec.lua b/src/luarocks/type/rockspec.lua new file mode 100644 index 0000000..0b4b5dc --- /dev/null +++ b/src/luarocks/type/rockspec.lua @@ -0,0 +1,199 @@ +local type_rockspec = {} + +local type_check = require("luarocks.type_check") + +type_rockspec.rockspec_format = "3.0" + +-- Syntax for type-checking tables: +-- +-- A type-checking table describes typing data for a value. +-- Any key starting with an underscore has a special meaning: +-- _type (string) is the Lua type of the value. Default is "table". +-- _mandatory (boolean) indicates if the value is a mandatory key in its container table. Default is false. +-- For "string" types only: +-- _pattern (string) is the string-matching pattern, valid for string types only. Default is ".*". +-- For "table" types only: +-- _any (table) is the type-checking table for unspecified keys, recursively checked. +-- _more (boolean) indicates that the table accepts unspecified keys and does not type-check them. +-- Any other string keys that don't start with an underscore represent known keys and are type-checking tables, recursively checked. + +local rockspec_formats, versions = type_check.declare_schemas({ + ["1.0"] = { + rockspec_format = { _type = "string" }, + package = { _type = "string", _mandatory = true }, + version = { _type = "string", _pattern = "[%w.]+-[%d]+", _mandatory = true }, + description = { + summary = { _type = "string" }, + detailed = { _type = "string" }, + homepage = { _type = "string" }, + license = { _type = "string" }, + maintainer = { _type = "string" }, + }, + dependencies = { + platforms = type_check.MAGIC_PLATFORMS, + _any = { + _type = "string", + _name = "a valid dependency string", + _pattern = "%s*([a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)", + }, + }, + supported_platforms = { + _any = { _type = "string" }, + }, + external_dependencies = { + platforms = type_check.MAGIC_PLATFORMS, + _any = { + program = { _type = "string" }, + header = { _type = "string" }, + library = { _type = "string" }, + } + }, + source = { + _mandatory = true, + platforms = type_check.MAGIC_PLATFORMS, + url = { _type = "string", _mandatory = true }, + md5 = { _type = "string" }, + file = { _type = "string" }, + dir = { _type = "string" }, + tag = { _type = "string" }, + branch = { _type = "string" }, + module = { _type = "string" }, + cvs_tag = { _type = "string" }, + cvs_module = { _type = "string" }, + }, + build = { + platforms = type_check.MAGIC_PLATFORMS, + type = { _type = "string" }, + install = { + lua = { + _more = true + }, + lib = { + _more = true + }, + conf = { + _more = true + }, + bin = { + _more = true + } + }, + copy_directories = { + _any = { _type = "string" }, + }, + _more = true, + _mandatory = true + }, + hooks = { + platforms = type_check.MAGIC_PLATFORMS, + post_install = { _type = "string" }, + }, + }, + + ["1.1"] = { + deploy = { + wrap_bin_scripts = { _type = "boolean" }, + } + }, + + ["3.0"] = { + description = { + labels = { + _any = { _type = "string" } + }, + issues_url = { _type = "string" }, + }, + dependencies = { + _any = { + _pattern = "%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)", + }, + }, + build_dependencies = { + platforms = type_check.MAGIC_PLATFORMS, + _any = { + _type = "string", + _name = "a valid dependency string", + _pattern = "%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)", + }, + }, + test_dependencies = { + platforms = type_check.MAGIC_PLATFORMS, + _any = { + _type = "string", + _name = "a valid dependency string", + _pattern = "%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)", + }, + }, + build = { + _mandatory = false, + }, + test = { + platforms = type_check.MAGIC_PLATFORMS, + type = { _type = "string" }, + _more = true, + }, + } +}) + +type_rockspec.order = {"rockspec_format", "package", "version", + { "source", { "url", "tag", "branch", "md5" } }, + { "description", {"summary", "detailed", "homepage", "license" } }, + "supported_platforms", "dependencies", "build_dependencies", "external_dependencies", + { "build", {"type", "modules", "copy_directories", "platforms"} }, + "test_dependencies", { "test", {"type"} }, + "hooks"} + +local function check_rockspec_using_version(rockspec, globals, version) + local schema = rockspec_formats[version] + if not schema then + return nil, "unknown rockspec format " .. version + end + local ok, err = type_check.check_undeclared_globals(globals, schema) + if ok then + ok, err = type_check.type_check_table(version, rockspec, schema, "") + end + if ok then + return true + else + return nil, err + end +end + +--- Type check a rockspec table. +-- Verify the correctness of elements from a +-- rockspec table, reporting on unknown fields and type +-- mismatches. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +function type_rockspec.check(rockspec, globals) + assert(type(rockspec) == "table") + + local version = rockspec.rockspec_format or "1.0" + local ok, err = check_rockspec_using_version(rockspec, globals, version) + if ok then + return true + end + + -- Rockspec parsing failed. + -- Let's see if it would pass using a later version. + + local found = false + for _, v in ipairs(versions) do + if not found then + if v == version then + found = true + end + else + local v_ok, v_err = check_rockspec_using_version(rockspec, globals, v) + if v_ok then + return nil, err .. " (using rockspec format " .. version .. " -- " .. + [[adding 'rockspec_format = "]] .. v .. [["' to the rockspec ]] .. + [[will fix this)]] + end + end + end + + return nil, err .. " (using rockspec format " .. version .. ")" +end + +return type_rockspec diff --git a/src/luarocks/type_check.lua b/src/luarocks/type_check.lua new file mode 100644 index 0000000..21085ef --- /dev/null +++ b/src/luarocks/type_check.lua @@ -0,0 +1,213 @@ + +local type_check = {} + +local cfg = require("luarocks.core.cfg") +local fun = require("luarocks.fun") +local util = require("luarocks.util") +local vers = require("luarocks.core.vers") +-------------------------------------------------------------------------------- + +-- A magic constant that is not used anywhere in a schema definition +-- and retains equality when the table is deep-copied. +type_check.MAGIC_PLATFORMS = 0xEBABEFAC + +do + local function fill_in_version(tbl, version) + for _, v in pairs(tbl) do + if type(v) == "table" then + if v._version == nil then + v._version = version + end + fill_in_version(v) + end + end + end + + local function expand_magic_platforms(tbl) + for k,v in pairs(tbl) do + if v == type_check.MAGIC_PLATFORMS then + tbl[k] = { + _any = util.deep_copy(tbl) + } + tbl[k]._any[k] = nil + elseif type(v) == "table" then + expand_magic_platforms(v) + end + end + end + + -- Build a table of schemas. + -- @param versions a table where each key is a version number as a string, + -- and the value is a schema specification. Schema versions are considered + -- incremental: version "2.0" only needs to specify what's new/changed from + -- version "1.0". + function type_check.declare_schemas(inputs) + local schemas = {} + local parent_version + + local versions = fun.reverse_in(fun.sort_in(util.keys(inputs), vers.compare_versions)) + + for _, version in ipairs(versions) do + local schema = inputs[version] + if parent_version ~= nil then + local copy = util.deep_copy(schemas[parent_version]) + util.deep_merge(copy, schema) + schema = copy + end + fill_in_version(schema, version) + expand_magic_platforms(schema) + parent_version = version + schemas[version] = schema + end + + return schemas, versions + end +end + +-------------------------------------------------------------------------------- + +local function check_version(version, typetbl, context) + local typetbl_version = typetbl._version or "1.0" + if vers.compare_versions(typetbl_version, version) then + if context == "" then + return nil, "Invalid rockspec_format version number in rockspec? Please fix rockspec accordingly." + else + return nil, context.." is not supported in rockspec format "..version.." (requires version "..typetbl_version.."), please fix the rockspec_format field accordingly." + end + end + return true +end + +--- Type check an object. +-- The object is compared against an archetypical value +-- matching the expected type -- the actual values don't matter, +-- only their types. Tables are type checked recursively. +-- @param version string: The version of the item. +-- @param item any: The object being checked. +-- @param typetbl any: The type-checking table for the object. +-- @param context string: A string indicating the "context" where the +-- error occurred (the full table path), for error messages. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +-- @see type_check_table +local function type_check_item(version, item, typetbl, context) + assert(type(version) == "string") + + if typetbl._version and typetbl._version ~= "1.0" then + local ok, err = check_version(version, typetbl, context) + if not ok then + return nil, err + end + end + + local item_type = type(item) or "nil" + local expected_type = typetbl._type or "table" + + if expected_type == "number" then + if not tonumber(item) then + return nil, "Type mismatch on field "..context..": expected a number" + end + elseif expected_type == "string" then + if item_type ~= "string" then + return nil, "Type mismatch on field "..context..": expected a string, got "..item_type + end + local pattern = typetbl._pattern + if pattern then + if not item:match("^"..pattern.."$") then + local what = typetbl._name or ("'"..pattern.."'") + return nil, "Type mismatch on field "..context..": invalid value '"..item.."' does not match " .. what + end + end + elseif expected_type == "table" then + if item_type ~= expected_type then + return nil, "Type mismatch on field "..context..": expected a table" + else + return type_check.type_check_table(version, item, typetbl, context) + end + elseif item_type ~= expected_type then + return nil, "Type mismatch on field "..context..": expected "..expected_type + end + return true +end + +local function mkfield(context, field) + if context == "" then + return tostring(field) + elseif type(field) == "string" then + return context.."."..field + else + return context.."["..tostring(field).."]" + end +end + +--- Type check the contents of a table. +-- The table's contents are compared against a reference table, +-- which contains the recognized fields, with archetypical values +-- matching the expected types -- the actual values of items in the +-- reference table don't matter, only their types (ie, for field x +-- in tbl that is correctly typed, type(tbl.x) == type(types.x)). +-- If the reference table contains a field called MORE, then +-- unknown fields in the checked table are accepted. +-- If it contains a field called ANY, then its type will be +-- used to check any unknown fields. If a field is prefixed +-- with MUST_, it is mandatory; its absence from the table is +-- a type error. +-- Tables are type checked recursively. +-- @param version string: The version of tbl. +-- @param tbl table: The table to be type checked. +-- @param typetbl table: The type-checking table, containing +-- values for recognized fields in the checked table. +-- @param context string: A string indicating the "context" where the +-- error occurred (such as the name of the table the item is a part of), +-- to be used by error messages. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +function type_check.type_check_table(version, tbl, typetbl, context) + assert(type(version) == "string") + assert(type(tbl) == "table") + assert(type(typetbl) == "table") + + local ok, err = check_version(version, typetbl, context) + if not ok then + return nil, err + end + + for k, v in pairs(tbl) do + local t = typetbl[k] or typetbl._any + if t then + local ok, err = type_check_item(version, v, t, mkfield(context, k)) + if not ok then return nil, err end + elseif typetbl._more then + -- Accept unknown field + else + if not cfg.accept_unknown_fields then + return nil, "Unknown field "..k + end + end + end + for k, v in pairs(typetbl) do + if k:sub(1,1) ~= "_" and v._mandatory then + if not tbl[k] then + return nil, "Mandatory field "..mkfield(context, k).." is missing." + end + end + end + return true +end + +function type_check.check_undeclared_globals(globals, typetbl) + local undeclared = {} + for glob, _ in pairs(globals) do + if not (typetbl[glob] or typetbl["MUST_"..glob]) then + table.insert(undeclared, glob) + end + end + if #undeclared == 1 then + return nil, "Unknown variable: "..undeclared[1] + elseif #undeclared > 1 then + return nil, "Unknown variables: "..table.concat(undeclared, ", ") + end + return true +end + +return type_check diff --git a/src/luarocks/upload/api.lua b/src/luarocks/upload/api.lua new file mode 100644 index 0000000..e141370 --- /dev/null +++ b/src/luarocks/upload/api.lua @@ -0,0 +1,265 @@ + +local api = {} + +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local persist = require("luarocks.persist") +local multipart = require("luarocks.upload.multipart") +local json = require("luarocks.vendor.dkjson") +local dir_sep = package.config:sub(1, 1) + +local Api = {} + +local function upload_config_file() + if not cfg.config_files.user.file then + return nil + end + return (cfg.config_files.user.file:gsub("[\\/][^\\/]+$", dir_sep .. "upload_config.lua")) +end + +function Api:load_config() + local upload_conf = upload_config_file() + if not upload_conf then return nil end + local config, err = persist.load_into_table(upload_conf) + return config +end + +function Api:save_config() + -- Test configuration before saving it. + local res, err = self:raw_method("status") + if not res then + return nil, err + end + if res.errors then + util.printerr("Server says: " .. tostring(res.errors[1])) + return + end + local upload_conf = upload_config_file() + if not upload_conf then return nil end + local ok, err = fs.make_dir(dir.dir_name(upload_conf)) + if not ok then + return nil, err + end + persist.save_from_table(upload_conf, self.config) + fs.set_permissions(upload_conf, "read", "user") +end + +function Api:check_version() + if not self._server_tool_version then + local tool_version = cfg.upload.tool_version + local res, err = self:request(tostring(self.config.server) .. "/api/tool_version", { + current = tool_version + }) + if not res then + return nil, err + end + if not res.version then + return nil, "failed to fetch tool version" + end + self._server_tool_version = res.version + if res.force_update then + return nil, "Your upload client is too out of date to continue, please upgrade LuaRocks." + end + if res.version ~= tool_version then + util.warning("your LuaRocks is out of date, consider upgrading.") + end + end + return true +end + +function Api:method(...) + local res, err = self:raw_method(...) + if not res then + return nil, err + end + if res.errors then + if res.errors[1] == "Invalid key" then + return nil, res.errors[1] .. " (use the --api-key flag to change)" + end + local msg = table.concat(res.errors, ", ") + return nil, "API Failed: " .. msg + end + return res +end + +function Api:raw_method(path, ...) + self:check_version() + local url = tostring(self.config.server) .. "/api/" .. tostring(cfg.upload.api_version) .. "/" .. tostring(self.config.key) .. "/" .. tostring(path) + return self:request(url, ...) +end + +local function encode_query_string(t, sep) + if sep == nil then + sep = "&" + end + local i = 0 + local buf = { } + for k, v in pairs(t) do + if type(k) == "number" and type(v) == "table" then + k, v = v[1], v[2] + end + buf[i + 1] = multipart.url_escape(k) + buf[i + 2] = "=" + buf[i + 3] = multipart.url_escape(v) + buf[i + 4] = sep + i = i + 4 + end + buf[i] = nil + return table.concat(buf) +end + +local function redact_api_url(url) + url = tostring(url) + return (url:gsub(".*/api/[^/]+/[^/]+", "")) or "" +end + +local ltn12_ok, ltn12 = pcall(require, "ltn12") +if not ltn12_ok then -- If not using LuaSocket and/or LuaSec... + +function Api:request(url, params, post_params) + local vars = cfg.variables + + if fs.which_tool("downloader") == "wget" then + local curl_ok, err = fs.is_tool_available(vars.CURL, "curl") + if not curl_ok then + return nil, err + end + end + + if not self.config.key then + return nil, "Must have API key before performing any actions." + end + if params and next(params) then + url = url .. ("?" .. encode_query_string(params)) + end + local method = "GET" + local out + local tmpfile = fs.tmpname() + if post_params then + method = "POST" + local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --silent --user-agent \""..cfg.user_agent.." via curl\" " + for k,v in pairs(post_params) do + local var = v + if type(v) == "table" then + var = "@"..v.fname + end + curl_cmd = curl_cmd .. "--form \""..k.."="..var.."\" " + end + if cfg.connection_timeout and cfg.connection_timeout > 0 then + curl_cmd = curl_cmd .. "--connect-timeout "..tonumber(cfg.connection_timeout).." " + end + local ok = fs.execute_string(curl_cmd..fs.Q(url).." -o "..fs.Q(tmpfile)) + if not ok then + return nil, "API failure: " .. redact_api_url(url) + end + else + local ok, err = fs.download(url, tmpfile) + if not ok then + return nil, "API failure: " .. tostring(err) .. " - " .. redact_api_url(url) + end + end + + local tmpfd = io.open(tmpfile) + if not tmpfd then + os.remove(tmpfile) + return nil, "API failure reading temporary file - " .. redact_api_url(url) + end + out = tmpfd:read("*a") + tmpfd:close() + os.remove(tmpfile) + + if self.debug then + util.printout("[" .. tostring(method) .. " via curl] " .. redact_api_url(url) .. " ... ") + end + + return json.decode(out) +end + +else -- use LuaSocket and LuaSec + +local warned_luasec = false + +function Api:request(url, params, post_params) + local server = tostring(self.config.server) + local http_ok, http + local via = "luasocket" + if server:match("^https://") then + http_ok, http = pcall(require, "ssl.https") + if http_ok then + via = "luasec" + else + if not warned_luasec then + util.printerr("LuaSec is not available; using plain HTTP. Install 'luasec' to enable HTTPS.") + warned_luasec = true + end + http_ok, http = pcall(require, "socket.http") + url = url:gsub("^https", "http") + via = "luasocket" + end + else + http_ok, http = pcall(require, "socket.http") + end + if not http_ok then + return nil, "Failed loading socket library!" + end + + if not self.config.key then + return nil, "Must have API key before performing any actions." + end + local body + local headers = {} + if params and next(params) then + url = url .. ("?" .. encode_query_string(params)) + end + if post_params then + local boundary + body, boundary = multipart.encode(post_params) + headers["Content-length"] = #body + headers["Content-type"] = "multipart/form-data; boundary=" .. tostring(boundary) + end + local method = post_params and "POST" or "GET" + if self.debug then + util.printout("[" .. tostring(method) .. " via "..via.."] " .. redact_api_url(url) .. " ... ") + end + local out = {} + local _, status = http.request({ + url = url, + headers = headers, + method = method, + sink = ltn12.sink.table(out), + source = body and ltn12.source.string(body) + }) + if self.debug then + util.printout(tostring(status)) + end + local pok, ret, err = pcall(json.decode, table.concat(out)) + if pok and ret then + return ret + end + return nil, "API returned " .. tostring(status) .. " - " .. redact_api_url(url) +end + +end + +function api.new(args) + local self = {} + setmetatable(self, { __index = Api }) + self.config = self:load_config() or {} + self.config.server = args.server or self.config.server or cfg.upload.server + self.config.version = self.config.version or cfg.upload.version + self.config.key = args.temp_key or args.api_key or self.config.key + self.debug = args.debug + if not self.config.key then + return nil, "You need an API key to upload rocks.\n" .. + "Navigate to "..self.config.server.."/settings to get a key\n" .. + "and then pass it through the --api-key= flag." + end + if args.api_key then + self:save_config() + end + return self +end + +return api diff --git a/src/luarocks/upload/multipart.lua b/src/luarocks/upload/multipart.lua new file mode 100644 index 0000000..56ae873 --- /dev/null +++ b/src/luarocks/upload/multipart.lua @@ -0,0 +1,109 @@ + +local multipart = {} + +local File = {} + +local unpack = unpack or table.unpack + +-- socket.url.escape(s) from LuaSocket 3.0rc1 +function multipart.url_escape(s) + return (string.gsub(s, "([^A-Za-z0-9_])", function(c) + return string.format("%%%02x", string.byte(c)) + end)) +end + +function File:mime() + if not self.mimetype then + local mimetypes_ok, mimetypes = pcall(require, "mimetypes") + if mimetypes_ok then + self.mimetype = mimetypes.guess(self.fname) + end + self.mimetype = self.mimetype or "application/octet-stream" + end + return self.mimetype +end + +function File:content() + local fd = io.open(self.fname, "rb") + if not fd then + return nil, "Failed to open file: "..self.fname + end + local data = fd:read("*a") + fd:close() + return data +end + +local function rand_string(len) + local shuffled = {} + for i = 1, len do + local r = math.random(97, 122) + if math.random() >= 0.5 then + r = r - 32 + end + shuffled[i] = r + end + return string.char(unpack(shuffled)) +end + +-- multipart encodes params +-- returns encoded string,boundary +-- params is an a table of tuple tables: +-- params = { +-- {key1, value2}, +-- {key2, value2}, +-- key3: value3 +-- } +function multipart.encode(params) + local tuples = { } + for i = 1, #params do + tuples[i] = params[i] + end + for k,v in pairs(params) do + if type(k) == "string" then + table.insert(tuples, {k, v}) + end + end + local chunks = {} + for _, tuple in ipairs(tuples) do + local k,v = unpack(tuple) + k = multipart.url_escape(k) + local buffer = { 'Content-Disposition: form-data; name="' .. k .. '"' } + local content + if type(v) == "table" and v.__class == File then + buffer[1] = buffer[1] .. ('; filename="' .. v.fname:gsub(".*/", "") .. '"') + table.insert(buffer, "Content-type: " .. v:mime()) + content = v:content() + else + content = v + end + table.insert(buffer, "") + table.insert(buffer, content) + table.insert(chunks, table.concat(buffer, "\r\n")) + end + local boundary + while not boundary do + boundary = "Boundary" .. rand_string(16) + for _, chunk in ipairs(chunks) do + if chunk:find(boundary) then + boundary = nil + break + end + end + end + local inner = "\r\n--" .. boundary .. "\r\n" + return table.concat({ "--", boundary, "\r\n", + table.concat(chunks, inner), + "\r\n", "--", boundary, "--", "\r\n" }), boundary +end + +function multipart.new_file(fname, mime) + local self = {} + setmetatable(self, { __index = File }) + self.__class = File + self.fname = fname + self.mimetype = mime + return self +end + +return multipart + diff --git a/src/luarocks/util.lua b/src/luarocks/util.lua new file mode 100644 index 0000000..de9157f --- /dev/null +++ b/src/luarocks/util.lua @@ -0,0 +1,634 @@ + +--- Assorted utilities for managing tables, plus a scheduler for rollback functions. +-- Does not requires modules directly (only as locals +-- inside specific functions) to avoid interdependencies, +-- as this is used in the bootstrapping stage of luarocks.core.cfg. + +local util = {} + +local core = require("luarocks.core.util") + +util.cleanup_path = core.cleanup_path +util.split_string = core.split_string +util.sortedpairs = core.sortedpairs +util.deep_merge = core.deep_merge +util.deep_merge_under = core.deep_merge_under +util.popen_read = core.popen_read +util.show_table = core.show_table +util.printerr = core.printerr +util.warning = core.warning +util.keys = core.keys + +local unpack = unpack or table.unpack +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +local scheduled_functions = {} + +--- Schedule a function to be executed upon program termination. +-- This is useful for actions such as deleting temporary directories +-- or failure rollbacks. +-- @param f function: Function to be executed. +-- @param ... arguments to be passed to function. +-- @return table: A token representing the scheduled execution, +-- which can be used to remove the item later from the list. +function util.schedule_function(f, ...) + assert(type(f) == "function") + + local item = { fn = f, args = pack(...) } + table.insert(scheduled_functions, item) + return item +end + +--- Unschedule a function. +-- This is useful for cancelling a rollback of a completed operation. +-- @param item table: The token representing the scheduled function that was +-- returned from the schedule_function call. +function util.remove_scheduled_function(item) + for k, v in pairs(scheduled_functions) do + if v == item then + table.remove(scheduled_functions, k) + return + end + end +end + +--- Execute scheduled functions. +-- Some calls create temporary files and/or directories and register +-- corresponding cleanup functions. Calling this function will run +-- these function, erasing temporaries. +-- Functions are executed in the inverse order they were scheduled. +function util.run_scheduled_functions() + local fs = require("luarocks.fs") + if fs.change_dir_to_root then + fs.change_dir_to_root() + end + for i = #scheduled_functions, 1, -1 do + local item = scheduled_functions[i] + item.fn(unpack(item.args, 1, item.args.n)) + end +end + +--- Produce a Lua pattern that matches precisely the given string +-- (this is suitable to be concatenating to other patterns, +-- so it does not include beginning- and end-of-string markers (^$) +-- @param s string: The input string +-- @return string: The equivalent pattern +function util.matchquote(s) + return (s:gsub("[?%-+*%[%].%%()$^]","%%%1")) +end + +local var_format_pattern = "%$%((%a[%a%d_]+)%)" + +-- Check if a set of needed variables are referenced +-- somewhere in a list of definitions, warning the user +-- about any unused ones. Each key in needed_set should +-- appear as a $(XYZ) variable at least once as a +-- substring of some value of var_defs. +-- @param var_defs: a table with string keys and string +-- values, containing variable definitions. +-- @param needed_set: a set where keys are the names of +-- needed variables. +-- @param msg string: the warning message to display. +function util.warn_if_not_used(var_defs, needed_set, msg) + local seen = {} + for _, val in pairs(var_defs) do + for used in val:gmatch(var_format_pattern) do + seen[used] = true + end + end + for var, _ in pairs(needed_set) do + if not seen[var] then + util.warning(msg:format(var)) + end + end +end + +-- Output any entries that might remain in $(XYZ) format, +-- warning the user that substitutions have failed. +-- @param line string: the input string +local function warn_failed_matches(line) + local any_failed = false + if line:match(var_format_pattern) then + for unmatched in line:gmatch(var_format_pattern) do + util.warning("unmatched variable " .. unmatched) + any_failed = true + end + end + return any_failed +end + +--- Perform make-style variable substitutions on string values of a table. +-- For every string value tbl.x which contains a substring of the format +-- "$(XYZ)" will have this substring replaced by vars["XYZ"], if that field +-- exists in vars. Only string values are processed; this function +-- does not scan subtables recursively. +-- @param tbl table: Table to have its string values modified. +-- @param vars table: Table containing string-string key-value pairs +-- representing variables to replace in the strings values of tbl. +function util.variable_substitutions(tbl, vars) + assert(type(tbl) == "table") + assert(type(vars) == "table") + + local updated = {} + for k, v in pairs(tbl) do + if type(v) == "string" then + updated[k] = v:gsub(var_format_pattern, vars) + if warn_failed_matches(updated[k]) then + updated[k] = updated[k]:gsub(var_format_pattern, "") + end + end + end + for k, v in pairs(updated) do + tbl[k] = v + end +end + +function util.lua_versions(sort) + local versions = { "5.1", "5.2", "5.3", "5.4" } + local i = 0 + if sort == "descending" then + i = #versions + 1 + return function() + i = i - 1 + return versions[i] + end + else + return function() + i = i + 1 + return versions[i] + end + end +end + +function util.lua_path_variables() + local cfg = require("luarocks.core.cfg") + local lpath_var = "LUA_PATH" + local lcpath_var = "LUA_CPATH" + + local lv = cfg.lua_version:gsub("%.", "_") + if lv ~= "5_1" then + if os.getenv("LUA_PATH_" .. lv) then + lpath_var = "LUA_PATH_" .. lv + end + if os.getenv("LUA_CPATH_" .. lv) then + lcpath_var = "LUA_CPATH_" .. lv + end + end + return lpath_var, lcpath_var +end + +function util.starts_with(s, prefix) + return s:sub(1,#prefix) == prefix +end + +--- Print a line to standard output +function util.printout(...) + io.stdout:write(table.concat({...},"\t")) + io.stdout:write("\n") +end + +function util.title(msg, porcelain, underline) + if porcelain then return end + util.printout() + util.printout(msg) + util.printout((underline or "-"):rep(#msg)) + util.printout() +end + +function util.this_program(default) + local i = 1 + local last, cur = default, default + while i do + local dbg = debug and debug.getinfo(i,"S") + if not dbg then break end + last = cur + cur = dbg.source + i=i+1 + end + local prog = last:sub(1,1) == "@" and last:sub(2) or last + + -- Check if we found the true path of a script that has a wrapper + local lrdir, binpath = prog:match("^(.*)/lib/luarocks/rocks%-[0-9.]*/[^/]+/[^/]+(/bin/[^/]+)$") + if lrdir then + -- Return the wrapper instead + return lrdir .. binpath + end + + return prog +end + +function util.format_rock_name(name, namespace, version) + return (namespace and namespace.."/" or "")..name..(version and " "..version or "") +end + +function util.deps_mode_option(parser, program) + local cfg = require("luarocks.core.cfg") + + parser:option("--deps-mode", "How to handle dependencies. Four modes are supported:\n".. + "* all - use all trees from the rocks_trees list for finding dependencies\n".. + "* one - use only the current tree (possibly set with --tree)\n".. + "* order - use trees based on order (use the current tree and all ".. + "trees below it on the rocks_trees list)\n".. + "* none - ignore dependencies altogether.\n".. + "The default mode may be set with the deps_mode entry in the configuration file.\n".. + 'The current default is "'..cfg.deps_mode..'".\n'.. + "Type '"..util.this_program(program or "luarocks").."' with no ".. + "arguments to see your list of rocks trees.") + :argname("") + :choices({"all", "one", "order", "none"}) + parser:flag("--nodeps"):hidden(true) +end + +function util.see_help(command, program) + return "See '"..util.this_program(program or "luarocks")..' help'..(command and " "..command or "").."'." +end + +function util.see_also(text) + local see_also = "See also:\n" + if text then + see_also = see_also..text.."\n" + end + return see_also.." '"..util.this_program("luarocks").." help' for general options and configuration." +end + +function util.announce_install(rockspec) + local cfg = require("luarocks.core.cfg") + local path = require("luarocks.path") + + local suffix = "" + if rockspec.description and rockspec.description.license then + suffix = " (license: "..rockspec.description.license..")" + end + + util.printout(rockspec.name.." "..rockspec.version.." is now installed in "..path.root_dir(cfg.root_dir)..suffix) + util.printout() +end + +--- Collect rockspecs located in a subdirectory. +-- @param versions table: A table mapping rock names to newest rockspec versions. +-- @param paths table: A table mapping rock names to newest rockspec paths. +-- @param unnamed_paths table: An array of rockspec paths that don't contain rock +-- name and version in regular format. +-- @param subdir string: path to subdirectory. +local function collect_rockspecs(versions, paths, unnamed_paths, subdir) + local fs = require("luarocks.fs") + local dir = require("luarocks.dir") + local path = require("luarocks.path") + local vers = require("luarocks.core.vers") + + if fs.is_dir(subdir) then + for file in fs.dir(subdir) do + file = dir.path(subdir, file) + + if file:match("rockspec$") and fs.is_file(file) then + local rock, version = path.parse_name(file) + + if rock then + if not versions[rock] or vers.compare_versions(version, versions[rock]) then + versions[rock] = version + paths[rock] = file + end + else + table.insert(unnamed_paths, file) + end + end + end + end +end + +--- Get default rockspec name for commands that take optional rockspec name. +-- @return string or (nil, string): path to the rockspec or nil and error message. +function util.get_default_rockspec() + local versions, paths, unnamed_paths = {}, {}, {} + -- Look for rockspecs in some common locations. + collect_rockspecs(versions, paths, unnamed_paths, ".") + collect_rockspecs(versions, paths, unnamed_paths, "rockspec") + collect_rockspecs(versions, paths, unnamed_paths, "rockspecs") + + if #unnamed_paths > 0 then + -- There are rockspecs not following "name-version.rockspec" format. + -- More than one are ambiguous. + if #unnamed_paths > 1 then + return nil, "Please specify which rockspec file to use." + else + return unnamed_paths[1] + end + else + local fs = require("luarocks.fs") + local dir = require("luarocks.dir") + local basename = dir.base_name(fs.current_dir()) + + if paths[basename] then + return paths[basename] + end + + local rock = next(versions) + + if rock then + -- If there are rockspecs for multiple rocks it's ambiguous. + if next(versions, rock) then + return nil, "Please specify which rockspec file to use." + else + return paths[rock] + end + else + return nil, "Argument missing: please specify a rockspec to use on current directory." + end + end +end + +-- Quote Lua string, analogous to fs.Q. +-- @param s A string, such as "hello" +-- @return string: A quoted string, such as '"hello"' +function util.LQ(s) + return ("%q"):format(s) +end + +-- Split name and namespace of a package name. +-- @param ns_name a name that may be in "namespace/name" format +-- @return string, string? - name and optionally a namespace +function util.split_namespace(ns_name) + local p1, p2 = ns_name:match("^([^/]+)/([^/]+)$") + if p1 then + return p2, p1 + end + return ns_name +end + +--- Argparse action callback for namespaced rock arguments. +function util.namespaced_name_action(args, target, ns_name) + assert(type(args) == "table") + assert(type(target) == "string") + assert(type(ns_name) == "string" or not ns_name) + + if not ns_name then + return + end + + if ns_name:match("%.rockspec$") or ns_name:match("%.rock$") then + args[target] = ns_name + else + local name, namespace = util.split_namespace(ns_name) + args[target] = name:lower() + if namespace then + args.namespace = namespace:lower() + end + end +end + +function util.deep_copy(tbl) + local copy = {} + for k, v in pairs(tbl) do + if type(v) == "table" then + copy[k] = util.deep_copy(v) + else + copy[k] = v + end + end + return copy +end + +-- A portable version of fs.exists that can be used at early startup, +-- before the platform has been determined and luarocks.fs has been +-- initialized. +function util.exists(file) + local fd, _, code = io.open(file, "r") + if code == 13 then + -- code 13 means "Permission denied" on both Unix and Windows + -- io.open on folders always fails with code 13 on Windows + return true + end + if fd then + fd:close() + return true + end + return false +end + +do + local function Q(pathname) + if pathname:match("^.:") then + return pathname:sub(1, 2) .. '"' .. pathname:sub(3) .. '"' + end + return '"' .. pathname .. '"' + end + + function util.check_lua_version(lua, luaver) + if not util.exists(lua) then + return nil + end + local lv, err = util.popen_read(Q(lua) .. ' -e "io.write(_VERSION:sub(5))"') + if lv == "" then + return nil + end + if luaver and luaver ~= lv then + return nil + end + return lv + end + + function util.get_luajit_version() + local cfg = require("luarocks.core.cfg") + if cfg.cache.luajit_version_checked then + return cfg.cache.luajit_version + end + cfg.cache.luajit_version_checked = true + + if not cfg.variables.LUA then + return nil + end + + local ljv + if cfg.lua_version == "5.1" then + -- Ignores extra version info for custom builds, e.g. "LuaJIT 2.1.0-beta3 some-other-version-info" + ljv = util.popen_read(Q(cfg.variables.LUA) .. ' -e "io.write(tostring(jit and jit.version:gsub([[^%S+ (%S+).*]], [[%1]])))"') + if ljv == "nil" then + ljv = nil + end + end + cfg.cache.luajit_version = ljv + return ljv + end + + local find_lua_bindir + do + local exe_suffix = (package.config:sub(1, 1) == "\\" and ".exe" or "") + + local function insert_lua_variants(names, luaver) + local variants = { + "lua" .. luaver .. exe_suffix, + "lua" .. luaver:gsub("%.", "") .. exe_suffix, + "lua-" .. luaver .. exe_suffix, + "lua-" .. luaver:gsub("%.", "") .. exe_suffix, + } + for _, name in ipairs(variants) do + names[name] = luaver + table.insert(names, name) + end + end + + find_lua_bindir = function(prefix, luaver, verbose) + local names = {} + if luaver then + insert_lua_variants(names, luaver) + else + for v in util.lua_versions("descending") do + insert_lua_variants(names, v) + end + end + if luaver == "5.1" or not luaver then + table.insert(names, "luajit" .. exe_suffix) + end + table.insert(names, "lua" .. exe_suffix) + + local tried = {} + local dir_sep = package.config:sub(1, 1) + for _, d in ipairs({ prefix .. dir_sep .. "bin", prefix }) do + for _, name in ipairs(names) do + local lua = d .. dir_sep .. name + local is_wrapper, err = util.lua_is_wrapper(lua) + if is_wrapper == false then + local lv = util.check_lua_version(lua, luaver) + if lv then + return lua, d, lv + end + elseif is_wrapper == true or err == nil then + table.insert(tried, lua) + else + table.insert(tried, string.format("%-13s (%s)", lua, err)) + end + end + end + local interp = luaver + and ("Lua " .. luaver .. " interpreter") + or "Lua interpreter" + return nil, interp .. " not found at " .. prefix .. "\n" .. + (verbose and "Tried:\t" .. table.concat(tried, "\n\t") or "") + end + end + + function util.find_lua(prefix, luaver, verbose) + local lua, bindir + lua, bindir, luaver = find_lua_bindir(prefix, luaver, verbose) + if not lua then + return nil, bindir + end + + return { + lua_version = luaver, + lua = lua, + lua_dir = prefix, + lua_bindir = bindir, + } + end +end + +function util.lua_is_wrapper(interp) + local fd, err = io.open(interp, "r") + if not fd then + return nil, err + end + local data, err = fd:read(1000) + fd:close() + if not data then + return nil, err + end + return not not data:match("LUAROCKS_SYSCONFDIR") +end + +function util.opts_table(type_name, valid_opts) + local opts_mt = {} + + opts_mt.__index = opts_mt + + function opts_mt.type() + return type_name + end + + return function(opts) + for k, v in pairs(opts) do + local tv = type(v) + if not valid_opts[k] then + error("invalid option: "..k) + end + local vo, optional = valid_opts[k]:match("^(.-)(%??)$") + if not (tv == vo or (optional == "?" and tv == nil)) then + error("invalid type option: "..k.." - got "..tv..", expected "..vo) + end + end + for k, v in pairs(valid_opts) do + if (not v:find("?", 1, true)) and opts[k] == nil then + error("missing option: "..k) + end + end + return setmetatable(opts, opts_mt) + end +end + +--- Return a table of modules that are already provided by the VM, which +-- can be specified as dependencies without having to install an actual rock. +-- @param rockspec (optional) a rockspec table, so that rockspec format +-- version compatibility can be checked. If not given, maximum compatibility +-- is assumed. +-- @return a table with rock names as keys and versions and values, +-- specifying modules that are already provided by the VM (including +-- "lua" for the Lua version and, for format 3.0+, "luajit" if detected). +function util.get_rocks_provided(rockspec) + local cfg = require("luarocks.core.cfg") + + if not rockspec and cfg.cache.rocks_provided then + return cfg.cache.rocks_provided + end + + local rocks_provided = {} + + local lv = cfg.lua_version + + rocks_provided["lua"] = lv.."-1" + + if lv == "5.2" then + rocks_provided["bit32"] = lv.."-1" + end + + if lv == "5.3" or lv == "5.4" then + rocks_provided["utf8"] = lv.."-1" + end + + if lv == "5.1" then + local ljv = util.get_luajit_version() + if ljv then + rocks_provided["luabitop"] = ljv.."-1" + if (not rockspec) or rockspec:format_is_at_least("3.0") then + rocks_provided["luajit"] = ljv.."-1" + end + end + end + + if cfg.rocks_provided then + util.deep_merge_under(rocks_provided, cfg.rocks_provided) + end + + if not rockspec then + cfg.cache.rocks_provided = rocks_provided + end + + return rocks_provided +end + +function util.remove_doc_dir(name, version) + local path = require("luarocks.path") + local fs = require("luarocks.fs") + local dir = require("luarocks.dir") + + local install_dir = path.install_dir(name, version) + for _, f in ipairs(fs.list_dir(install_dir)) do + local doc_dirs = { "doc", "docs" } + for _, d in ipairs(doc_dirs) do + if f == d then + fs.delete(dir.path(install_dir, f)) + end + end + end +end + +return util diff --git a/src/luarocks/vendor/argparse.lua b/src/luarocks/vendor/argparse.lua new file mode 100644 index 0000000..2c2585d --- /dev/null +++ b/src/luarocks/vendor/argparse.lua @@ -0,0 +1,2103 @@ +-- The MIT License (MIT) + +-- Copyright (c) 2013 - 2018 Peter Melnichenko +-- 2019 Paul Ouellette + +-- 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. + +local function deep_update(t1, t2) + for k, v in pairs(t2) do + if type(v) == "table" then + v = deep_update({}, v) + end + + t1[k] = v + end + + return t1 +end + +-- A property is a tuple {name, callback}. +-- properties.args is number of properties that can be set as arguments +-- when calling an object. +local function class(prototype, properties, parent) + -- Class is the metatable of its instances. + local cl = {} + cl.__index = cl + + if parent then + cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) + else + cl.__prototype = prototype + end + + if properties then + local names = {} + + -- Create setter methods and fill set of property names. + for _, property in ipairs(properties) do + local name, callback = property[1], property[2] + + cl[name] = function(self, value) + if not callback(self, value) then + self["_" .. name] = value + end + + return self + end + + names[name] = true + end + + function cl.__call(self, ...) + -- When calling an object, if the first argument is a table, + -- interpret keys as property names, else delegate arguments + -- to corresponding setters in order. + if type((...)) == "table" then + for name, value in pairs((...)) do + if names[name] then + self[name](self, value) + end + end + else + local nargs = select("#", ...) + + for i, property in ipairs(properties) do + if i > nargs or i > properties.args then + break + end + + local arg = select(i, ...) + + if arg ~= nil then + self[property[1]](self, arg) + end + end + end + + return self + end + end + + -- If indexing class fails, fallback to its parent. + local class_metatable = {} + class_metatable.__index = parent + + function class_metatable.__call(self, ...) + -- Calling a class returns its instance. + -- Arguments are delegated to the instance. + local object = deep_update({}, self.__prototype) + setmetatable(object, self) + return object(...) + end + + return setmetatable(cl, class_metatable) +end + +local function typecheck(name, types, value) + for _, type_ in ipairs(types) do + if type(value) == type_ then + return true + end + end + + error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value))) +end + +local function typechecked(name, ...) + local types = {...} + return {name, function(_, value) typecheck(name, types, value) end} +end + +local multiname = {"name", function(self, value) + typecheck("name", {"string"}, value) + + for alias in value:gmatch("%S+") do + self._name = self._name or alias + table.insert(self._aliases, alias) + table.insert(self._public_aliases, alias) + -- If alias contains '_', accept '-' also. + if alias:find("_", 1, true) then + table.insert(self._aliases, (alias:gsub("_", "-"))) + end + end + + -- Do not set _name as with other properties. + return true +end} + +local multiname_hidden = {"hidden_name", function(self, value) + typecheck("hidden_name", {"string"}, value) + + for alias in value:gmatch("%S+") do + table.insert(self._aliases, alias) + if alias:find("_", 1, true) then + table.insert(self._aliases, (alias:gsub("_", "-"))) + end + end + + return true +end} + +local function parse_boundaries(str) + if tonumber(str) then + return tonumber(str), tonumber(str) + end + + if str == "*" then + return 0, math.huge + end + + if str == "+" then + return 1, math.huge + end + + if str == "?" then + return 0, 1 + end + + if str:match "^%d+%-%d+$" then + local min, max = str:match "^(%d+)%-(%d+)$" + return tonumber(min), tonumber(max) + end + + if str:match "^%d+%+$" then + local min = str:match "^(%d+)%+$" + return tonumber(min), math.huge + end +end + +local function boundaries(name) + return {name, function(self, value) + typecheck(name, {"number", "string"}, value) + + local min, max = parse_boundaries(value) + + if not min then + error(("bad property '%s'"):format(name)) + end + + self["_min" .. name], self["_max" .. name] = min, max + end} +end + +local actions = {} + +local option_action = {"action", function(_, value) + typecheck("action", {"function", "string"}, value) + + if type(value) == "string" and not actions[value] then + error(("unknown action '%s'"):format(value)) + end +end} + +local option_init = {"init", function(self) + self._has_init = true +end} + +local option_default = {"default", function(self, value) + if type(value) ~= "string" then + self._init = value + self._has_init = true + return true + end +end} + +local add_help = {"add_help", function(self, value) + typecheck("add_help", {"boolean", "string", "table"}, value) + + if self._help_option_idx then + table.remove(self._options, self._help_option_idx) + self._help_option_idx = nil + end + + if value then + local help = self:flag() + :description "Show this help message and exit." + :action(function() + print(self:get_help()) + os.exit(0) + end) + + if value ~= true then + help = help(value) + end + + if not help._name then + help "-h" "--help" + end + + self._help_option_idx = #self._options + end +end} + +local Parser = class({ + _arguments = {}, + _options = {}, + _commands = {}, + _mutexes = {}, + _groups = {}, + _require_command = true, + _handle_options = true +}, { + args = 3, + typechecked("name", "string"), + typechecked("description", "string"), + typechecked("epilog", "string"), + typechecked("usage", "string"), + typechecked("help", "string"), + typechecked("require_command", "boolean"), + typechecked("handle_options", "boolean"), + typechecked("action", "function"), + typechecked("command_target", "string"), + typechecked("help_vertical_space", "number"), + typechecked("usage_margin", "number"), + typechecked("usage_max_width", "number"), + typechecked("help_usage_margin", "number"), + typechecked("help_description_margin", "number"), + typechecked("help_max_width", "number"), + add_help +}) + +local Command = class({ + _aliases = {}, + _public_aliases = {} +}, { + args = 3, + multiname, + typechecked("description", "string"), + typechecked("epilog", "string"), + multiname_hidden, + typechecked("summary", "string"), + typechecked("target", "string"), + typechecked("usage", "string"), + typechecked("help", "string"), + typechecked("require_command", "boolean"), + typechecked("handle_options", "boolean"), + typechecked("action", "function"), + typechecked("command_target", "string"), + typechecked("help_vertical_space", "number"), + typechecked("usage_margin", "number"), + typechecked("usage_max_width", "number"), + typechecked("help_usage_margin", "number"), + typechecked("help_description_margin", "number"), + typechecked("help_max_width", "number"), + typechecked("hidden", "boolean"), + add_help +}, Parser) + +local Argument = class({ + _minargs = 1, + _maxargs = 1, + _mincount = 1, + _maxcount = 1, + _defmode = "unused", + _show_default = true +}, { + args = 5, + typechecked("name", "string"), + typechecked("description", "string"), + option_default, + typechecked("convert", "function", "table"), + boundaries("args"), + typechecked("target", "string"), + typechecked("defmode", "string"), + typechecked("show_default", "boolean"), + typechecked("argname", "string", "table"), + typechecked("choices", "table"), + typechecked("hidden", "boolean"), + option_action, + option_init +}) + +local Option = class({ + _aliases = {}, + _public_aliases = {}, + _mincount = 0, + _overwrite = true +}, { + args = 6, + multiname, + typechecked("description", "string"), + option_default, + typechecked("convert", "function", "table"), + boundaries("args"), + boundaries("count"), + multiname_hidden, + typechecked("target", "string"), + typechecked("defmode", "string"), + typechecked("show_default", "boolean"), + typechecked("overwrite", "boolean"), + typechecked("argname", "string", "table"), + typechecked("choices", "table"), + typechecked("hidden", "boolean"), + option_action, + option_init +}, Argument) + +function Parser:_inherit_property(name, default) + local element = self + + while true do + local value = element["_" .. name] + + if value ~= nil then + return value + end + + if not element._parent then + return default + end + + element = element._parent + end +end + +function Argument:_get_argument_list() + local buf = {} + local i = 1 + + while i <= math.min(self._minargs, 3) do + local argname = self:_get_argname(i) + + if self._default and self._defmode:find "a" then + argname = "[" .. argname .. "]" + end + + table.insert(buf, argname) + i = i+1 + end + + while i <= math.min(self._maxargs, 3) do + table.insert(buf, "[" .. self:_get_argname(i) .. "]") + i = i+1 + + if self._maxargs == math.huge then + break + end + end + + if i < self._maxargs then + table.insert(buf, "...") + end + + return buf +end + +function Argument:_get_usage() + local usage = table.concat(self:_get_argument_list(), " ") + + if self._default and self._defmode:find "u" then + if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then + usage = "[" .. usage .. "]" + end + end + + return usage +end + +function actions.store_true(result, target) + result[target] = true +end + +function actions.store_false(result, target) + result[target] = false +end + +function actions.store(result, target, argument) + result[target] = argument +end + +function actions.count(result, target, _, overwrite) + if not overwrite then + result[target] = result[target] + 1 + end +end + +function actions.append(result, target, argument, overwrite) + result[target] = result[target] or {} + table.insert(result[target], argument) + + if overwrite then + table.remove(result[target], 1) + end +end + +function actions.concat(result, target, arguments, overwrite) + if overwrite then + error("'concat' action can't handle too many invocations") + end + + result[target] = result[target] or {} + + for _, argument in ipairs(arguments) do + table.insert(result[target], argument) + end +end + +function Argument:_get_action() + local action, init + + if self._maxcount == 1 then + if self._maxargs == 0 then + action, init = "store_true", nil + else + action, init = "store", nil + end + else + if self._maxargs == 0 then + action, init = "count", 0 + else + action, init = "append", {} + end + end + + if self._action then + action = self._action + end + + if self._has_init then + init = self._init + end + + if type(action) == "string" then + action = actions[action] + end + + return action, init +end + +-- Returns placeholder for `narg`-th argument. +function Argument:_get_argname(narg) + local argname = self._argname or self:_get_default_argname() + + if type(argname) == "table" then + return argname[narg] + else + return argname + end +end + +function Argument:_get_choices_list() + return "{" .. table.concat(self._choices, ",") .. "}" +end + +function Argument:_get_default_argname() + if self._choices then + return self:_get_choices_list() + else + return "<" .. self._name .. ">" + end +end + +function Option:_get_default_argname() + if self._choices then + return self:_get_choices_list() + else + return "<" .. self:_get_default_target() .. ">" + end +end + +-- Returns labels to be shown in the help message. +function Argument:_get_label_lines() + if self._choices then + return {self:_get_choices_list()} + else + return {self._name} + end +end + +function Option:_get_label_lines() + local argument_list = self:_get_argument_list() + + if #argument_list == 0 then + -- Don't put aliases for simple flags like `-h` on different lines. + return {table.concat(self._public_aliases, ", ")} + end + + local longest_alias_length = -1 + + for _, alias in ipairs(self._public_aliases) do + longest_alias_length = math.max(longest_alias_length, #alias) + end + + local argument_list_repr = table.concat(argument_list, " ") + local lines = {} + + for i, alias in ipairs(self._public_aliases) do + local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr + + if i ~= #self._public_aliases then + line = line .. "," + end + + table.insert(lines, line) + end + + return lines +end + +function Command:_get_label_lines() + return {table.concat(self._public_aliases, ", ")} +end + +function Argument:_get_description() + if self._default and self._show_default then + if self._description then + return ("%s (default: %s)"):format(self._description, self._default) + else + return ("default: %s"):format(self._default) + end + else + return self._description or "" + end +end + +function Command:_get_description() + return self._summary or self._description or "" +end + +function Option:_get_usage() + local usage = self:_get_argument_list() + table.insert(usage, 1, self._name) + usage = table.concat(usage, " ") + + if self._mincount == 0 or self._default then + usage = "[" .. usage .. "]" + end + + return usage +end + +function Argument:_get_default_target() + return self._name +end + +function Option:_get_default_target() + local res + + for _, alias in ipairs(self._public_aliases) do + if alias:sub(1, 1) == alias:sub(2, 2) then + res = alias:sub(3) + break + end + end + + res = res or self._name:sub(2) + return (res:gsub("-", "_")) +end + +function Option:_is_vararg() + return self._maxargs ~= self._minargs +end + +function Parser:_get_fullname(exclude_root) + local parent = self._parent + if exclude_root and not parent then + return "" + end + local buf = {self._name} + + while parent do + if not exclude_root or parent._parent then + table.insert(buf, 1, parent._name) + end + parent = parent._parent + end + + return table.concat(buf, " ") +end + +function Parser:_update_charset(charset) + charset = charset or {} + + for _, command in ipairs(self._commands) do + command:_update_charset(charset) + end + + for _, option in ipairs(self._options) do + for _, alias in ipairs(option._aliases) do + charset[alias:sub(1, 1)] = true + end + end + + return charset +end + +function Parser:argument(...) + local argument = Argument(...) + table.insert(self._arguments, argument) + return argument +end + +function Parser:option(...) + local option = Option(...) + table.insert(self._options, option) + return option +end + +function Parser:flag(...) + return self:option():args(0)(...) +end + +function Parser:command(...) + local command = Command():add_help(true)(...) + command._parent = self + table.insert(self._commands, command) + return command +end + +function Parser:mutex(...) + local elements = {...} + + for i, element in ipairs(elements) do + local mt = getmetatable(element) + assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i)) + end + + table.insert(self._mutexes, elements) + return self +end + +function Parser:group(name, ...) + assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name))) + + local group = {name = name, ...} + + for i, element in ipairs(group) do + local mt = getmetatable(element) + assert(mt == Option or mt == Argument or mt == Command, + ("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1)) + end + + table.insert(self._groups, group) + return self +end + +local usage_welcome = "Usage: " + +function Parser:get_usage() + if self._usage then + return self._usage + end + + local usage_margin = self:_inherit_property("usage_margin", #usage_welcome) + local max_usage_width = self:_inherit_property("usage_max_width", 70) + local lines = {usage_welcome .. self:_get_fullname()} + + local function add(s) + if #lines[#lines]+1+#s <= max_usage_width then + lines[#lines] = lines[#lines] .. " " .. s + else + lines[#lines+1] = (" "):rep(usage_margin) .. s + end + end + + -- Normally options are before positional arguments in usage messages. + -- However, vararg options should be after, because they can't be reliable used + -- before a positional argument. + -- Mutexes come into play, too, and are shown as soon as possible. + -- Overall, output usages in the following order: + -- 1. Mutexes that don't have positional arguments or vararg options. + -- 2. Options that are not in any mutexes and are not vararg. + -- 3. Positional arguments - on their own or as a part of a mutex. + -- 4. Remaining mutexes. + -- 5. Remaining options. + + local elements_in_mutexes = {} + local added_elements = {} + local added_mutexes = {} + local argument_to_mutexes = {} + + local function add_mutex(mutex, main_argument) + if added_mutexes[mutex] then + return + end + + added_mutexes[mutex] = true + local buf = {} + + for _, element in ipairs(mutex) do + if not element._hidden and not added_elements[element] then + if getmetatable(element) == Option or element == main_argument then + table.insert(buf, element:_get_usage()) + added_elements[element] = true + end + end + end + + if #buf == 1 then + add(buf[1]) + elseif #buf > 1 then + add("(" .. table.concat(buf, " | ") .. ")") + end + end + + local function add_element(element) + if not element._hidden and not added_elements[element] then + add(element:_get_usage()) + added_elements[element] = true + end + end + + for _, mutex in ipairs(self._mutexes) do + local is_vararg = false + local has_argument = false + + for _, element in ipairs(mutex) do + if getmetatable(element) == Option then + if element:_is_vararg() then + is_vararg = true + end + else + has_argument = true + argument_to_mutexes[element] = argument_to_mutexes[element] or {} + table.insert(argument_to_mutexes[element], mutex) + end + + elements_in_mutexes[element] = true + end + + if not is_vararg and not has_argument then + add_mutex(mutex) + end + end + + for _, option in ipairs(self._options) do + if not elements_in_mutexes[option] and not option:_is_vararg() then + add_element(option) + end + end + + -- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex. + for _, argument in ipairs(self._arguments) do + -- Pick a mutex as a part of which to show this argument, take the first one that's still available. + local mutex + + if elements_in_mutexes[argument] then + for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do + if not added_mutexes[argument_mutex] then + mutex = argument_mutex + end + end + end + + if mutex then + add_mutex(mutex, argument) + else + add_element(argument) + end + end + + for _, mutex in ipairs(self._mutexes) do + add_mutex(mutex) + end + + for _, option in ipairs(self._options) do + add_element(option) + end + + if #self._commands > 0 then + if self._require_command then + add("") + else + add("[]") + end + + add("...") + end + + return table.concat(lines, "\n") +end + +local function split_lines(s) + if s == "" then + return {} + end + + local lines = {} + + if s:sub(-1) ~= "\n" then + s = s .. "\n" + end + + for line in s:gmatch("([^\n]*)\n") do + table.insert(lines, line) + end + + return lines +end + +local function autowrap_line(line, max_length) + -- Algorithm for splitting lines is simple and greedy. + local result_lines = {} + + -- Preserve original indentation of the line, put this at the beginning of each result line. + -- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts + -- of the second and the following lines vertically align with the start of the second word. + local indentation = line:match("^ *") + + if line:find("^ *[%*%+%-]") then + indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)") + end + + -- Parts of the last line being assembled. + local line_parts = {} + + -- Length of the current line. + local line_length = 0 + + -- Index of the next character to consider. + local index = 1 + + while true do + local word_start, word_finish, word = line:find("([^ ]+)", index) + + if not word_start then + -- Ignore trailing spaces, if any. + break + end + + local preceding_spaces = line:sub(index, word_start - 1) + index = word_finish + 1 + + if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then + -- Either this is the very first word or it fits as an addition to the current line, add it. + table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation. + table.insert(line_parts, word) + line_length = line_length + #preceding_spaces + #word + else + -- Does not fit, finish current line and put the word into a new one. + table.insert(result_lines, table.concat(line_parts)) + line_parts = {indentation, word} + line_length = #indentation + #word + end + end + + if #line_parts > 0 then + table.insert(result_lines, table.concat(line_parts)) + end + + if #result_lines == 0 then + -- Preserve empty lines. + result_lines[1] = "" + end + + return result_lines +end + +-- Automatically wraps lines within given array, +-- attempting to limit line length to `max_length`. +-- Existing line splits are preserved. +local function autowrap(lines, max_length) + local result_lines = {} + + for _, line in ipairs(lines) do + local autowrapped_lines = autowrap_line(line, max_length) + + for _, autowrapped_line in ipairs(autowrapped_lines) do + table.insert(result_lines, autowrapped_line) + end + end + + return result_lines +end + +function Parser:_get_element_help(element) + local label_lines = element:_get_label_lines() + local description_lines = split_lines(element:_get_description()) + + local result_lines = {} + + -- All label lines should have the same length (except the last one, it has no comma). + -- If too long, start description after all the label lines. + -- Otherwise, combine label and description lines. + + local usage_margin_len = self:_inherit_property("help_usage_margin", 3) + local usage_margin = (" "):rep(usage_margin_len) + local description_margin_len = self:_inherit_property("help_description_margin", 25) + local description_margin = (" "):rep(description_margin_len) + + local help_max_width = self:_inherit_property("help_max_width") + + if help_max_width then + local description_max_width = math.max(help_max_width - description_margin_len, 10) + description_lines = autowrap(description_lines, description_max_width) + end + + if #label_lines[1] >= (description_margin_len - usage_margin_len) then + for _, label_line in ipairs(label_lines) do + table.insert(result_lines, usage_margin .. label_line) + end + + for _, description_line in ipairs(description_lines) do + table.insert(result_lines, description_margin .. description_line) + end + else + for i = 1, math.max(#label_lines, #description_lines) do + local label_line = label_lines[i] + local description_line = description_lines[i] + + local line = "" + + if label_line then + line = usage_margin .. label_line + end + + if description_line and description_line ~= "" then + line = line .. (" "):rep(description_margin_len - #line) .. description_line + end + + table.insert(result_lines, line) + end + end + + return table.concat(result_lines, "\n") +end + +local function get_group_types(group) + local types = {} + + for _, element in ipairs(group) do + types[getmetatable(element)] = true + end + + return types +end + +function Parser:_add_group_help(blocks, added_elements, label, elements) + local buf = {label} + + for _, element in ipairs(elements) do + if not element._hidden and not added_elements[element] then + added_elements[element] = true + table.insert(buf, self:_get_element_help(element)) + end + end + + if #buf > 1 then + table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1))) + end +end + +function Parser:get_help() + if self._help then + return self._help + end + + local blocks = {self:get_usage()} + + local help_max_width = self:_inherit_property("help_max_width") + + if self._description then + local description = self._description + + if help_max_width then + description = table.concat(autowrap(split_lines(description), help_max_width), "\n") + end + + table.insert(blocks, description) + end + + -- 1. Put groups containing arguments first, then other arguments. + -- 2. Put remaining groups containing options, then other options. + -- 3. Put remaining groups containing commands, then other commands. + -- Assume that an element can't be in several groups. + local groups_by_type = { + [Argument] = {}, + [Option] = {}, + [Command] = {} + } + + for _, group in ipairs(self._groups) do + local group_types = get_group_types(group) + + for _, mt in ipairs({Argument, Option, Command}) do + if group_types[mt] then + table.insert(groups_by_type[mt], group) + break + end + end + end + + local default_groups = { + {name = "Arguments", type = Argument, elements = self._arguments}, + {name = "Options", type = Option, elements = self._options}, + {name = "Commands", type = Command, elements = self._commands} + } + + local added_elements = {} + + for _, default_group in ipairs(default_groups) do + local type_groups = groups_by_type[default_group.type] + + for _, group in ipairs(type_groups) do + self:_add_group_help(blocks, added_elements, group.name .. ":", group) + end + + local default_label = default_group.name .. ":" + + if #type_groups > 0 then + default_label = "Other " .. default_label:gsub("^.", string.lower) + end + + self:_add_group_help(blocks, added_elements, default_label, default_group.elements) + end + + if self._epilog then + local epilog = self._epilog + + if help_max_width then + epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n") + end + + table.insert(blocks, epilog) + end + + return table.concat(blocks, "\n\n") +end + +function Parser:add_help_command(value) + if value then + assert(type(value) == "string" or type(value) == "table", + ("bad argument #1 to 'add_help_command' (string or table expected, got %s)"):format(type(value))) + end + + local help = self:command() + :description "Show help for commands." + help:argument "command" + :description "The command to show help for." + :args "?" + :action(function(_, _, cmd) + if not cmd then + print(self:get_help()) + os.exit(0) + else + for _, command in ipairs(self._commands) do + for _, alias in ipairs(command._aliases) do + if alias == cmd then + print(command:get_help()) + os.exit(0) + end + end + end + end + help:error(("unknown command '%s'"):format(cmd)) + end) + + if value then + help = help(value) + end + + if not help._name then + help "help" + end + + help._is_help_command = true + return self +end + +function Parser:_is_shell_safe() + if self._basename then + if self._basename:find("[^%w_%-%+%.]") then + return false + end + else + for _, alias in ipairs(self._aliases) do + if alias:find("[^%w_%-%+%.]") then + return false + end + end + end + for _, option in ipairs(self._options) do + for _, alias in ipairs(option._aliases) do + if alias:find("[^%w_%-%+%.]") then + return false + end + end + if option._choices then + for _, choice in ipairs(option._choices) do + if choice:find("[%s'\"]") then + return false + end + end + end + end + for _, argument in ipairs(self._arguments) do + if argument._choices then + for _, choice in ipairs(argument._choices) do + if choice:find("[%s'\"]") then + return false + end + end + end + end + for _, command in ipairs(self._commands) do + if not command:_is_shell_safe() then + return false + end + end + return true +end + +function Parser:add_complete(value) + if value then + assert(type(value) == "string" or type(value) == "table", + ("bad argument #1 to 'add_complete' (string or table expected, got %s)"):format(type(value))) + end + + local complete = self:option() + :description "Output a shell completion script for the specified shell." + :args(1) + :choices {"bash", "zsh", "fish"} + :action(function(_, _, shell) + io.write(self["get_" .. shell .. "_complete"](self)) + os.exit(0) + end) + + if value then + complete = complete(value) + end + + if not complete._name then + complete "--completion" + end + + return self +end + +function Parser:add_complete_command(value) + if value then + assert(type(value) == "string" or type(value) == "table", + ("bad argument #1 to 'add_complete_command' (string or table expected, got %s)"):format(type(value))) + end + + local complete = self:command() + :description "Output a shell completion script." + complete:argument "shell" + :description "The shell to output a completion script for." + :choices {"bash", "zsh", "fish"} + :action(function(_, _, shell) + io.write(self["get_" .. shell .. "_complete"](self)) + os.exit(0) + end) + + if value then + complete = complete(value) + end + + if not complete._name then + complete "completion" + end + + return self +end + +local function base_name(pathname) + return pathname:gsub("[/\\]*$", ""):match(".*[/\\]([^/\\]*)") or pathname +end + +local function get_short_description(element) + local short = element:_get_description():match("^(.-)%.%s") + return short or element:_get_description():match("^(.-)%.?$") +end + +function Parser:_get_options() + local options = {} + for _, option in ipairs(self._options) do + for _, alias in ipairs(option._aliases) do + table.insert(options, alias) + end + end + return table.concat(options, " ") +end + +function Parser:_get_commands() + local commands = {} + for _, command in ipairs(self._commands) do + for _, alias in ipairs(command._aliases) do + table.insert(commands, alias) + end + end + return table.concat(commands, " ") +end + +function Parser:_bash_option_args(buf, indent) + local opts = {} + for _, option in ipairs(self._options) do + if option._choices or option._minargs > 0 then + local compreply + if option._choices then + compreply = 'COMPREPLY=($(compgen -W "' .. table.concat(option._choices, " ") .. '" -- "$cur"))' + else + compreply = 'COMPREPLY=($(compgen -f -- "$cur"))' + end + table.insert(opts, (" "):rep(indent + 4) .. table.concat(option._aliases, "|") .. ")") + table.insert(opts, (" "):rep(indent + 8) .. compreply) + table.insert(opts, (" "):rep(indent + 8) .. "return 0") + table.insert(opts, (" "):rep(indent + 8) .. ";;") + end + end + + if #opts > 0 then + table.insert(buf, (" "):rep(indent) .. 'case "$prev" in') + table.insert(buf, table.concat(opts, "\n")) + table.insert(buf, (" "):rep(indent) .. "esac\n") + end +end + +function Parser:_bash_get_cmd(buf, indent) + if #self._commands == 0 then + return + end + + table.insert(buf, (" "):rep(indent) .. 'args=("${args[@]:1}")') + table.insert(buf, (" "):rep(indent) .. 'for arg in "${args[@]}"; do') + table.insert(buf, (" "):rep(indent + 4) .. 'case "$arg" in') + + for _, command in ipairs(self._commands) do + table.insert(buf, (" "):rep(indent + 8) .. table.concat(command._aliases, "|") .. ")") + if self._parent then + table.insert(buf, (" "):rep(indent + 12) .. 'cmd="$cmd ' .. command._name .. '"') + else + table.insert(buf, (" "):rep(indent + 12) .. 'cmd="' .. command._name .. '"') + end + table.insert(buf, (" "):rep(indent + 12) .. 'opts="$opts ' .. command:_get_options() .. '"') + command:_bash_get_cmd(buf, indent + 12) + table.insert(buf, (" "):rep(indent + 12) .. "break") + table.insert(buf, (" "):rep(indent + 12) .. ";;") + end + + table.insert(buf, (" "):rep(indent + 4) .. "esac") + table.insert(buf, (" "):rep(indent) .. "done") +end + +function Parser:_bash_cmd_completions(buf) + local cmd_buf = {} + if self._parent then + self:_bash_option_args(cmd_buf, 12) + end + if #self._commands > 0 then + table.insert(cmd_buf, (" "):rep(12) .. 'COMPREPLY=($(compgen -W "' .. self:_get_commands() .. '" -- "$cur"))') + elseif self._is_help_command then + table.insert(cmd_buf, (" "):rep(12) + .. 'COMPREPLY=($(compgen -W "' + .. self._parent:_get_commands() + .. '" -- "$cur"))') + end + if #cmd_buf > 0 then + table.insert(buf, (" "):rep(8) .. "'" .. self:_get_fullname(true) .. "')") + table.insert(buf, table.concat(cmd_buf, "\n")) + table.insert(buf, (" "):rep(12) .. ";;") + end + + for _, command in ipairs(self._commands) do + command:_bash_cmd_completions(buf) + end +end + +function Parser:get_bash_complete() + self._basename = base_name(self._name) + assert(self:_is_shell_safe()) + local buf = {([[ +_%s() { + local IFS=$' \t\n' + local args cur prev cmd opts arg + args=("${COMP_WORDS[@]}") + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="%s" +]]):format(self._basename, self:_get_options())} + + self:_bash_option_args(buf, 4) + self:_bash_get_cmd(buf, 4) + if #self._commands > 0 then + table.insert(buf, "") + table.insert(buf, (" "):rep(4) .. 'case "$cmd" in') + self:_bash_cmd_completions(buf) + table.insert(buf, (" "):rep(4) .. "esac\n") + end + + table.insert(buf, ([=[ + if [[ "$cur" = -* ]]; then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + fi +} + +complete -F _%s -o bashdefault -o default %s +]=]):format(self._basename, self._basename)) + + return table.concat(buf, "\n") +end + +function Parser:_zsh_arguments(buf, cmd_name, indent) + if self._parent then + table.insert(buf, (" "):rep(indent) .. "options=(") + table.insert(buf, (" "):rep(indent + 2) .. "$options") + else + table.insert(buf, (" "):rep(indent) .. "local -a options=(") + end + + for _, option in ipairs(self._options) do + local line = {} + if #option._aliases > 1 then + if option._maxcount > 1 then + table.insert(line, '"*"') + end + table.insert(line, "{" .. table.concat(option._aliases, ",") .. '}"') + else + table.insert(line, '"') + if option._maxcount > 1 then + table.insert(line, "*") + end + table.insert(line, option._name) + end + if option._description then + local description = get_short_description(option):gsub('["%]:`$]', "\\%0") + table.insert(line, "[" .. description .. "]") + end + if option._maxargs == math.huge then + table.insert(line, ":*") + end + if option._choices then + table.insert(line, ": :(" .. table.concat(option._choices, " ") .. ")") + elseif option._maxargs > 0 then + table.insert(line, ": :_files") + end + table.insert(line, '"') + table.insert(buf, (" "):rep(indent + 2) .. table.concat(line)) + end + + table.insert(buf, (" "):rep(indent) .. ")") + table.insert(buf, (" "):rep(indent) .. "_arguments -s -S \\") + table.insert(buf, (" "):rep(indent + 2) .. "$options \\") + + if self._is_help_command then + table.insert(buf, (" "):rep(indent + 2) .. '": :(' .. self._parent:_get_commands() .. ')" \\') + else + for _, argument in ipairs(self._arguments) do + local spec + if argument._choices then + spec = ": :(" .. table.concat(argument._choices, " ") .. ")" + else + spec = ": :_files" + end + if argument._maxargs == math.huge then + table.insert(buf, (" "):rep(indent + 2) .. '"*' .. spec .. '" \\') + break + end + for _ = 1, argument._maxargs do + table.insert(buf, (" "):rep(indent + 2) .. '"' .. spec .. '" \\') + end + end + + if #self._commands > 0 then + table.insert(buf, (" "):rep(indent + 2) .. '": :_' .. cmd_name .. '_cmds" \\') + table.insert(buf, (" "):rep(indent + 2) .. '"*:: :->args" \\') + end + end + + table.insert(buf, (" "):rep(indent + 2) .. "&& return 0") +end + +function Parser:_zsh_cmds(buf, cmd_name) + table.insert(buf, "\n_" .. cmd_name .. "_cmds() {") + table.insert(buf, " local -a commands=(") + + for _, command in ipairs(self._commands) do + local line = {} + if #command._aliases > 1 then + table.insert(line, "{" .. table.concat(command._aliases, ",") .. '}"') + else + table.insert(line, '"' .. command._name) + end + if command._description then + table.insert(line, ":" .. get_short_description(command):gsub('["`$]', "\\%0")) + end + table.insert(buf, " " .. table.concat(line) .. '"') + end + + table.insert(buf, ' )\n _describe "command" commands\n}') +end + +function Parser:_zsh_complete_help(buf, cmds_buf, cmd_name, indent) + if #self._commands == 0 then + return + end + + self:_zsh_cmds(cmds_buf, cmd_name) + table.insert(buf, "\n" .. (" "):rep(indent) .. "case $words[1] in") + + for _, command in ipairs(self._commands) do + local name = cmd_name .. "_" .. command._name + table.insert(buf, (" "):rep(indent + 2) .. table.concat(command._aliases, "|") .. ")") + command:_zsh_arguments(buf, name, indent + 4) + command:_zsh_complete_help(buf, cmds_buf, name, indent + 4) + table.insert(buf, (" "):rep(indent + 4) .. ";;\n") + end + + table.insert(buf, (" "):rep(indent) .. "esac") +end + +function Parser:get_zsh_complete() + self._basename = base_name(self._name) + assert(self:_is_shell_safe()) + local buf = {("#compdef %s\n"):format(self._basename)} + local cmds_buf = {} + table.insert(buf, "_" .. self._basename .. "() {") + if #self._commands > 0 then + table.insert(buf, " local context state state_descr line") + table.insert(buf, " typeset -A opt_args\n") + end + self:_zsh_arguments(buf, self._basename, 2) + self:_zsh_complete_help(buf, cmds_buf, self._basename, 2) + table.insert(buf, "\n return 1") + table.insert(buf, "}") + + local result = table.concat(buf, "\n") + if #cmds_buf > 0 then + result = result .. "\n" .. table.concat(cmds_buf, "\n") + end + return result .. "\n\n_" .. self._basename .. "\n" +end + +local function fish_escape(string) + return string:gsub("[\\']", "\\%0") +end + +function Parser:_fish_get_cmd(buf, indent) + if #self._commands == 0 then + return + end + + table.insert(buf, (" "):rep(indent) .. "set -e cmdline[1]") + table.insert(buf, (" "):rep(indent) .. "for arg in $cmdline") + table.insert(buf, (" "):rep(indent + 4) .. "switch $arg") + + for _, command in ipairs(self._commands) do + table.insert(buf, (" "):rep(indent + 8) .. "case " .. table.concat(command._aliases, " ")) + table.insert(buf, (" "):rep(indent + 12) .. "set cmd $cmd " .. command._name) + command:_fish_get_cmd(buf, indent + 12) + table.insert(buf, (" "):rep(indent + 12) .. "break") + end + + table.insert(buf, (" "):rep(indent + 4) .. "end") + table.insert(buf, (" "):rep(indent) .. "end") +end + +function Parser:_fish_complete_help(buf, basename) + local prefix = "complete -c " .. basename + table.insert(buf, "") + + for _, command in ipairs(self._commands) do + local aliases = table.concat(command._aliases, " ") + local line + if self._parent then + line = ("%s -n '__fish_%s_using_command %s' -xa '%s'") + :format(prefix, basename, self:_get_fullname(true), aliases) + else + line = ("%s -n '__fish_%s_using_command' -xa '%s'"):format(prefix, basename, aliases) + end + if command._description then + line = ("%s -d '%s'"):format(line, fish_escape(get_short_description(command))) + end + table.insert(buf, line) + end + + if self._is_help_command then + local line = ("%s -n '__fish_%s_using_command %s' -xa '%s'") + :format(prefix, basename, self:_get_fullname(true), self._parent:_get_commands()) + table.insert(buf, line) + end + + for _, option in ipairs(self._options) do + local parts = {prefix} + + if self._parent then + table.insert(parts, "-n '__fish_" .. basename .. "_seen_command " .. self:_get_fullname(true) .. "'") + end + + for _, alias in ipairs(option._aliases) do + if alias:match("^%-.$") then + table.insert(parts, "-s " .. alias:sub(2)) + elseif alias:match("^%-%-.+") then + table.insert(parts, "-l " .. alias:sub(3)) + end + end + + if option._choices then + table.insert(parts, "-xa '" .. table.concat(option._choices, " ") .. "'") + elseif option._minargs > 0 then + table.insert(parts, "-r") + end + + if option._description then + table.insert(parts, "-d '" .. fish_escape(get_short_description(option)) .. "'") + end + + table.insert(buf, table.concat(parts, " ")) + end + + for _, command in ipairs(self._commands) do + command:_fish_complete_help(buf, basename) + end +end + +function Parser:get_fish_complete() + self._basename = base_name(self._name) + assert(self:_is_shell_safe()) + local buf = {} + + if #self._commands > 0 then + table.insert(buf, ([[ +function __fish_%s_print_command + set -l cmdline (commandline -poc) + set -l cmd]]):format(self._basename)) + self:_fish_get_cmd(buf, 4) + table.insert(buf, ([[ + echo "$cmd" +end + +function __fish_%s_using_command + test (__fish_%s_print_command) = "$argv" + and return 0 + or return 1 +end + +function __fish_%s_seen_command + string match -q "$argv*" (__fish_%s_print_command) + and return 0 + or return 1 +end]]):format(self._basename, self._basename, self._basename, self._basename)) + end + + self:_fish_complete_help(buf, self._basename) + return table.concat(buf, "\n") .. "\n" +end + +local function get_tip(context, wrong_name) + local context_pool = {} + local possible_name + local possible_names = {} + + for name in pairs(context) do + if type(name) == "string" then + for i = 1, #name do + possible_name = name:sub(1, i - 1) .. name:sub(i + 1) + + if not context_pool[possible_name] then + context_pool[possible_name] = {} + end + + table.insert(context_pool[possible_name], name) + end + end + end + + for i = 1, #wrong_name + 1 do + possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1) + + if context[possible_name] then + possible_names[possible_name] = true + elseif context_pool[possible_name] then + for _, name in ipairs(context_pool[possible_name]) do + possible_names[name] = true + end + end + end + + local first = next(possible_names) + + if first then + if next(possible_names, first) then + local possible_names_arr = {} + + for name in pairs(possible_names) do + table.insert(possible_names_arr, "'" .. name .. "'") + end + + table.sort(possible_names_arr) + return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" + else + return "\nDid you mean '" .. first .. "'?" + end + else + return "" + end +end + +local ElementState = class({ + invocations = 0 +}) + +function ElementState:__call(state, element) + self.state = state + self.result = state.result + self.element = element + self.target = element._target or element:_get_default_target() + self.action, self.result[self.target] = element:_get_action() + return self +end + +function ElementState:error(fmt, ...) + self.state:error(fmt, ...) +end + +function ElementState:convert(argument, index) + local converter = self.element._convert + + if converter then + local ok, err + + if type(converter) == "function" then + ok, err = converter(argument) + elseif type(converter[index]) == "function" then + ok, err = converter[index](argument) + else + ok = converter[argument] + end + + if ok == nil then + self:error(err and "%s" or "malformed argument '%s'", err or argument) + end + + argument = ok + end + + return argument +end + +function ElementState:default(mode) + return self.element._defmode:find(mode) and self.element._default +end + +local function bound(noun, min, max, is_max) + local res = "" + + if min ~= max then + res = "at " .. (is_max and "most" or "least") .. " " + end + + local number = is_max and max or min + return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s") +end + +function ElementState:set_name(alias) + self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) +end + +function ElementState:invoke() + self.open = true + self.overwrite = false + + if self.invocations >= self.element._maxcount then + if self.element._overwrite then + self.overwrite = true + else + local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true) + self:error("%s must be used %s", self.name, num_times_repr) + end + else + self.invocations = self.invocations + 1 + end + + self.args = {} + + if self.element._maxargs <= 0 then + self:close() + end + + return self.open +end + +function ElementState:check_choices(argument) + if self.element._choices then + for _, choice in ipairs(self.element._choices) do + if argument == choice then + return + end + end + local choices_list = "'" .. table.concat(self.element._choices, "', '") .. "'" + local is_option = getmetatable(self.element) == Option + self:error("%s%s must be one of %s", is_option and "argument for " or "", self.name, choices_list) + end +end + +function ElementState:pass(argument) + self:check_choices(argument) + argument = self:convert(argument, #self.args + 1) + table.insert(self.args, argument) + + if #self.args >= self.element._maxargs then + self:close() + end + + return self.open +end + +function ElementState:complete_invocation() + while #self.args < self.element._minargs do + self:pass(self.element._default) + end +end + +function ElementState:close() + if self.open then + self.open = false + + if #self.args < self.element._minargs then + if self:default("a") then + self:complete_invocation() + else + if #self.args == 0 then + if getmetatable(self.element) == Argument then + self:error("missing %s", self.name) + elseif self.element._maxargs == 1 then + self:error("%s requires an argument", self.name) + end + end + + self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs)) + end + end + + local args + + if self.element._maxargs == 0 then + args = self.args[1] + elseif self.element._maxargs == 1 then + if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then + args = self.args + else + args = self.args[1] + end + else + args = self.args + end + + self.action(self.result, self.target, args, self.overwrite) + end +end + +local ParseState = class({ + result = {}, + options = {}, + arguments = {}, + argument_i = 1, + element_to_mutexes = {}, + mutex_to_element_state = {}, + command_actions = {} +}) + +function ParseState:__call(parser, error_handler) + self.parser = parser + self.error_handler = error_handler + self.charset = parser:_update_charset() + self:switch(parser) + return self +end + +function ParseState:error(fmt, ...) + self.error_handler(self.parser, fmt:format(...)) +end + +function ParseState:switch(parser) + self.parser = parser + + if parser._action then + table.insert(self.command_actions, {action = parser._action, name = parser._name}) + end + + for _, option in ipairs(parser._options) do + option = ElementState(self, option) + table.insert(self.options, option) + + for _, alias in ipairs(option.element._aliases) do + self.options[alias] = option + end + end + + for _, mutex in ipairs(parser._mutexes) do + for _, element in ipairs(mutex) do + if not self.element_to_mutexes[element] then + self.element_to_mutexes[element] = {} + end + + table.insert(self.element_to_mutexes[element], mutex) + end + end + + for _, argument in ipairs(parser._arguments) do + argument = ElementState(self, argument) + table.insert(self.arguments, argument) + argument:set_name() + argument:invoke() + end + + self.handle_options = parser._handle_options + self.argument = self.arguments[self.argument_i] + self.commands = parser._commands + + for _, command in ipairs(self.commands) do + for _, alias in ipairs(command._aliases) do + self.commands[alias] = command + end + end +end + +function ParseState:get_option(name) + local option = self.options[name] + + if not option then + self:error("unknown option '%s'%s", name, get_tip(self.options, name)) + else + return option + end +end + +function ParseState:get_command(name) + local command = self.commands[name] + + if not command then + if #self.commands > 0 then + self:error("unknown command '%s'%s", name, get_tip(self.commands, name)) + else + self:error("too many arguments") + end + else + return command + end +end + +function ParseState:check_mutexes(element_state) + if self.element_to_mutexes[element_state.element] then + for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do + local used_element_state = self.mutex_to_element_state[mutex] + + if used_element_state and used_element_state ~= element_state then + self:error("%s can not be used together with %s", element_state.name, used_element_state.name) + else + self.mutex_to_element_state[mutex] = element_state + end + end + end +end + +function ParseState:invoke(option, name) + self:close() + option:set_name(name) + self:check_mutexes(option, name) + + if option:invoke() then + self.option = option + end +end + +function ParseState:pass(arg) + if self.option then + if not self.option:pass(arg) then + self.option = nil + end + elseif self.argument then + self:check_mutexes(self.argument) + + if not self.argument:pass(arg) then + self.argument_i = self.argument_i + 1 + self.argument = self.arguments[self.argument_i] + end + else + local command = self:get_command(arg) + self.result[command._target or command._name] = true + + if self.parser._command_target then + self.result[self.parser._command_target] = command._name + end + + self:switch(command) + end +end + +function ParseState:close() + if self.option then + self.option:close() + self.option = nil + end +end + +function ParseState:finalize() + self:close() + + for i = self.argument_i, #self.arguments do + local argument = self.arguments[i] + if #argument.args == 0 and argument:default("u") then + argument:complete_invocation() + else + argument:close() + end + end + + if self.parser._require_command and #self.commands > 0 then + self:error("a command is required") + end + + for _, option in ipairs(self.options) do + option.name = option.name or ("option '%s'"):format(option.element._name) + + if option.invocations == 0 then + if option:default("u") then + option:invoke() + option:complete_invocation() + option:close() + end + end + + local mincount = option.element._mincount + + if option.invocations < mincount then + if option:default("a") then + while option.invocations < mincount do + option:invoke() + option:close() + end + elseif option.invocations == 0 then + self:error("missing %s", option.name) + else + self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount)) + end + end + end + + for i = #self.command_actions, 1, -1 do + self.command_actions[i].action(self.result, self.command_actions[i].name) + end +end + +function ParseState:parse(args) + for _, arg in ipairs(args) do + local plain = true + + if self.handle_options then + local first = arg:sub(1, 1) + + if self.charset[first] then + if #arg > 1 then + plain = false + + if arg:sub(2, 2) == first then + if #arg == 2 then + if self.options[arg] then + local option = self:get_option(arg) + self:invoke(option, arg) + else + self:close() + end + + self.handle_options = false + else + local equals = arg:find "=" + if equals then + local name = arg:sub(1, equals - 1) + local option = self:get_option(name) + + if option.element._maxargs <= 0 then + self:error("option '%s' does not take arguments", name) + end + + self:invoke(option, name) + self:pass(arg:sub(equals + 1)) + else + local option = self:get_option(arg) + self:invoke(option, arg) + end + end + else + for i = 2, #arg do + local name = first .. arg:sub(i, i) + local option = self:get_option(name) + self:invoke(option, name) + + if i ~= #arg and option.element._maxargs > 0 then + self:pass(arg:sub(i + 1)) + break + end + end + end + end + end + end + + if plain then + self:pass(arg) + end + end + + self:finalize() + return self.result +end + +function Parser:error(msg) + io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg)) + os.exit(1) +end + +-- Compatibility with strict.lua and other checkers: +local default_cmdline = rawget(_G, "arg") or {} + +function Parser:_parse(args, error_handler) + return ParseState(self, error_handler):parse(args or default_cmdline) +end + +function Parser:parse(args) + return self:_parse(args, self.error) +end + +local function xpcall_error_handler(err) + if not debug then + return tostring(err) + end + return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2) +end + +function Parser:pparse(args) + local parse_error + + local ok, result = xpcall(function() + return self:_parse(args, function(_, err) + parse_error = err + error(err, 0) + end) + end, xpcall_error_handler) + + if ok then + return true, result + elseif not parse_error then + error(result, 0) + else + return false, parse_error + end +end + +local argparse = {} + +argparse.version = "0.7.0" + +setmetatable(argparse, {__call = function(_, ...) + return Parser(default_cmdline[0]):add_help(true)(...) +end}) + +return argparse diff --git a/src/luarocks/vendor/dkjson.lua b/src/luarocks/vendor/dkjson.lua new file mode 100644 index 0000000..7a86724 --- /dev/null +++ b/src/luarocks/vendor/dkjson.lua @@ -0,0 +1,749 @@ +-- Module options: +local always_use_lpeg = false +local register_global_module_table = false +local global_module_name = 'json' + +--[==[ + +David Kolf's JSON module for Lua 5.1 - 5.4 + +Version 2.7 + + +For the documentation see the corresponding readme.txt or visit +. + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + + +Copyright (C) 2010-2024 David Heiko Kolf + +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. + +--]==] + +-- global dependencies: +local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset = + pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset +local error, require, pcall, select = error, require, pcall, select +local floor, huge = math.floor, math.huge +local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = + string.rep, string.gsub, string.sub, string.byte, string.char, + string.find, string.len, string.format +local strmatch = string.match +local concat = table.concat + +local json = { version = "dkjson 2.7" } + +local jsonlpeg = {} + +if register_global_module_table then + if always_use_lpeg then + _G[global_module_name] = jsonlpeg + else + _G[global_module_name] = json + end +end + +local _ENV = nil -- blocking globals in Lua 5.2 and later + +pcall (function() + -- Enable access to blocked metatables. + -- Don't worry, this module doesn't change anything in them. + local debmeta = require "debug".getmetatable + if debmeta then getmetatable = debmeta end +end) + +json.null = setmetatable ({}, { + __tojson = function () return "null" end +}) + +local function isarray (tbl) + local max, n, arraylen = 0, 0, 0 + for k,v in pairs (tbl) do + if k == 'n' and type(v) == 'number' then + arraylen = v + if v > max then + max = v + end + else + if type(k) ~= 'number' or k < 1 or floor(k) ~= k then + return false + end + if k > max then + max = k + end + n = n + 1 + end + end + if max > 10 and max > arraylen and max > n * 2 then + return false -- don't create an array with too many holes + end + return true, max +end + +local escapecodes = { + ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", + ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" +} + +local function escapeutf8 (uchar) + local value = escapecodes[uchar] + if value then + return value + end + local a, b, c, d = strbyte (uchar, 1, 4) + a, b, c, d = a or 0, b or 0, c or 0, d or 0 + if a <= 0x7f then + value = a + elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then + value = (a - 0xc0) * 0x40 + b - 0x80 + elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then + value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 + elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then + value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 + else + return "" + end + if value <= 0xffff then + return strformat ("\\u%.4x", value) + elseif value <= 0x10ffff then + -- encode as UTF-16 surrogate pair + value = value - 0x10000 + local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) + return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) + else + return "" + end +end + +local function fsub (str, pattern, repl) + -- gsub always builds a new string in a buffer, even when no match + -- exists. First using find should be more efficient when most strings + -- don't contain the pattern. + if strfind (str, pattern) then + return gsub (str, pattern, repl) + else + return str + end +end + +local function quotestring (value) + -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js + value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) + if strfind (value, "[\194\216\220\225\226\239]") then + value = fsub (value, "\194[\128-\159\173]", escapeutf8) + value = fsub (value, "\216[\128-\132]", escapeutf8) + value = fsub (value, "\220\143", escapeutf8) + value = fsub (value, "\225\158[\180\181]", escapeutf8) + value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) + value = fsub (value, "\226\129[\160-\175]", escapeutf8) + value = fsub (value, "\239\187\191", escapeutf8) + value = fsub (value, "\239\191[\176-\191]", escapeutf8) + end + return "\"" .. value .. "\"" +end +json.quotestring = quotestring + +local function replace(str, o, n) + local i, j = strfind (str, o, 1, true) + if i then + return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) + else + return str + end +end + +-- locale independent num2str and str2num functions +local decpoint, numfilter + +local function updatedecpoint () + decpoint = strmatch(tostring(0.5), "([^05+])") + -- build a filter that can be used to remove group separators + numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" +end + +updatedecpoint() + +local function num2str (num) + return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") +end + +local function str2num (str) + local num = tonumber(replace(str, ".", decpoint)) + if not num then + updatedecpoint() + num = tonumber(replace(str, ".", decpoint)) + end + return num +end + +local function addnewline2 (level, buffer, buflen) + buffer[buflen+1] = "\n" + buffer[buflen+2] = strrep (" ", level) + buflen = buflen + 2 + return buflen +end + +function json.addnewline (state) + if state.indent then + state.bufferlen = addnewline2 (state.level or 0, + state.buffer, state.bufferlen or #(state.buffer)) + end +end + +local encode2 -- forward declaration + +local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) + local kt = type (key) + if kt ~= 'string' and kt ~= 'number' then + return nil, "type '" .. kt .. "' is not supported as a key by JSON." + end + if prev then + buflen = buflen + 1 + buffer[buflen] = "," + end + if indent then + buflen = addnewline2 (level, buffer, buflen) + end + buffer[buflen+1] = quotestring (key) + buffer[buflen+2] = ":" + return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) +end + +local function appendcustom(res, buffer, state) + local buflen = state.bufferlen + if type (res) == 'string' then + buflen = buflen + 1 + buffer[buflen] = res + end + return buflen +end + +local function exception(reason, value, state, buffer, buflen, defaultmessage) + defaultmessage = defaultmessage or reason + local handler = state.exception + if not handler then + return nil, defaultmessage + else + state.bufferlen = buflen + local ret, msg = handler (reason, value, state, defaultmessage) + if not ret then return nil, msg or defaultmessage end + return appendcustom(ret, buffer, state) + end +end + +function json.encodeexception(reason, value, state, defaultmessage) + return quotestring("<" .. defaultmessage .. ">") +end + +encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) + local valtype = type (value) + local valmeta = getmetatable (value) + valmeta = type (valmeta) == 'table' and valmeta -- only tables + local valtojson = valmeta and valmeta.__tojson + if valtojson then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + state.bufferlen = buflen + local ret, msg = valtojson (value, state) + if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end + tables[value] = nil + buflen = appendcustom(ret, buffer, state) + elseif value == nil then + buflen = buflen + 1 + buffer[buflen] = "null" + elseif valtype == 'number' then + local s + if value ~= value or value >= huge or -value >= huge then + -- This is the behaviour of the original JSON implementation. + s = "null" + else + s = num2str (value) + end + buflen = buflen + 1 + buffer[buflen] = s + elseif valtype == 'boolean' then + buflen = buflen + 1 + buffer[buflen] = value and "true" or "false" + elseif valtype == 'string' then + buflen = buflen + 1 + buffer[buflen] = quotestring (value) + elseif valtype == 'table' then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + level = level + 1 + local isa, n = isarray (value) + if n == 0 and valmeta and valmeta.__jsontype == 'object' then + isa = false + end + local msg + if isa then -- JSON array + buflen = buflen + 1 + buffer[buflen] = "[" + for i = 1, n do + buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + if i < n then + buflen = buflen + 1 + buffer[buflen] = "," + end + end + buflen = buflen + 1 + buffer[buflen] = "]" + else -- JSON object + local prev = false + buflen = buflen + 1 + buffer[buflen] = "{" + local order = valmeta and valmeta.__jsonorder or globalorder + if order then + local used = {} + n = #order + for i = 1, n do + local k = order[i] + local v = value[k] + if v ~= nil then + used[k] = true + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + for k,v in pairs (value) do + if not used[k] then + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + else -- unordered + for k,v in pairs (value) do + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + if indent then + buflen = addnewline2 (level - 1, buffer, buflen) + end + buflen = buflen + 1 + buffer[buflen] = "}" + end + tables[value] = nil + else + return exception ('unsupported type', value, state, buffer, buflen, + "type '" .. valtype .. "' is not supported by JSON.") + end + return buflen +end + +function json.encode (value, state) + state = state or {} + local oldbuffer = state.buffer + local buffer = oldbuffer or {} + state.buffer = buffer + updatedecpoint() + local ret, msg = encode2 (value, state.indent, state.level or 0, + buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) + if not ret then + error (msg, 2) + elseif oldbuffer == buffer then + state.bufferlen = ret + return true + else + state.bufferlen = nil + state.buffer = nil + return concat (buffer) + end +end + +local function loc (str, where) + local line, pos, linepos = 1, 1, 0 + while true do + pos = strfind (str, "\n", pos, true) + if pos and pos < where then + line = line + 1 + linepos = pos + pos = pos + 1 + else + break + end + end + return "line " .. line .. ", column " .. (where - linepos) +end + +local function unterminated (str, what, where) + return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) +end + +local function scanwhite (str, pos) + while true do + pos = strfind (str, "%S", pos) + if not pos then return nil end + local sub2 = strsub (str, pos, pos + 1) + if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then + -- UTF-8 Byte Order Mark + pos = pos + 3 + elseif sub2 == "//" then + pos = strfind (str, "[\n\r]", pos + 2) + if not pos then return nil end + elseif sub2 == "/*" then + pos = strfind (str, "*/", pos + 2) + if not pos then return nil end + pos = pos + 2 + else + return pos + end + end +end + +local escapechars = { + ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", + ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" +} + +local function unichar (value) + if value < 0 then + return nil + elseif value <= 0x007f then + return strchar (value) + elseif value <= 0x07ff then + return strchar (0xc0 + floor(value/0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0xffff then + return strchar (0xe0 + floor(value/0x1000), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0x10ffff then + return strchar (0xf0 + floor(value/0x40000), + 0x80 + (floor(value/0x1000) % 0x40), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + else + return nil + end +end + +local function scanstring (str, pos) + local lastpos = pos + 1 + local buffer, n = {}, 0 + while true do + local nextpos = strfind (str, "[\"\\]", lastpos) + if not nextpos then + return unterminated (str, "string", pos) + end + if nextpos > lastpos then + n = n + 1 + buffer[n] = strsub (str, lastpos, nextpos - 1) + end + if strsub (str, nextpos, nextpos) == "\"" then + lastpos = nextpos + 1 + break + else + local escchar = strsub (str, nextpos + 1, nextpos + 1) + local value + if escchar == "u" then + value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) + if value then + local value2 + if 0xD800 <= value and value <= 0xDBff then + -- we have the high surrogate of UTF-16. Check if there is a + -- low surrogate escaped nearby to combine them. + if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then + value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) + if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then + value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 + else + value2 = nil -- in case it was out of range for a low surrogate + end + end + end + value = value and unichar (value) + if value then + if value2 then + lastpos = nextpos + 12 + else + lastpos = nextpos + 6 + end + end + end + end + if not value then + value = escapechars[escchar] or escchar + lastpos = nextpos + 2 + end + n = n + 1 + buffer[n] = value + end + end + if n == 1 then + return buffer[1], lastpos + elseif n > 1 then + return concat (buffer), lastpos + else + return "", lastpos + end +end + +local scanvalue -- forward declaration + +local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) + local len = strlen (str) + local tbl, n = {}, 0 + local pos = startpos + 1 + if what == 'object' then + setmetatable (tbl, objectmeta) + else + setmetatable (tbl, arraymeta) + end + while true do + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + local char = strsub (str, pos, pos) + if char == closechar then + return tbl, pos + 1 + end + local val1, err + val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + if char == ":" then + if val1 == nil then + return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" + end + pos = scanwhite (str, pos + 1) + if not pos then return unterminated (str, what, startpos) end + local val2 + val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + tbl[val1] = val2 + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + else + n = n + 1 + tbl[n] = val1 + end + if char == "," then + pos = pos + 1 + end + end +end + +scanvalue = function (str, pos, nullval, objectmeta, arraymeta) + pos = pos or 1 + pos = scanwhite (str, pos) + if not pos then + return nil, strlen (str) + 1, "no valid JSON value (reached the end)" + end + local char = strsub (str, pos, pos) + if char == "{" then + return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) + elseif char == "[" then + return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) + elseif char == "\"" then + return scanstring (str, pos) + else + local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) + if pstart then + local number = str2num (strsub (str, pstart, pend)) + if number then + return number, pend + 1 + end + end + pstart, pend = strfind (str, "^%a%w*", pos) + if pstart then + local name = strsub (str, pstart, pend) + if name == "true" then + return true, pend + 1 + elseif name == "false" then + return false, pend + 1 + elseif name == "null" then + return nullval, pend + 1 + end + end + return nil, pos, "no valid JSON value at " .. loc (str, pos) + end +end + +local function optionalmetatables(...) + if select("#", ...) > 0 then + return ... + else + return {__jsontype = 'object'}, {__jsontype = 'array'} + end +end + +function json.decode (str, pos, nullval, ...) + local objectmeta, arraymeta = optionalmetatables(...) + return scanvalue (str, pos, nullval, objectmeta, arraymeta) +end + +function json.use_lpeg () + local g = require ("lpeg") + + if type(g.version) == 'function' and g.version() == "0.11" then + error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" + end + + local pegmatch = g.match + local P, S, R = g.P, g.S, g.R + + local function ErrorCall (str, pos, msg, state) + if not state.msg then + state.msg = msg .. " at " .. loc (str, pos) + state.pos = pos + end + return false + end + + local function Err (msg) + return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) + end + + local function ErrorUnterminatedCall (str, pos, what, state) + return ErrorCall (str, pos - 1, "unterminated " .. what, state) + end + + local SingleLineComment = P"//" * (1 - S"\n\r")^0 + local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" + local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 + + local function ErrUnterminated (what) + return g.Cmt (g.Cc (what) * g.Carg (2), ErrorUnterminatedCall) + end + + local PlainChar = 1 - S"\"\\\n\r" + local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars + local HexDigit = R("09", "af", "AF") + local function UTF16Surrogate (match, pos, high, low) + high, low = tonumber (high, 16), tonumber (low, 16) + if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then + return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) + else + return false + end + end + local function UTF16BMP (hex) + return unichar (tonumber (hex, 16)) + end + local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) + local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP + local Char = UnicodeEscape + EscapeSequence + PlainChar + local String = P"\"" * (g.Cs (Char ^ 0) * P"\"" + ErrUnterminated "string") + local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) + local Fractal = P"." * R"09"^0 + local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 + local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num + local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) + local SimpleValue = Number + String + Constant + local ArrayContent, ObjectContent + + -- The functions parsearray and parseobject parse only a single value/pair + -- at a time and store them directly to avoid hitting the LPeg limits. + local function parsearray (str, pos, nullval, state) + local obj, cont + local start = pos + local npos + local t, nt = {}, 0 + repeat + obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) + if cont == 'end' then + return ErrorUnterminatedCall (str, start, "array", state) + end + pos = npos + if cont == 'cont' or cont == 'last' then + nt = nt + 1 + t[nt] = obj + end + until cont ~= 'cont' + return pos, setmetatable (t, state.arraymeta) + end + + local function parseobject (str, pos, nullval, state) + local obj, key, cont + local start = pos + local npos + local t = {} + repeat + key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) + if cont == 'end' then + return ErrorUnterminatedCall (str, start, "object", state) + end + pos = npos + if cont == 'cont' or cont == 'last' then + t[key] = obj + end + until cont ~= 'cont' + return pos, setmetatable (t, state.objectmeta) + end + + local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) + local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) + local Value = Space * (Array + Object + SimpleValue) + local ExpectedValue = Value + Space * Err "value expected" + local ExpectedKey = String + Err "key expected" + local End = P(-1) * g.Cc'end' + local ErrInvalid = Err "invalid JSON" + ArrayContent = (Value * Space * (P"," * g.Cc'cont' + P"]" * g.Cc'last'+ End + ErrInvalid) + g.Cc(nil) * (P"]" * g.Cc'empty' + End + ErrInvalid)) * g.Cp() + local Pair = g.Cg (Space * ExpectedKey * Space * (P":" + Err "colon expected") * ExpectedValue) + ObjectContent = (g.Cc(nil) * g.Cc(nil) * P"}" * g.Cc'empty' + End + (Pair * Space * (P"," * g.Cc'cont' + P"}" * g.Cc'last' + End + ErrInvalid) + ErrInvalid)) * g.Cp() + local DecodeValue = ExpectedValue * g.Cp () + + jsonlpeg.version = json.version + jsonlpeg.encode = json.encode + jsonlpeg.null = json.null + jsonlpeg.quotestring = json.quotestring + jsonlpeg.addnewline = json.addnewline + jsonlpeg.encodeexception = json.encodeexception + jsonlpeg.using_lpeg = true + + function jsonlpeg.decode (str, pos, nullval, ...) + local state = {} + state.objectmeta, state.arraymeta = optionalmetatables(...) + local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) + if state.msg then + return nil, state.pos, state.msg + else + return obj, retpos + end + end + + -- cache result of this function: + json.use_lpeg = function () return jsonlpeg end + jsonlpeg.use_lpeg = json.use_lpeg + + return jsonlpeg +end + +if always_use_lpeg then + return json.use_lpeg() +end + +return json + diff --git a/test_regression.sh b/test_regression.sh new file mode 100755 index 0000000..4780893 --- /dev/null +++ b/test_regression.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +# ## Usage examples: +# +# Test current branch against master: +# test_regression.sh +# Test current branch against another branch: +# test_regression.sh another-branch +# Test current branch against master passing arguments to busted: +# test_regression.sh -- --exclude-tags=flaky +# Test current branch against next passing arguments to busted: +# test_regression.sh next -- --exclude-tags=flaky + +if [ $(git status --untracked-files=no --porcelain | wc -l) != "0" ] +then + echo "==============================" + echo "Local tree is not clean, please commit or stash before running this." + echo "==============================" + exit 1 +fi + +newbranch=$(git rev-parse --abbrev-ref HEAD) +if [ "$newbranch" = "master" ] || [ "$newbranch" = "next" ] +then + echo "==============================" + echo "Please run this from a topic branch, and not from 'next' or 'master'." + echo "==============================" + exit 1 +fi + +basebranch="$1" +if [ "$1" == "" ] || [ "$1" == "--" ] +then + basebranch=master +else + basebranch="$1" + shift +fi +if [ "$1" == "--" ] +then + shift +fi + +git checkout "$newbranch" +rm -rf .spec-new +rm -rf .spec-old +cp -a spec .spec-new +git checkout "$basebranch" + +echo "----------------------------------------" +echo "Tests changed between $basebranch and $newbranch:" +echo "----------------------------------------" +specfiles=($(git diff-tree --no-commit-id --name-only -r "..$newbranch" | grep "^spec/")) +echo "${specfiles[@]}" +echo "----------------------------------------" + +mv spec .spec-old +mv .spec-new spec +./luarocks test -- "${specfiles[@]}" "$@" +if [ $? = 0 ] +then + git checkout . + git checkout $newbranch + echo "==============================" + echo "Regression test does not trigger the issue in base branch" + echo "==============================" + exit 1 +fi +mv spec .spec-new +mv .spec-old spec +git checkout $newbranch +./luarocks test -- "${specfiles[@]}" "$@" +ret=$? +if [ "$ret" != "0" ] +then + echo "==============================" + echo "New branch does not fix issue (returned $ret)" + echo "==============================" + exit 1 +fi + +echo "==============================" +echo "All good! New branch fixes regression!" +echo "==============================" +exit 0 + -- cgit v1.2.3