#!/usr/bin/env python2 # # Copyright 2015 Anthony Scemama # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This file can be downloaded here: # https://raw.githubusercontent.com/scemama/ninja_ocaml/master/ninja_ocaml.py # """Build OCaml projects using ninja.""" __author__ = """Anthony Scemama """ import os import sys import subprocess def _help_ (): print """ 1) Download and install ninja : https://github.com/martine/ninja/releases/latest 2) Copy the script into your OCaml project. 3) Run the script. It will build a default build.ninja file 4) Edit the build.ninja file 5) Compile the main target using `ninja` 6) Compile all the targets using `ninja all` """ def create_generated_ninja(): """Creates the generated.ninja file""" # Header PACKAGES="" THREAD="" SYNTAX="" OCAMLC_FLAGS="" GENERATED_NINJA="generated.ninja" with open('build.ninja','r') as f: for line in f: if line.startswith("PACKAGES"): PACKAGES=line.split('=',1)[1].strip() elif line.startswith("THREAD"): THREAD=line.split('=',1)[1].strip() elif line.startswith("SYNTAX"): SYNTAX=line.split('=',1)[1].strip() elif line.startswith("OCAMLC_FLAGS"): OCAMLC_FLAGS=line.split('=',1)[1].strip() elif line.startswith("LINK_FLAGS"): LINK_FLAGS=line.split('=',1)[1].strip() elif line.startswith("GENERATED_NINJA"): GENERATED_NINJA=line.split('=',1)[1].strip() if PACKAGES != "": LINK_FLAGS = "-linkpkg "+PACKAGES header = [ """ ######################################################## # This file was auto-generated. # # This file will be overwritten. Don't edit this file! # # Changes should be done in the build.ninja file. # ######################################################## """, "PACKAGES=%s"%(PACKAGES), "THREAD=%s"%(THREAD), "SYNTAX=%s"%(SYNTAX), "OCAMLC_FLAGS=%s"%(OCAMLC_FLAGS), "LINK_FLAGS=%s"%(LINK_FLAGS), "GENERATED_NINJA=%s"%(GENERATED_NINJA), ] header += """ rule ocamlc command = ocamlfind ocamlc -c $OCAMLC_FLAGS $THREAD $PACKAGES $SYNTAX -o $out $in description = Compiling $out (bytecode) rule ocamlopt command = ocamlfind ocamlopt -c $OCAMLC_FLAGS $THREAD $PACKAGES $SYNTAX -o $o $in description = Compiling $out (native) rule ocamlc_link command = ocamlfind ocamlc $OCAMLC_FLAGS $THREAD $LINK_FLAGS $PACKAGES $SYNTAX -o $out $in description = Compiling $out (bytecode) rule ocamlopt_link command = ocamlfind ocamlopt $OCAMLC_FLAGS $THREAD $LINK_FLAGS $PACKAGES $SYNTAX -o $out $in description = Compiling $out (native) """.splitlines() # Get the list of .ml files all_files = os.listdir(os.getcwd()) files = [ os.path.splitext(i)[0] for i in all_files if i.endswith('.ml') ] while "myocamlbuild" in files: files.remove("myocamlbuild") ml_files = ' '.join( [ '%s.ml'%i for i in files ] ) # Dependencies result = subprocess.Popen( ("ocamlfind ocamldep {0} {1} {2}".format(PACKAGES,SYNTAX,ml_files)).split() ,stdout=subprocess.PIPE).communicate()[0] result = result.replace('\\\n',' ') dependencies = {} for line in result.splitlines(): key, value = line.split(':') dependencies[key.strip()] = value.strip() result = header template = """ build {0}.cmi: ocamlc {0}.mli | $GENERATED_NINJA build {0}.cmo: ocamlc {0}.ml | $GENERATED_NINJA {1} build {0}.cmx {0}.o: ocamlopt {0}.ml | $GENERATED_NINJA {2} o = {0}.o """ template_root_byte = """ build {2}.byte: ocamlc_link {1} {0} """ template_root_native = """ build {2}: ocamlopt_link {1} {0} """ # Find roots dep = {} for f in dependencies: dep[f] = [ i.strip() for i in dependencies[f].split() ] roots = {} for f in dependencies: Found = False for g,l in dep.iteritems(): if f in l: Found = True if not Found: roots[f] = [] def get_deps(l): result = [] for i in l: if i in dep: result += get_deps(dep[i]) result += l newresult = [] for r in result: if r not in newresult: newresult.append(r) return newresult for r in roots: roots[r] = [ i for i in get_deps(dep[r]) if not i.endswith(".cmi") ] # Write the $GENERATED_NINJA file result += [ template.format(basename, dependencies["%s.cmo"%basename], dependencies["%s.cmx"%basename] ) for basename in files ] result += [ template_root_byte.format(basename, ' '.join(roots[basename]), os.path.splitext(basename)[0] ) for basename in roots if basename.endswith('.cmo')] result += [ template_root_native.format(basename, ' '.join(roots[basename]), os.path.splitext(basename)[0] ) for basename in roots if basename.endswith('.cmx')] output = '\n'.join(result) try: with open(GENERATED_NINJA,'r') as f: inp = f.read() except IOError: inp = "" if inp != output: with open(GENERATED_NINJA,'w') as f: f.write(output) def create_build_ninja (): with open('build.ninja','w') as f: f.write(""" MAIN= # Main program to build PACKAGES= # Required opam packages, for example: # PACKAGES=-package core THREAD= # If you need threding support, use: # THREAD=-thread SYNTAX= # If you need pre-processing, use: # SYNTAX=-syntax camlp4o OCAMLC_FLAGS= # Flags to give to ocamlc, for example: # OCAMLC_FLAGS=-g -warn-error A LINK_FLAGS= # Flags to give to the linker, for example: # LINK_FLAGS=-cclib '-Wl,-rpath=../lib,--enable-new-dtags' GENERATED_NINJA=generated.ninja # Name of the auto-generated ninja file rule create_generated command = python2 ./ninja_ocaml.py description = Finding dependencies between modules rule run_ninja command = ninja -f $in $target description = Compiling OCaml executables pool = console rule run_clean command = ninja -f $GENERATED_NINJA -t clean ; rm $GENERATED_NINJA pool = console description = Cleaning directory rule ocamlc command = ocamlfind ocamlc -c $OCAMLC_FLAGS $THREAD $PACKAGES $SYNTAX -o $out $in description = Compiling $in (bytecode) rule ocamlopt command = ocamlfind ocamlopt -c $OCAMLC_FLAGS $THREAD $PACKAGES $SYNTAX -o $out $in description = Compiling $in (native) rule ocamlc_link command = ocamlfind ocamlc $OCAMLC_FLAGS $THREAD $LINK_FLAGS $PACKAGES $SYNTAX -o $out $in description = Compiling $out (bytecode) rule ocamlopt_link command = ocamlfind ocamlopt $OCAMLC_FLAGS $THREAD $LINK_FLAGS $PACKAGES $SYNTAX -o $out $in description = Compiling $out (native) build clean: run_clean build always $GENERATED_NINJA: create_generated build $MAIN: run_ninja $GENERATED_NINJA target = $MAIN build all: run_ninja $GENERATED_NINJA target = default $MAIN """) def main(): for h in "help -h -help --help ?".split(): if h in sys.argv: _help_ () return if "build.ninja" in os.listdir(os.getcwd()): create_generated_ninja () else: create_build_ninja () print """ ========================================================== A default build.ninja file was created. Now, edit build.ninja and compile your project using: ninja ========================================================== """ if __name__ == '__main__': main()