diff --git a/.gitignore b/.gitignore index 450f32ec4..96f94ee46 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,8 @@ cabal.sandbox.config .stack-work/ cabal.project.local .HTF/ +/bazel-bin +/bazel-genfiles +/bazel-out +/bazel-rules_haskell +/bazel-testlogs diff --git a/WORKSPACE b/WORKSPACE index cbe43c69f..1c722d661 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1 +1,6 @@ workspace(name = "io_tweag_rules_haskell") + +local_repository( + name = "examples", + path = "examples" +) \ No newline at end of file diff --git a/examples/BUILD b/examples/BUILD deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/WORKSPACE b/examples/WORKSPACE new file mode 100644 index 000000000..7e1f4f0a3 --- /dev/null +++ b/examples/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "examples") \ No newline at end of file diff --git a/examples/hello-lib/BUILD b/examples/hello-lib/BUILD new file mode 100644 index 000000000..b584ff6b1 --- /dev/null +++ b/examples/hello-lib/BUILD @@ -0,0 +1,15 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_tweag_rules_haskell//haskell:haskell.bzl", + "haskell_library", +) + +haskell_library( + name = 'hello-lib', + srcs = [ + 'MsgType.hs', + 'Unused.hs', + 'Lib.hs' + ] +) diff --git a/examples/hello-lib/Lib.hs b/examples/hello-lib/Lib.hs new file mode 100644 index 000000000..0e21618f6 --- /dev/null +++ b/examples/hello-lib/Lib.hs @@ -0,0 +1,6 @@ +module Lib (libText) where + +import MsgType (Msg) + +libText :: Msg +libText = "hello world" diff --git a/examples/hello-lib/MsgType.hs b/examples/hello-lib/MsgType.hs new file mode 100644 index 000000000..d5905e0f2 --- /dev/null +++ b/examples/hello-lib/MsgType.hs @@ -0,0 +1,4 @@ +module MsgType (Msg) where + + +type Msg = String diff --git a/examples/hello-lib/Unused.hs b/examples/hello-lib/Unused.hs new file mode 100644 index 000000000..13b974228 --- /dev/null +++ b/examples/hello-lib/Unused.hs @@ -0,0 +1,4 @@ +module Unused (someInt) where + +someInt :: Int +someInt = 9 diff --git a/examples/hello-world/BUILD b/examples/hello-world/BUILD new file mode 100644 index 000000000..aa3763c8a --- /dev/null +++ b/examples/hello-world/BUILD @@ -0,0 +1,12 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_tweag_rules_haskell//haskell:haskell.bzl", + "haskell_binary" +) + +haskell_binary( + name = "hello-world", + srcs = ["Main.hs"], + deps = ["@examples//hello-lib:hello-lib"] +) diff --git a/examples/hello-world/Main.hs b/examples/hello-world/Main.hs new file mode 100644 index 000000000..aad411cde --- /dev/null +++ b/examples/hello-world/Main.hs @@ -0,0 +1,6 @@ +module Main (main) where + +import Lib (libText) + +main :: IO () +main = putStrLn libText diff --git a/haskell/BUILD b/haskell/BUILD new file mode 100644 index 000000000..cd7c59004 --- /dev/null +++ b/haskell/BUILD @@ -0,0 +1,6 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "haskell.bzl", + "toolchain.bzl", +]) \ No newline at end of file diff --git a/haskell/haskell.bzl b/haskell/haskell.bzl index c96f7bedb..33d925f73 100644 --- a/haskell/haskell.bzl +++ b/haskell/haskell.bzl @@ -1,38 +1,120 @@ -HASKELL_FILETYPE = FileType([".hs"]) +load(":toolchain.bzl", + "HaskellPackageInfo", + "ar_args", + "ghc_bin_link_args", + "ghc_bin_obj_args", + "ghc_lib_args", + "mk_registration_file", + "register_package", + "src_to_ext", +) + +def _haskell_binary_impl(ctx): + depInputs = [] + systemLibs = [] + for d in ctx.attr.deps: + # depend on output of deps, i.e. package file + depInputs += d.files.to_list() + # We need interface files of the package for compilation + depInputs += d[HaskellPackageInfo].interfaceFiles + # Lastly we need library object for linking + systemLibs.append(d[HaskellPackageInfo].systemLib) + + objDir = ctx.actions.declare_directory("objects") + binObjs = [ctx.actions.declare_file(src_to_ext(ctx, s, "o", directory=objDir)) + for s in ctx.files.srcs] + + # Compile sources of the binary. + ctx.actions.run( + inputs = ctx.files.srcs + depInputs + systemLibs, + outputs = binObjs + [objDir], + use_default_shell_env = True, + progress_message = "Building {0}".format(ctx.attr.name), + executable = "ghc", + arguments = [ghc_bin_obj_args(ctx, objDir)], + ) + + # Link everything together + linkTarget, linkArgs = ghc_bin_link_args(ctx, binObjs, systemLibs) + ctx.actions.run( + inputs = binObjs + systemLibs + [linkTarget], + outputs = [ctx.outputs.executable], + use_default_shell_env = True, + progress_message = "Linking {0}".format(ctx.outputs.executable), + executable = "ghc", + arguments = [linkArgs], + ) + +def _haskell_library_impl(ctx): -# TODO -haskell_library = 1 + objDir = ctx.actions.declare_directory("objects") + objectFiles = [ctx.actions.declare_file(src_to_ext(ctx, s, "o", directory=objDir)) + for s in ctx.files.srcs] + + ifaceDir = ctx.actions.declare_directory("interfaces") + interfaceFiles = [ctx.actions.declare_file(src_to_ext(ctx, s, "hi", directory=ifaceDir)) + for s in ctx.files.srcs ] + + # Compile library files + # + # TODO: Library deps + ctx.actions.run( + inputs = ctx.files.srcs, + outputs = [ifaceDir, objDir] + objectFiles + interfaceFiles, + use_default_shell_env = True, + progress_message = "Compiling {0}".format(ctx.attr.name), + executable = "ghc", + arguments = [ghc_lib_args(ctx, objDir, ifaceDir)] + ) + + # Make library archive; currently only static + # + # TODO: configurable shared &c. see various scenarios in buck + libDir = ctx.actions.declare_directory("lib") + systemLib = ctx.actions.declare_file("{0}/lib{1}.a".format(libDir.basename, ctx.attr.name)) + + ctx.actions.run( + inputs = objectFiles, + outputs = [systemLib, libDir], + use_default_shell_env = True, + executable = "ar", + arguments = [ar_args(ctx, systemLib, objectFiles)], + ) + + # Create and register ghc package. + pkgId = "{0}-{1}".format(ctx.attr.name, ctx.attr.version) + confFile = ctx.actions.declare_file("{0}.conf".format(pkgId)) + cacheFile = ctx.actions.declare_file("package.cache") + registrationFile = mk_registration_file(ctx, pkgId, ifaceDir, libDir) + ctx.actions.run_shell( + inputs = [systemLib, ifaceDir, registrationFile], + outputs = [confFile, cacheFile], + use_default_shell_env = True, + command = register_package(ifaceDir, registrationFile, confFile.dirname) + ) + return [HaskellPackageInfo( packageName = pkgId, + pkgDb = confFile.dirname, + systemLib = systemLib, + interfaceFiles = interfaceFiles)] _haskell_common_attrs = { - "srcs": attr.label_list(allow_files = HASKELL_FILETYPE), - "deps": attr.label_list(), + "srcs": attr.label_list(allow_files = FileType([".hs"])), + "deps": attr.label_list(), } -def _haskell_binary_impl(ctx): - haskell_binary = ctx.outputs.executable - - compile_inputs = ctx.files.srcs - - ctx.actions.run( - inputs = compile_inputs, - outputs = [haskell_binary], - mnemonic = "Ghc", - executable = "ghc", - arguments = [ - "-o", - haskell_binary.path, - ] + [ - src.path for src in compile_inputs - ], - use_default_shell_env = True, - progress_message = ("Compiling Haskell binary %s (%d files)" - % (ctx.label.name, len(ctx.files.srcs)))) - - return struct(haskell_srcs = ctx.files.srcs, - haskell_deps = ctx.attr.deps) +haskell_library = rule( + _haskell_library_impl, + outputs = { + "conf": "%{name}-%{version}.conf", + "packageCache": "package.cache" + }, + attrs = _haskell_common_attrs + { + "version": attr.string(default="1.0.0"), + } +) haskell_binary = rule( - _haskell_binary_impl, - attrs = _haskell_common_attrs, - executable = True, + _haskell_binary_impl, + executable = True, + attrs = _haskell_common_attrs, ) diff --git a/haskell/toolchain.bzl b/haskell/toolchain.bzl new file mode 100644 index 000000000..5ee096fc9 --- /dev/null +++ b/haskell/toolchain.bzl @@ -0,0 +1,200 @@ +HaskellPackageInfo = provider( + doc = "Package information exposed by Haskell libraries.", + fields = { + "packageName": "Package name, usually of the form name-version.", + "pkgDb": "Directory containing the registered package database.", + "systemLib": "Compiled library archive.", + "interfaceFiles": "Interface files belonging to the package." + } +) + +def ghc_bin_obj_args(ctx, objDir): + """Build arguments for Haskell binary object building. + + Args: + ctx: Rule context. + objDir: Output directory for object files. + """ + args = ctx.actions.args() + args.add("-no-link") + args.add(ctx.files.srcs) + args.add(["-odir", objDir]) + for d in ctx.attr.deps: + args.add(["-package", d[HaskellPackageInfo].packageName]) + args.add(["-package-db", d[HaskellPackageInfo].pkgDb]) + return args + +def ghc_bin_link_args(ctx, binObjs, systemLibs): + """Build arguments for Haskell binary linking stage. + + Also creates an empty library archive to as a build target: this + stops GHC from complaining about no target when we only want to use + it for linking. This result is silently passed into the arguments + but the link target should be explicitly added to the action as an + input. + + Args: + ctx: Rule context. + binObjs: Object files to include during linking. + systemLibs: Library archives to include during linking. + + """ + # Create empty archive so that GHC has some input files to work on during linking + # + # https://github.com/facebook/buck/blob/126d576d5c07ce382e447533b57794ae1a358cc2/src/com/facebook/buck/haskell/HaskellDescriptionUtils.java#L295 + dummy = ctx.actions.declare_file("BazelDummy.hs") + dummyObj = ctx.actions.declare_file("BazelDummy.o") + ctx.actions.write(output=dummy, content="module BazelDummy () where") + dummyLib = ctx.actions.declare_file("libempty.a") + dummyArgs = ctx.actions.args() + dummyArgs.add(["-no-link", dummy]) + ctx.actions.run( + inputs = [dummy], + outputs = [dummyObj], + use_default_shell_env = True, + executable = "ghc", + arguments = [dummyArgs] + ) + # TODO: Buck also calls ranlib on the output: should we? + ctx.actions.run( + inputs = [dummyObj], + outputs = [dummyLib], + use_default_shell_env = True, + executable = "ar", + arguments = [ar_args(ctx, dummyLib, [dummyObj])] + ) + + args = ctx.actions.args() + args.add(["-o", ctx.outputs.executable]) + args.add(dummyLib) + for o in binObjs: + args.add(["-optl", o]) + for l in systemLibs: + args.add(["-optl", l]) + return dummyLib, args + +def ghc_lib_args(ctx, objDir, ifaceDir): + """Build arguments for Haskell package build. + + Args: + ctx: Rule context. + objDir: Output directory for object files. + ifaceDir: Output directory for interface files. + """ + args = ctx.actions.args() + args.add(["-package-name", "{0}-{1}".format(ctx.attr.name, ctx.attr.version)]) + args.add(["-odir", objDir, "-hidir", ifaceDir]) + args.add("-i") + args.add(ctx.files.srcs) + return args + +def take_haskell_module(ctx, f): + """Given Haskell source file, get the path hierarchy without the extension. + + some-workspace/some-package/Foo/Bar/Baz.hs => Foo/Bar/Baz + + Args: + f: Haskell source file. + """ + pkgDir = "{0}/{1}/".format(ctx.label.workspace_root, ctx.label.package) + pkgDirLen = len(pkgDir) + # TODO: hack; depending on circumstance, workspace_root can have a + # leading / which f.path does not have: if that's the case, drop one + # less character + if pkgDirLen > 0 and pkgDir[0] == '/': + pkgDirLen -= 1 + return f.path[pkgDirLen:f.path.rfind(".")] + +def mk_registration_file(ctx, pkgId, interfaceDir, libDir): + """Prepare a file we'll use to register a package with. + + Args: + ctx: Rule context. + pkgId: Package ID, usually in name-version format. + interfaceDir: Directory with interface files. + libDir: Directory containing library archive(s). + """ + registrationFile = ctx.actions.declare_file("registration-file") + registrationFileDict = { + "name": ctx.attr.name, + "version": ctx.attr.version, + "id": pkgId, + "key": pkgId, + "exposed": "True", + # Translate module source paths in Haskell modules. Best effort + # without GHC help. + "exposed-modules": " ".join([take_haskell_module(ctx, f).replace("/", ".") + for f in ctx.files.srcs]), + "import-dirs": "${{pkgroot}}/{0}/{1}".format(ctx.label.name, interfaceDir.basename), + "library-dirs": "${{pkgroot}}/{0}/{1}".format(ctx.label.name, libDir.basename), + "hs-libraries": ctx.attr.name, + "depends": "" # TODO + } + ctx.actions.write( + output=registrationFile, + content="\n".join(['{0}: {1}'.format(k, v) + for k, v in registrationFileDict.items()]) + ) + return registrationFile + +def register_package(ifaceDir, registrationFile, pkgDir): + """Initialises, registers and checks ghc DB package. + + Args: + ifaceDir: undefined + registrationFile: undefined + pkgDir: undefined + """ + scratchDir = "ghc-pkg-init-scratch" + initPackage = "ghc-pkg init {0}".format(scratchDir) + # Move things out of scratch to make it easier for everyone. ghc-pkg + # refuses to use an existing directory. + mvFromScratch = "mv {0}/* {1}".format(scratchDir, pkgDir) + registerPackage = " ".join( + [ "GHC_PACKAGE_PATH=''", + "ghc-pkg", + "-v0", + "register", + "--package-conf={0}".format(pkgDir), + "--no-expand-pkgroot", + "--force-files", + registrationFile.path, + ] + ) + # make sure what we produce is valid + checkPackage = "ghc-pkg check --package-conf={0}".format(pkgDir) + return " && ".join( + [ initPackage, + mvFromScratch, + registerPackage, + checkPackage + ] + ) + +def src_to_ext(ctx, src, ext, directory=None): + """Haskell source file to Haskell file living in specified directory. + + Args: + ctx: Rule context. + src: Haskell source file. + ext: New extension. + directory: Directory the new file should live in. + """ + fp = "{0}.{1}".format(take_haskell_module(ctx, src), ext) + if directory == None: + return fp + else: + return "{0}/{1}".format(directory.basename, fp) + +def ar_args(ctx, systemLib, objectFiles): + """Create arguments for `ar` tool. + + Args: + systemLib: The declared static library to generate. + objectFiles: Object files to pack into the library. + """ + args = ctx.actions.args() + args.add("qc") + args.add(systemLib) + args.add(objectFiles) + return args diff --git a/tests/BUILD b/tests/BUILD index 8b73cf2c4..10a7fccdd 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -1,14 +1,13 @@ package(default_testonly = 1) -load("//haskell:haskell.bzl", - "haskell_library", +load("@io_tweag_rules_haskell//haskell:haskell.bzl", "haskell_binary", ) haskell_binary( name = "hello", - srcs = ["hello.hs"], - # main_is = "hello.hs", + srcs = ["Main.hs"], + deps = ["//tests/test-lib:test-lib"] ) [sh_test( diff --git a/tests/Main.hs b/tests/Main.hs new file mode 100644 index 000000000..d6c0271e2 --- /dev/null +++ b/tests/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import TestLib (testMessage) + +main = do + putStrLn testMessage diff --git a/tests/hello.hs b/tests/hello.hs deleted file mode 100644 index 45286be1c..000000000 --- a/tests/hello.hs +++ /dev/null @@ -1,4 +0,0 @@ -module Main where - -main = do - putStrLn "hello" diff --git a/tests/test-lib/BUILD b/tests/test-lib/BUILD new file mode 100644 index 000000000..8d2c1c6b7 --- /dev/null +++ b/tests/test-lib/BUILD @@ -0,0 +1,13 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_tweag_rules_haskell//haskell:haskell.bzl", + "haskell_library", +) + +haskell_library( + name = 'test-lib', + srcs = [ + 'TestLib.hs', + ] +) diff --git a/tests/test-lib/TestLib.hs b/tests/test-lib/TestLib.hs new file mode 100644 index 000000000..c1797ccc9 --- /dev/null +++ b/tests/test-lib/TestLib.hs @@ -0,0 +1,4 @@ +module TestLib (testMessage) where + +testMessage :: String +testMessage = "hello"