This blog post is part of an ongoing series about writing LiveCode Builder applications without the LiveCode engine.
Multi-module programs
When writing a large program, it's often useful to break it down into more than one module. For example, you might want to make a module that's dedicated to loading and saving the program's data, which has quite a lot of internal complexity but exposes a very simple API with Load() and Save() handlers. This is handy for making sure that it's easy to find the source file where each piece of functionality is located.
However, it can become tricky to compile the program. Each module may depend on any number of other modules, and you have to compile them in the correct order or the compilation result may be incorrect. Also, if one module changes, you have to recompile all of the modules that depend on it. If you tried to do this all by hand, it would be nigh-on impossible to correctly compile your program once you got above about 10 source files.
Fortunately, there are two really useful tools that can make it all rather easy. GNU Make (the make command) can perform all the required build steps in the correct order (and even in parallel!). And to help you avoid writing Makefiles by hand, lc-compile has a useful --deps mode.
Most of the remainder of this blog post will assume some familiarity with make and common Unix command-line tools.
The --deps option for lc-compile
make lets you express dependencies between files. However, you already express the dependencies between LCB source files when you write a use declaration. For example:
use com.livecode.foreign
says that your module depends on the .lci (LiveCode Interface) file for the com.livecode.foreign module.
So, the LCB compiler (a) already knows all the dependencies between the source files of your project and (b) already knows how to find the files. To take advantage of this and to massively simplify the process of creating a Makefile for a LCB project, lc-compile provides a --deps mode. In --deps mode, lc-compile doesn't do any of the normal compilation steps; instead, it outputs a set of Make rules on standard output.
Consider the following trivial two-file program.
-- org.example.numargs.lcb module org.example.numargs public handler NumArgs() return the number of elements in the command arguments end handler end module
-- org.example.countargs.lcb module org.example.countargs use org.example.numargs public handler Main() quit with status NumArgs() end handler end module
To generate the dependency rules, you run lc-compile with almost a normal command line — but you specify --deps make instead of an --output argument, and you list all of your source files instead of just one of them. See also my previous blog post about compiling and running pure LCB programs. For the "countargs" example program you could run:
$TOOLCHAIN/lc-compile --modulepath . --modulepath $TOOLCHAIN/modules/lci --deps make org.example.numargs.lcb org.example.countargs.lcb
This would print the following rules:
org.example.countargs.lci: org.example.numargs.lci org.example.countargs.lcb org.example.numargs.lci: org.example.numargs.lcb
Integrating with make
You can integrate this info into a Makefile quite easily. There are two pieces that you need: 1) tell make to load the extra rules, and 2) tell make how to generate them. In particular, it's important to regenerate the rules whenever the Makefile itself is modified (e.g. to add an additional source file).
# List of source code files SOURCES = org.example.countargs.lcb org.example.numargs.lcb # Include all the generated dependency rules include deps.mk # Rules for regenerating dependency rules whenever # the source code changes deps.mk: $(SOURCES) Makefile $(TOOLCHAIN)/lc-compile --modulepath . --modulepath $(TOOLCHAIN)/modules/lci --deps make -- $(SOURCES) > $@
A complete Makefile
Putting this all together, I've created a complete Makefile for the example multi-file project. It has the usual make compile and make clean targets, and places all of the built artefacts in a subdirectory called _build.
################################################################ # Parameters # Tools etc. LC_SRC_DIR ?= ../livecode LC_BUILD_DIR ?= $(LC_SRC_DIR)/build-linux-x86_64/livecode/out/Debug LC_LCI_DIR = $(LC_BUILD_DIR)/modules/lci LC_COMPILE ?= $(LC_BUILD_DIR)/lc-compile LC_RUN ?= $(LC_BUILD_DIR)/lc-run BUILDDIR = _build LC_COMPILE_FLAGS += --modulepath $(BUILDDIR) --modulepath $(LC_LCI_DIR) # List of source code files. SOURCES = org.example.countargs.lcb org.example.numargs.lcb # List of compiled module filenames. MODULES = $(patsubst %.lcb,$(BUILDDIR)/%.lcm,$(SOURCES)) ################################################################ # Top-level targets all: compile compile: $(MODULES) clean: -rm -rf $(BUILDDIR) .DEFAULT: all .PHONY: all compile clean ################################################################ # Build dependencies rules include $(BUILDDIR)/deps.mk $(BUILDDIR): mkdir -p $(BUILDDIR) $(BUILDDIR)/deps.mk: $(SOURCES) Makefile | $(BUILDDIR) $(LC_COMPILE) $(LC_COMPILE_FLAGS) --deps make -- $(SOURCES) > $@ ################################################################ # Build rules $(BUILDDIR)/%.lcm $(BUILDDIR)/%.lci: %.lcb | $(BUILDDIR) $(LC_COMPILE) $(LC_COMPILE_FLAGS) --output $@ -- $<
You should be able to use this directly in your own projects. All you need to do is to modify the list of source files in the SOURCES variable!
Note that you need to name your source files exactly the same as the corresponding interface files in order for this Makefile to work correctly. I'll leave adapting to the case where the source file and interface file are named differently as an exercise to the reader…
I hope you find this useful as a basis for writing new LiveCode Builder projects! Let me know how you get on.
2 comments:
This may be a stupid question but if lc-compile can work out the dependency order then why doesn't it just order them on the fly or at least re-order if they are in the wrong order?
It's not a stupid question!
Going back to basics, lc-compile only compiles one .lcb source file at a time. The names of its output files (interface, .lci and bytecode, .lcm) are not determined by the filename of the input file. The interface filename is determined by the module declaration in the source file, and the bytecode filename is determined by the --output argument passed to lc-compile.
This is pretty much the same as most other compilers (such as gcc and clang). The advantage of compiling one source file at a time is that you can compile multiple files in parallel using multiple compiler instances. The advantage of separating source and output file names is that you don't have to name your source files in a particular way or adopt a very strict filesystem layout (Java is a right pain in that respect).
In my example Makefile, I required the source files to be named the same as their corresponding output files. Without that constraint, the Makefile becomes a lot hairier (yay, rewriting make rules with sed).
There are a couple of bugs in lc-compile --deps make but I feel like the design and implementation is fundamentally a good one, and pretty consistent with the way other compilers work.
Post a Comment