From 398458675ac3c69aba981b9bfa7ff7302736faaf Mon Sep 17 00:00:00 2001 From: Dusty Phillips Date: Tue, 27 Aug 2024 16:23:50 -0300 Subject: [PATCH] Add another level of indirection for projects. This is a step towards project dependencies... I hope. I realized that I don't want a separate build directory for every project; just for the one that is being built. if my package depends on gleam_stdlib, I need the files in that package to be in the build directory for this one. I think I'm still doing it wrong, though because gleam_stdlib is the package name and right now if you tried to build something like that you'd need to import from gleam_stdlib. But e.g. gleam/io is not in a gleam_stdlib namespace. --- gleam.toml | 1 + manifest.toml | 1 + src/compiler.gleam | 39 +++------ src/compiler/package.gleam | 84 ++++++++++++++----- src/compiler/project.gleam | 52 ++++++++++++ src/errors.gleam | 5 ++ src/macabre.gleam | 67 +++++++++------ test/compiler/directory_structure_tests.gleam | 71 +++++++++++----- 8 files changed, 228 insertions(+), 92 deletions(-) create mode 100644 src/compiler/project.gleam diff --git a/gleam.toml b/gleam.toml index 203162a..0996800 100644 --- a/gleam.toml +++ b/gleam.toml @@ -19,6 +19,7 @@ argv = ">= 1.0.2 and < 2.0.0" simplifile = ">= 2.0.1 and < 3.0.0" filepath = ">= 1.0.0 and < 2.0.0" glexer = ">= 1.0.1 and < 2.0.0" +tom = ">= 1.0.1 and < 2.0.0" [dev-dependencies] gleescript = ">= 1.4.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 8d94dbd..b3b471d 100644 --- a/manifest.toml +++ b/manifest.toml @@ -32,3 +32,4 @@ glexer = { version = ">= 1.0.1 and < 2.0.0" } pprint = { version = ">= 1.0.3 and < 2.0.0" } simplifile = { version = ">= 2.0.1 and < 3.0.0" } temporary = { version = ">= 1.0.0 and < 2.0.0" } +tom = { version = ">= 1.0.1 and < 2.0.0" } diff --git a/src/compiler.gleam b/src/compiler.gleam index a229dca..a473018 100644 --- a/src/compiler.gleam +++ b/src/compiler.gleam @@ -1,12 +1,9 @@ import compiler/generator import compiler/package +import compiler/project import compiler/transformer import glance import gleam/dict -import gleam/list -import gleam/option -import gleam/result -import gleam/string pub fn compile_module(glance_module: glance.Module) -> String { glance_module @@ -16,34 +13,22 @@ pub fn compile_module(glance_module: glance.Module) -> String { pub fn compile_package(package: package.GleamPackage) -> package.CompiledPackage { package.CompiledPackage( + config: package.config, base_directory: package.base_directory, - main_module: dict.get(package.modules, package.main_module) - |> result.try(fn(mod) { mod.functions |> has_main_function }) - |> result.replace(package.main_module |> string.drop_right(6)) - |> option.from_result, modules: package.modules |> dict.map_values(fn(_key, value) { compile_module(value) }), external_import_files: package.external_import_files, ) } -pub fn has_main_function( - functions: List(glance.Definition(glance.Function)), -) -> Result(Bool, Nil) { - functions - |> list.find(fn(x) { - case x { - glance.Definition( - definition: glance.Function( - name: "main", - publicity: glance.Public, - parameters: [], - .., - ), - .., - ) -> True - _ -> False - } - }) - |> result.replace(True) +pub fn compile_project( + gleam_project: project.GleamProject, +) -> project.CompiledProject { + // TODO: Compile dependencies + project.CompiledProject( + base_directory: gleam_project.base_directory, + build_directory: project.build_directory(gleam_project.base_directory), + main_package: compile_package(gleam_project.main_package), + main_module: gleam_project.main_module, + ) } diff --git a/src/compiler/package.gleam b/src/compiler/package.gleam index bdef1cf..20678fe 100644 --- a/src/compiler/package.gleam +++ b/src/compiler/package.gleam @@ -10,14 +10,15 @@ import filepath import glance import gleam/dict import gleam/list -import gleam/option import gleam/result import gleam/set import gleam/string import simplifile +import tom pub type GleamPackage { GleamPackage( + config: Config, base_directory: String, main_module: String, modules: dict.Dict(String, glance.Module), @@ -27,33 +28,55 @@ pub type GleamPackage { pub type CompiledPackage { CompiledPackage( + config: Config, base_directory: String, - main_module: option.Option(String), modules: dict.Dict(String, String), external_import_files: set.Set(String), ) } +pub type Config { + Config(name: String) +} + /// Load the entry_point file and recursively load and parse any modules it ///returns. pub fn load_package( - source_directory: String, + base_directory: String, ) -> Result(GleamPackage, errors.Error) { - source_directory - |> simplifile.is_directory - |> result.map_error(errors.FileOrDirectoryNotFound(source_directory, _)) - |> result.try(fn(_) { find_entrypoint(source_directory) }) - |> result.try(fn(entrypoint) { - load_module( - GleamPackage( - source_directory, - entrypoint, - dict.new(), - external_import_files: set.new(), - ), + use _ <- result.try( + base_directory + |> simplifile.is_directory + |> result.map_error(errors.FileOrDirectoryNotFound(base_directory, _)), + ) + use config <- result.try(load_config(base_directory)) + use entrypoint <- result.try(find_entrypoint(base_directory)) + load_module( + GleamPackage( + config, + base_directory, entrypoint, - ) - }) + dict.new(), + external_import_files: set.new(), + ), + entrypoint, + ) +} + +pub fn load_config(base_directory: String) -> Result(Config, errors.Error) { + let config_path = filepath.join(base_directory, "gleam.toml") + use config_contents <- result.try( + simplifile.read(config_path) + |> result.map_error(errors.FileReadError(config_path, _)), + ) + use parsed <- result.try( + tom.parse(config_contents) + |> result.map_error(errors.TomlError(config_path, _)), + ) + use name <- result.try( + tom.get_string(parsed, ["name"]) |> result.map_error(errors.InvalidConfig), + ) + Ok(Config(name)) } pub fn find_entrypoint(source_directory: String) -> Result(String, errors.Error) { @@ -68,8 +91,31 @@ pub fn source_directory(base_directory: String) -> String { filepath.join(base_directory, "src") } -pub fn build_directory(base_directory: String) -> String { - filepath.join(base_directory, "build") +pub fn has_main_function(package: GleamPackage) -> Result(String, Nil) { + dict.get(package.modules, package.main_module) + |> result.try(fn(mod) { mod.functions |> function_list_has_main_function }) + |> result.replace(package.main_module |> string.drop_right(6)) +} + +fn function_list_has_main_function( + functions: List(glance.Definition(glance.Function)), +) -> Result(Bool, Nil) { + functions + |> list.find(fn(x) { + case x { + glance.Definition( + definition: glance.Function( + name: "main", + publicity: glance.Public, + parameters: [], + .., + ), + .., + ) -> True + _ -> False + } + }) + |> result.replace(True) } /// Parse the module and add it to the package's modules, if it can be parsed. diff --git a/src/compiler/project.gleam b/src/compiler/project.gleam new file mode 100644 index 0000000..ed859a3 --- /dev/null +++ b/src/compiler/project.gleam @@ -0,0 +1,52 @@ +//// A project encompasses everything needed to build a package +//// and all its dependencies (which are other packages). +//// +//// For the most part, a project is just a wrapper of a package +//// and a build directory for that package. +//// +//// Note that any one project can have multiple packages in it, +//// with one main package and any number of dependency packages. +//// Each of those dependency packages is probably the main package +//// of its own project from the point of view of the person developing +//// that project, but the current developer only cares about the current +//// package. +//// +//// I really hope that makes sense cause it took we a while to puzzle +//// it out. + +import compiler/package +import errors +import filepath +import gleam/option +import gleam/result + +pub type GleamProject { + GleamProject( + base_directory: String, + main_package: package.GleamPackage, + main_module: option.Option(String), + ) +} + +pub type CompiledProject { + CompiledProject( + base_directory: String, + build_directory: String, + main_package: package.CompiledPackage, + main_module: option.Option(String), + ) +} + +pub fn load_project( + base_directory: String, +) -> Result(GleamProject, errors.Error) { + use main_package <- result.try(package.load_package(base_directory)) + let main_module = + main_package |> package.has_main_function |> option.from_result + // TODO: dependencies need to come from gleam.toml + Ok(GleamProject(base_directory, main_package, main_module)) +} + +pub fn build_directory(base_directory: String) -> String { + filepath.join(base_directory, "build") +} diff --git a/src/errors.gleam b/src/errors.gleam index e09cc9c..46090b3 100644 --- a/src/errors.gleam +++ b/src/errors.gleam @@ -1,8 +1,11 @@ import glance import internal/errors as internal import simplifile +import tom pub type Error { + TomlError(path: String, error: tom.ParseError) + InvalidConfig(error: tom.GetError) FileOrDirectoryNotFound(path: String, error: simplifile.FileError) FileReadError(path: String, error: simplifile.FileError) FileWriteError(path: String, error: simplifile.FileError) @@ -14,6 +17,8 @@ pub type Error { pub fn format_error(error: Error) -> String { case error { + TomlError(path, _) -> "Error reading Toml file " <> path + InvalidConfig(_error) -> "Invalid config file (missing name?)" FileOrDirectoryNotFound(filename, _) -> "File or directory not found " <> filename FileReadError(filename, simplifile.Enoent) -> "File not found " <> filename diff --git a/src/macabre.gleam b/src/macabre.gleam index f2387d4..6c0164b 100644 --- a/src/macabre.gleam +++ b/src/macabre.gleam @@ -1,10 +1,12 @@ import argv import compiler import compiler/package +import compiler/project import errors import filepath import gleam/dict import gleam/io +import gleam/option import gleam/result import gleam/set import output @@ -14,9 +16,9 @@ pub fn main() { [] -> usage("Not enough arguments") [directory] -> directory - |> package.load_package - |> result.map(compiler.compile_package) - |> result.try(write_package) + |> project.load_project + |> result.map(compiler.compile_project) + |> result.try(write_project) |> result.map_error(output.write_error) |> result.unwrap_both // both nil @@ -28,32 +30,43 @@ pub fn usage(message: String) -> Nil { io.println("Usage: macabre \n\n" <> message) } -pub fn write_package( - package: package.CompiledPackage, +pub fn write_project( + project: project.CompiledProject, ) -> Result(Nil, errors.Error) { - let build_directory = package.build_directory(package.base_directory) - let source_directory = package.source_directory(package.base_directory) - output.delete(build_directory) - |> result.try(fn(_) { output.create_directory(build_directory) }) - |> result.try(fn(_) { output.write_prelude_file(build_directory) }) + use _ <- result.try(output.delete(project.build_directory)) + use _ <- result.try(output.create_directory(project.build_directory)) + use _ <- result.try(output.write_prelude_file(project.build_directory)) + // TODO: write dependencies + write_package( + project.main_package, + filepath.join(project.build_directory, project.main_package.config.name), + ) |> result.try(fn(_) { - output.write_py_main(build_directory, package.main_module) - }) - |> result.try(fn(_) { - output.copy_externals( - build_directory, - source_directory, - package.external_import_files |> set.to_list, + output.write_py_main( + project.build_directory, + option.map(project.main_module, fn(name) { + project.main_package.config.name <> "." <> name + }), ) }) - |> result.try(fn(_) { - dict.fold(package.modules, Ok(Nil), fn(state, name, module) { - result.try(state, fn(_) { - build_directory - |> filepath.join(name) - |> output.replace_extension() - |> output.write(module, _) - }) - }) - }) +} + +pub fn write_package( + package: package.CompiledPackage, + build_directory: String, +) -> Result(Nil, errors.Error) { + let source_directory = package.source_directory(package.base_directory) + use _ <- result.try(output.copy_externals( + build_directory, + source_directory, + package.external_import_files |> set.to_list, + )) + use state, name, module <- dict.fold(package.modules, Ok(Nil)) + { + use _ <- result.try(state) + build_directory + |> filepath.join(name) + |> output.replace_extension() + |> output.write(module, _) + } } diff --git a/test/compiler/directory_structure_tests.gleam b/test/compiler/directory_structure_tests.gleam index de0ca6f..2c46e92 100644 --- a/test/compiler/directory_structure_tests.gleam +++ b/test/compiler/directory_structure_tests.gleam @@ -1,5 +1,5 @@ import compiler -import compiler/package +import compiler/project import filepath import gleam/dict import gleam/list @@ -8,6 +8,7 @@ import gleam/set import gleam/string import gleeunit/should import macabre +import pprint import simplifile import temporary @@ -27,6 +28,7 @@ pub type ProjectFiles { src_dir: String, build_dir: String, main_path: String, + toml_path: String, ) } @@ -38,6 +40,7 @@ fn init_folders( let src = filepath.join(dir, "src") let build = filepath.join(dir, "build") let main_path = filepath.join(src, project_name <> ".gleam") + let toml_path = filepath.join(dir, "gleam.toml") simplifile.create_directory_all(src) |> should.be_ok @@ -47,16 +50,19 @@ fn init_folders( src_dir: src, build_dir: build, main_path: main_path, + toml_path: toml_path, ) use_function(project_files) } -pub fn package_compile_test_with_nested_folders_test() { +pub fn project_compile_test_with_nested_folders_test() { // src/ // src/baz.py // src/foo/bar.gleam // src/foo/bindings.py use project_files <- init_folders() + simplifile.write(project_files.toml_path, "name = \"example\"") + |> should.be_ok simplifile.write( to: project_files.main_path, contents: "import foo/bar @@ -94,52 +100,79 @@ pub fn package_compile_test_with_nested_folders_test() { ) |> should.be_ok - let gleam_package = - package.load_package(project_files.base_dir) + // --- load + let gleam_project = + project.load_project(project_files.base_dir) |> should.be_ok - // load - - should.equal(gleam_package.base_directory, project_files.base_dir) + should.equal(gleam_project.base_directory, project_files.base_dir) should.equal( - gleam_package.main_module, + gleam_project.main_module, + option.Some( + filepath.base_name(project_files.main_path) |> string.drop_right(6), + ), + ) + should.equal( + gleam_project.main_package.base_directory, + project_files.base_dir, + ) + should.equal( + gleam_project.main_package.main_module, filepath.base_name(project_files.main_path), ) - gleam_package.modules + gleam_project.main_package.modules |> dict.size |> should.equal(2) - gleam_package.external_import_files |> set.size |> should.equal(2) + gleam_project.main_package.external_import_files + |> set.size + |> should.equal(2) // --- compile - let compiled_package = compiler.compile_package(gleam_package) - should.equal(compiled_package.base_directory, project_files.base_dir) + let compiled_project = compiler.compile_project(gleam_project) + should.equal(compiled_project.base_directory, project_files.base_dir) should.equal( - compiled_package.main_module, + compiled_project.main_module, option.Some( filepath.base_name(project_files.main_path) |> string.drop_right(6), ), ) - compiled_package.modules + should.equal( + compiled_project.main_package.base_directory, + project_files.base_dir, + ) + compiled_project.main_package.modules |> dict.size |> should.equal(2) - compiled_package.external_import_files |> set.size |> should.equal(2) + compiled_project.main_package.external_import_files + |> set.size + |> should.equal(2) // --- write output - macabre.write_package(compiled_package) |> should.be_ok + macabre.write_project(compiled_project) |> should.be_ok + + simplifile.read_directory(project_files.base_dir) + |> should.be_ok + |> list.sort(string.compare) + |> should.equal(["build", "gleam.toml", "src"]) simplifile.read_directory(project_files.build_dir) |> should.be_ok |> list.sort(string.compare) + |> should.equal(["__main__.py", "example", "gleam_builtins.py"]) + + project_files.build_dir + |> filepath.join("example") + |> simplifile.read_directory + |> should.be_ok + |> list.sort(string.compare) |> should.equal([ filepath.base_name(project_files.main_path) |> string.drop_right(6) <> ".py", - "__main__.py", "baz.py", "foo", - "gleam_builtins.py", ]) project_files.build_dir - |> filepath.join("foo") + |> filepath.join("example/foo") |> simplifile.read_directory |> should.be_ok |> should.equal(["bindings.py", "bar.py"])