#!/usr/bin/boron -sp
/*
	M2 - A meta-make tool
	Copyright 2001-2015,2019,2020 Karl Robillard
	Version 2.0.2

	TODO:
		* Remove files outside the build directory from dist rule.
		* Add Qt moc files to sources but not to distribution rule.

	These words are defined in the template file:
		generate_makefile

		exe_target
		lib_target
		shlib_target

		makerule_cxx
		makerule_asm
		makerule_c

	NOTE: Symbolic links don't work!
*/


project_file: "project.b"
template_file: any [
	getenv "M2_TEMPLATE"
	"/usr/share/boron/m2/m2_template.b"
]
cli_options: ""
debug_include: false


m2: context
[
	;--------------------------------------
	; These are constant for each makefile.

	makefile: "Makefile"
	default_block: none

	; Returns string with name and version
	project_version: does [
		replace/all either version
			[rejoin [project '-' version]]
			[copy project]
				' ' '_'
	]

	ct: none
	targets: []
	sub-projects: []
	output_buf: make string! 2048
	new-block: does [make block! 0]

	; This contains file! block! pairs.  The block contains the files
	; included in the file.
	include_dict: []

	distribution_files: []


	;--------------------------------------
	; These can be changed for each target.

	configuration: context [
		release: debug:
		opengl: qt: qt-static: x11: false
		warn: true
	]

	target_env: context [
		name: none
		uc_name: none
		output_file: none
		link_cxx: false

		objdir: none
		output_dir: "" 
		obj_ext: ".o"
		cxx_ext: ".cpp"
		defines: []

		include_paths: []
		link_paths: []
		link_libs: []

		header_files: []
		source_files: []
		object_files: []
		custom_flags: []	; Pairs of file & flags.

		moc_files: []
		srcmoc_files: []

		menv_aflags: {}
		menv_cflags: {}
		menv_cxxflags: {}
		menv_lflags: {}

		cfg: none
	
		configure:
		macro_text:
		rule_text: none
		built_obj_rule: none	; Contains the output of make_obj_rule.

		custom_src: func [src] [
			either flags: select custom_flags src
				[rejoin [src ' ' flags]]
				[src]
		]

		rule_makeobj: func [cc flags obj src] [
			; Default to GNU
			rejoin ["^-$(" cc ") -c $(" uc_name flags ") -o " obj 
					" $(" uc_name "_INCPATH) " custom_src src]
		]
		makerule_cxx: func [obj src /extern link_cxx] [
			link_cxx: true
			rule_makeobj "CXX" "_CXXFLAGS" obj src
		]
		makerule_c:   func [obj src] [rule_makeobj "CC" "_CFLAGS" obj src]
		makerule_asm: func [obj src] [
			; Default to GNU
			rejoin ["^-$(AS) $(" uc_name "_AFLAGS) -o " obj 
					" $(" uc_name "_INCPATH) " custom_src src]
		]

		file_rules: [
			".cpp" makerule_cxx
			".cxx" makerule_cxx
			".cc"  makerule_cxx
			".c"   makerule_c	 ; Must follow c++ entries.
			".s"   makerule_asm
			".asm" makerule_asm
		]

		make_obj_rule: func [
			/* Builds rules which make object files.
			   Dependencies are automatically generated by recursively
			   searching in sources and included files.
			   Called from the template. */
			/local obj ext rfunc
		][
			str: make string! 512
			src: source_files
			foreach obj object_files [
				append str rejoin [conv_slash obj ": " conv_slash src/1]

				srcfile: to-file src/1

				either exists? srcfile [
					dependencies: select include_dict srcfile
				][
					print ["make_obj_rule: Warning -" srcfile "not found"]
					dependencies: []
				]

				either dependencies [
					either empty? dependencies [
						append str '^/'
					][
						append str " \^/"
						foreach dep dependencies [
							inc: select include_dict dep
							ifn inc [
							  append_include_dict dependencies include_paths
							  inc: select include_dict dep
							]
							common: intersect dependencies inc
							append dependencies difference inc common
						]
						append str expand_list_gnu sort dependencies
					]
				][
					append str '^/'
				]

				; Explicitly state rule
				foreach [ext rfunc] file_rules [
					;print [ext get rfunc]
					if find src/1 ext [
						append str join do rfunc obj src/1 "^/^/"
						break
					]
				]

				++ src

				append header_files dependencies
			]

			header_files: sort intersect header_files header_files
			str
		]

		moc_rule: func [/local file] [
			str: make string! 0
			src: srcmoc_files
			foreach file moc_files [
				append str rejoin [
					first src ": " file
					"^/^-$(MOC) " file " -o " first src "^/^/"
				]
				++ src
			]
			str
		]
	]

	;--------------------------------------

	built_lib_targets: []

	local_libs: func [
		; Returns string of libraries from libs which are built by this
		; project file.
		libs
		/local base path
	][
		str: make string! 0
		foreach [base path] built_lib_targets [
			if l: find libs base [
				append str rejoin [' ' path]
				;probe first l
			]
		]
		str
	]


	header_files_used: func [
		; Returns a block containing all header files used in the project.
		/local f b
	][
		blk: new-block
		foreach [f b] include_dict [
			append blk b
		]
		sort intersect blk blk
	]


	append_include_dict: func [
		; Returns a block all files included from files
		files paths /local f
	][
		headers: new-block

		; Should make include_dict a hash?
		foreach f files [
			ifn select include_dict f [
				blk: included_files f paths
				append headers blk

				append include_dict f
				append/block include_dict blk
			]
		]
		sort intersect headers headers
	]

	add_target: func [target blk /extern ct] [
		ct: target
		append targets ct

		do default_block
		do blk

		either ct/objdir [
			ifn exists? ct/objdir [
				make-dir ct/objdir
			]
		][
			ct/objdir: ""
		]

		ct/configure
		ct/object_files: make_file_list ct/source_files ct/objdir ct/obj_ext

		ct/header_files: append_include_dict ct/source_files ct/include_paths
		;probe include_dict
		;probe ct/header_files

		if ct/cfg/qt [
			add_moc_files ct ct/header_files

			; Uncomment this to search cpp files for Q_OBJECT.
			;add_moc_files source_files

			append ct/object_files
				make_file_list ct/srcmoc_files ct/objdir ct/obj_ext

			append ct/source_files ct/srcmoc_files
		]
	]

	set 'build_makefile does [
		clear output_buf
		write makefile generate_makefile
	]

	exec-rule: func [def str] [
		cmd: new-block
		parse str [some[
			any '^/' targ: to ':' :targ to '^-'
			some [
				'^-' s: to '^/' :s (
					if ne? '\' pick s -3 [append cmd mark-sol s]
				) skip
			]
		]]
		foreach c1 cmd [
			while [find c1 '$'] [
			    c2: clear ""
				parse s: c1 [some[
					'$' '(' w: some define-symbol :w ')' s:
								 (append c2 get in def to-word w)
				  | '$' '@' s:   (append c2 targ)
				  | s: to '$' w: (append c2 slice s w)
				]]
				c1: copy append c2 s
			]
			print c1
			ifn zero? rc: execute c1 [
				print "Build Failure!"
				quit/return rc
			]
		]
	]

	def-to-block: func [str] [
		blk: new-block
		if str [
			parse construct str [
				" \^/^-" ' '
				"^/^/"   '^/'
			][some[
				w: some define-symbol :w thru '='
				some space s: to '^/' :s skip
					(append append blk to-set-word w s)
			]]
		]
		blk
	]

	set 'build_targets does [
		emit-makefile: func [description tool_def] [
			tools: context def-to-block tool_def
			foreach t targets [
				clear output_buf
				d: make tools def-to-block t/macro_text

				if t/cfg/qt [exec-rule d t/moc_rule]
				exec-rule d t/make_obj_rule

				clear output_buf
				rtext: t/rule_text
				exec-rule d either block? rtext [rejoin rtext] rtext
			]
		]
		generate_makefile
	]

	;------------------------------------------------------------------
	; Used in template file.

	eol: '^/'
	emit: func [data] [
		append output_buf either block? data [rejoin data] data
	]

	dist_files: func [] [
		str: make string! 64

		obj: new-block
		foreach t targets [
			if dir: t/objdir [append obj dir]
		]
		obj: intersect obj obj   ; Removes duplicates.
		append str obj

		foreach t targets [append str t/dist]
		str
	]

	; Return dependency string with sub-project libraries.
	sub-project-libs: func [libs /local l p] [
		out: copy ""
		foreach l libs [
			foreach p sub-projects [
				if all [
					or eq? p/type 'lib
					   eq? p/type 'shlib
					eq? p/base-name l
				][
					append out rejoin [' ' p/path '/' p/target]
				]
			]
		]
		out
	]

	emit-sub-project-targets: func [/local sub] [
		sep: pick ['\' '/'] windows?
		foreach sub sub-projects [
			emit [' ' sub/path sep sub/target]
		]
	]

	emit-sub-project-clean: func [/local sub] [
		cmd: pick [
			["^-cd " sub/path " & nmake clean^/"]
			["^-$(MAKE) -C " sub/path " clean^/"]
		] windows?
		foreach sub sub-projects [emit cmd]
	]

	emit-sub-projects: func [/local sub] [
		foreach sub sub-projects [
			either windows?
				[sep: '\' qt: ""  m2-make: "& boron -s c:\bin\m2& nmake"]
				[sep: '/' qt: '"' m2-make: "; m2; $(MAKE)"]

			emit [eol sub/path sep sub/target ':' eol]
			if sub/config [
				emit [
					{^-echo } qt sub/config qt { >} sub/path sep
					{project.config^/}
				]
			]
			emit ["^-cd " sub/path m2-make eol]
		]
	]

	emit-makefile: func [description tool_def /local t] [
		foreach t targets [
			; make_obj_rule must be called before we write DIST_FILES since
			; it finds the header dependencies.
			t/built_obj_rule: t/make_obj_rule
		]

		emit [
{#----------------------------------------------------------------------------
# } description {
# Generated by m2 at } now/date {
# Project: } m2/project {
#----------------------------------------------------------------------------
^/^/#------ Compiler and tools^/
}
		]
		emit tool_def

		emit "^/^/#------ Target settings"
		foreach t targets [emit "^/^/" t/macro_text]

		emit [{^/ARCHIVE = } project_version {^/DIST_FILES = \^/}]
		ifn empty? distribution_files [
			emit expand_list_gnu/part distribution_files
		]
		emit expand_list_gnu header_files_used

		emit "^/^/#------ Build rules^/^/all:"
		emit-sub-project-targets
		foreach t targets [emit [' ' t/output_file]]
		emit eol

		emit-sub-projects
		foreach t targets [emit '^/' emit t/rule_text]

		emit [
			"^/^/" makefile ": " project_file {
^-m2 } project_file {

.PHONY: dist
dist:
^-$(TAR) $(ARCHIVE).tar --exclude CVS --exclude .svn --exclude *.o }
	project_file ' ' dist_files { $(DIST_FILES)
^-mkdir /tmp/$(ARCHIVE)
^-tar -C /tmp/$(ARCHIVE) -xf $(ARCHIVE).tar
^-tar -C /tmp -cf $(ARCHIVE).tar $(ARCHIVE)
^-rm -rf /tmp/$(ARCHIVE)
^-$(ZIP) $(ARCHIVE).tar

.PHONY: clean
clean:
}
		]

		emit-sub-project-clean
		foreach t targets [emit t/clean]

		emit "^/^/#------ Compile rules^/^/"
		foreach t targets [
			emit t/built_obj_rule
			if t/cfg/qt [emit t/moc_rule]
		]

		emit "^/#EOF^/"
	]

	;------------------------------------------------------------------
	; Used in project file.

	project: "project"
	version: none

	do-any: func [file] [
		if exists? file [do file]
	]

	options: func [spec] [
		do spec
		do-any %project.config

		; Validate command line options.
		w1: words-of context spec
		w2: collect set-word! to-block cli_options
		ifn empty? w2: difference w2 w1 [
			error join "Invalid project options: " mold w2
		]
		do cli_options
	]

	bsd:
	linux:
	macx:
	sun:
	unix:
	win32: none		; func [blk block!] []  ; skip block by default

	windows?: false

	default:	func [blk /extern default_block] [default_block: blk]
	objdir:		func [dir] [ct/objdir: dir]
	into:		func [dir] [ct/output_dir: term-dir dir]

	aflags:		func [str string!] [add_flags ct/menv_aflags   str]
	cflags:		func [str string!] [add_flags ct/menv_cflags   str add_def str]
	cxxflags:	func [str string!] [add_flags ct/menv_cxxflags str add_def str]
	lflags:		func [str string!] [add_flags ct/menv_lflags   str]

	debug:		does [ct/cfg/debug:   true] ; Building debug version.
	release:	does [ct/cfg/release: true] ; Optimize for release build.
	warn:		does [ct/cfg/warn:    true] ; Enable compiler warnings.
	no-warn:	does [ct/cfg/warn:   false] ; Disable compiler warnings.

	opengl:		does [ct/cfg/opengl: true]  ; Using OpenGL libraries.
	qt:			func [libs block!] [ct/cfg/qt: validate-qt libs]
	qt-static:	does [ct/cfg/qt-static: true]
	qt-version: 5
	x11:		does [ct/cfg/x11: true]     ; Using X11 libraries.

	include-define:	func [str string!] [append ct/defines parse-white str]

	include_from: func [list string!/file!/block!] [
		if string? list [
			list: parse-white list
		]
		; TODO: Run conv_slash on each item.
		append ct/include_paths reduce list
	]

	libs: func [list string!/file!/block!] [
		if string? list [
			list: parse-white list
		]
		append ct/link_libs list
	]

	libs_from: func [dir list] [
		append ct/link_paths dir
		libs list
	]

	lib_path: func [dir] [
		append ct/link_paths dir
	]

	dist: func [arg block!] [
		append distribution_files arg
	]

	sub-project: func [def block!] [
		parse def [some[
			tok:
	   ;	set lpath file!
			file! (lpath: first tok)
		  | word! file! (
				ltype: first tok
				lbase: second tok
				ltarget: switch ltype [
					exe [
						either windows?
							[join lbase %.exe]
							[lbase]
					]
					lib [
						either windows?
							[join lbase %.lib]
							[rejoin [%lib lbase %.a]]
					]
					shlib [
						either windows?
							[join lbase %.dll]
							[rejoin [%lib lbase %.so]]
					]
					[error "Invalid sub-project target. Expected exe/lib/shlib"]
				]
			)
		  | string! (lconfig: trim/lines first tok)
		]]
		append sub-projects context [
			path: lpath
			type: ltype
			base-name: lbase
			target: ltarget
			config: lconfig
		]
	]

	sources: func [arg block! /flags fstr] [
		append ct/source_files arg
		if flags [
			forall arg [
				append ct/custom_flags first arg
				append ct/custom_flags fstr
			]
		]
	]

	sources_from: func [path files block!] [
		; include_from path
		term-dir path
		forall files [
			append ct/source_files join path first files
		]
	]

	var-name: func [name] [
		; NMake does not allow '-' in variable names.
		replace/all uppercase copy name '-' '_'
	]

	; Builds executable rule.
	exe: func [basename blk /extern uc_name cfg] [
		add_target (make exe_target [
				uc_name: var-name name: basename
				cfg: copy configuration
			]) blk
	]

	; Builds static library rule.
	lib: func [basename blk /extern uc_name cfg] [
		add_target (make lib_target [
				uc_name: join "LIB_" var-name name: basename
				cfg: copy configuration
			]) blk
		append built_lib_targets reduce [basename ct/output_file]
	]

	; Builds shared library rule.
	shlib: func [basename blk /extern uc_name cfg version] [
		if block? basename [
			ver: second basename
			basename: first basename
		]
		add_target (make shlib_target [
				uc_name: join "SLIB_" var-name name: basename
				cfg: copy configuration
				version: ver
			]) blk
		append built_lib_targets reduce [basename ct/output_file]
	]

	; Builds generic make rule.
	rule: func [output dep commands
		/extern uc_name cfg clean dist rule_text
	][
		append targets make target_env [
			uc_name: join "FILE_" var-name name: output_file: output
			cfg: copy configuration

			built_obj_rule: make_obj_rule: ""
			clean: rejoin [
				either windows? ["^--@del "] ["^--rm -f "] output_file eol
			]
			dist: either dep [join ' ' to-text dep] ""
			rule_text: rejoin [
				output_file ':' dist "^/^-"
				; Trim twice to remove any newline from last command.
				replace/all trim trim/indent commands '^/' "^/^-" eol
			]
		]
	]

	gnu_string: func [
		; Returns string of items each with a prefix.
		prefix string!
		items  block!
	][
		str: make string! 64
		forall items [
			append str either (find items/1 ' ') [
				rejoin [' ' prefix '"' items/1 '"']
			][
				rejoin [' ' prefix items/1]
			]
		]
		remove str
	]


	;------------------------------------------------------------------
	; Private Helpers


rbuf: make string! 8192
read-buf: func [file] [
	read/into file rbuf
	rbuf	; Read can return none
]

white: charset " ^-^/"
non-white: complement copy white
parse-white: func [str string!] [
	blk: make block! 8
	parse str [any[
		any white tok: some non-white :tok (append blk tok)
	]]
	if empty? blk [append blk str]
	blk
]

add_flags: func [flags str] [
	append flags
		either empty? flags
			[trim/lines str]
			[join ' ' trim/lines str]
]

strip_ext: func [file] [		; Modifies file
	clear find/last file '.'
	file
]


make_file_list: func [
	; Copy a block of files with changed path and extension
	src  block!
	dir
	extension
	| obj file
][
	obj: copy/deep src
	either empty? dir [
		forall obj [
			strip_ext obj/1
			append obj/1 extension
		]
	][
		forall obj [
			file: second split-path obj/1
			obj/1: rejoin [term-dir dir strip_ext file extension]
		]
	]
	head obj
]


idprint: func [blk] pick [[print blk] []] debug_include

find_include_file: func [
	; Checks if a file exists in the current directory or any set of paths.
	file  file!/string!
	paths block!
][
	idprint [' ' file]
	if exists? file [return file]

	forall paths [
		path: to-file paths/1
		term-dir path
		full: join path file

		idprint ["   " full]
		if exists? full [return full]
	]
	none
]


space: charset " ^-"
define-symbol: charset "_0-9A-Za-z"

add_def: func [str] [
	parse str [some[
		thru "-D" tok: some define-symbol :tok (append ct/defines tok)
	]]
]

context [
	dtok:
	use-include: none
	inc-stack: []

	if-case: func [enable /extern use-include] [
		if last inc-stack [use-include: enable]
	]
	push-if: func [enable] [
		append inc-stack use-include
		if-case enable
	]

	def-exp: [
		some space opt "defined(" dtok: some define-symbol :dtok thru '^/'
	]

	set 'get-includes func [code defs inc-blk /extern dtok use-include] [
		use-include: true
		clear inc-stack
		defs: copy defs
		parse code [any[
			any space '#' [
				"include" some space '"' dtok: to '"' :dtok (
					if use-include [append inc-blk copy dtok]
				)
			  | "define"  def-exp   (if use-include [append defs dtok])
			  | "ifdef"   def-exp   (push-if find defs dtok)
			  | "ifndef"  def-exp   (push-if not find defs dtok)
			  | "if"      def-exp   (push-if either eq? dtok "1"
										[true] [find defs dtok])
			  | "else"    thru '^/' (if-case not use-include)
			  | "elif"    def-exp   (if-case find defs dtok)
			  | "endif"   thru '^/' (use-include: pop inc-stack)
			]
		  | any space "/*" thru "*/"
		  | thru '^/'
		]]
	]
]

included_files: func [
   /* Returns a block of the files which are included in a C/C++ file.
	  Adds    #include "file.h"
	  Ignores #include <file.h>
   */
	file  file!/string! "File to check for #include statements"
	paths block!        "Paths to check for included files"
][
	out: new-block

	ifn exists? file [
		print ["included_files:" file "not found"]
		return out
	]

	get-includes read-buf file ct/defines out
	blk: intersect out out   ; Removes duplicates.

	if debug_include [print ["Finding include files for" file] probe blk]

	clear out
	forall blk [
		tmp: find_include_file first blk paths
		either tmp [
			append out tmp
		][
			print rejoin
				["included_files: " first blk " (from " file ") not found!"]
		]
	]
	out
]


; Nothing by default.
conv_slash: func [file] [file]


expand_list_gnu: func [
	; Returns a string with each file on a seperate line.
	files  block!
	/part    ; Do not erase trailing space and slash
	| str
][
	str: make string! 64
	forall files [
		append str rejoin ['^-' conv_slash first files { \^/}]
	]
	ifn part [
		remove/part find/last str ' ' 2
	]
	str
]


add_moc_files: func [ct files block! /local file]
[
	blk: new-block
	foreach file files [
		if find/case read-buf file "Q_OBJECT" [
			append ct/moc_files file

			set [dir file] split-path file

			; MOC bug - specifying "./" on output file creates bad include
			; statements.
			; if equal? dir "./" [dir: ""]
			ifn dir [dir: ""]

			append ct/srcmoc_files join dir
				["moc_" strip_ext copy file ct/cxx_ext]
		]
	]
	blk
]

validate-qt: func [blk] [
	if all [find blk 'widgets  not find blk 'gui] [
		append blk 'gui
	]
	if eq? qt-version 4 [
		if pos: find blk 'widgets [remove pos]
	]
	ifn find blk 'core [append blk 'core]
	blk
]
]


show-help: func [] [
	usage: {m2 version 2.0.2

M2 Options:
  -t <file>       Specify template file   (default is M2_TEMPLATE env.)
  -o <file>       Specify output makefile (default is Makefile)
  -b              Build all targets
  -h              Print this help and quit
  <project>       Specify project file    (default is project.b)
  <opt>:<value>   Set project option

Project Options:}

	either all [
		exists? project_file
		opt: select load project_file 'options
	][
		parse opt [some[
			tok:
			set-word! (
				append usage rejoin ["^/  " pw: to-text first tok ':']
			)
			| string! (
				append usage join skip "               " size? pw first tok
			)
			| skip
		]]
	][
		append usage "^/  none"
	]
	prin terminate usage '^/'
]


;------------------------------------------------------------------
; Read command line arguments and invoke build_makefile.


build-op: :build_makefile
if args [
	forall args [
		switch first args [
			"-t" [template_file: second ++ args]
			"-o" [m2/makefile:   second ++ args]
			"-h" [show-help quit]
			"-b" [build-op: :build_targets]
			[
				either find a1: first args ':' [
					append append cli_options a1 '^/'
				][
					project_file: a1
				]
			]
		]
	]
]

;print [project_file template_file m2/makefile]

ifn exists? template_file [
	print [
		"Template" template_file
		"not found.^/Use the -t option or edit template_file in the m2 script."
	]
	quit
]

either exists? project_file [
	do bind load template_file m2
	do bind load project_file m2
	build-op
][
	print [project_file "does not exist; Please specify project file."]
]

;probe m2/header_files_used
quit


;EOF
