Property definitions

nitc $ MakefileToolchain :: defaultinit
# Default toolchain using a Makefile
class MakefileToolchain
	super Toolchain

	redef fun write_and_make
	do
		var debug = toolcontext.opt_debug.value
		var compile_dir = compile_dir

		# Remove the compilation directory unless explicitly set
		var auto_remove = toolcontext.opt_compile_dir.value == null
		# If debug flag is set, do not remove sources
		if debug then auto_remove = false

		# Generate the .h and .c files
		# A single C file regroups many compiled rumtime functions
		# Note that we do not try to be clever an a small change in a Nit source file may change the content of all the generated .c files
		var time0 = get_time
		self.toolcontext.info("*** WRITING C ***", 1)

		root_compile_dir.mkdir
		compile_dir.mkdir

		var cfiles = new Array[String]
		write_files(compile_dir, cfiles)

		# Generate the Makefile

		write_makefile(compile_dir, cfiles)

		var time1 = get_time
		self.toolcontext.info("*** END WRITING C: {time1-time0} ***", 2)

		toolcontext.check_errors

		# Execute the Makefile

		if self.toolcontext.opt_no_cc.value then return

		time0 = time1
		self.toolcontext.info("*** COMPILING C ***", 1)

		compile_c_code(compile_dir)

		if auto_remove then
			sys.system("rm -r -- '{root_compile_dir.escape_to_sh}/'")
		end

		if toolcontext.opt_run.value then
			var mainmodule = compiler.mainmodule
			var out = outfile(mainmodule)
			var cmd = ["."/out]
			cmd.append toolcontext.option_context.rest
			toolcontext.exec_and_check(cmd, "--run")
		end

		time1 = get_time
		self.toolcontext.info("*** END COMPILING C: {time1-time0} ***", 2)
	end

	# Write all source files to the `compile_dir`
	fun write_files(compile_dir: String, cfiles: Array[String])
	do
		var platform = compiler.target_platform
		if platform.supports_libunwind then compiler.build_c_to_nit_bindings
		var cc_opt_with_libgc = "-DWITH_LIBGC"
		if not platform.supports_libgc then cc_opt_with_libgc = ""

		# Add gc_choser.h to aditionnal bodies
		var gc_chooser = new ExternCFile("gc_chooser.c", cc_opt_with_libgc)
		if cc_opt_with_libgc != "" then gc_chooser.pkgconfigs.add "bdw-gc"
		compiler.extern_bodies.add(gc_chooser)
		var clib = toolcontext.nit_dir / "clib"
		compiler.files_to_copy.add "{clib}/gc_chooser.c"
		compiler.files_to_copy.add "{clib}/gc_chooser.h"

		# Add lttng traces provider to external bodies
		if toolcontext.opt_trace.value then
			#-I. is there in order to make the TRACEPOINT_INCLUDE directive in clib/traces.h refer to the directory in which gcc was invoked.
			var traces = new ExternCFile("traces.c", "-I.")
			traces.pkgconfigs.add "lttng-ust"
			compiler.extern_bodies.add(traces)
			compiler.files_to_copy.add "{clib}/traces.c"
			compiler.files_to_copy.add "{clib}/traces.h"
		end

		# FFI
		for m in compiler.mainmodule.in_importation.greaters do
			compiler.finalize_ffi_for_module(m)
		end

		# Copy original .[ch] files to compile_dir
		for src in compiler.files_to_copy do
			var basename = src.basename
			var dst = "{compile_dir}/{basename}"
			src.file_copy_to dst
		end

		var hfilename = compiler.header.file.name + ".h"
		var hfilepath = "{compile_dir}/{hfilename}"
		var h = new FileWriter.open(hfilepath)
		for l in compiler.header.decl_lines do
			h.write l
			h.write "\n"
		end
		for l in compiler.header.lines do
			h.write l
			h.write "\n"
		end
		h.close

		var max_c_lines = toolcontext.opt_max_c_lines.value
		for f in compiler.files do
			var i = 0
			var count = 0
			var file: nullable FileWriter = null
			for vis in f.writers do
				if vis == compiler.header then continue
				var total_lines = vis.lines.length + vis.decl_lines.length
				if total_lines == 0 then continue
				count += total_lines
				if file == null or (count > max_c_lines and max_c_lines > 0) then
					i += 1
					if file != null then file.close
					var cfilename = "{f.name}.{i}.c"
					var cfilepath = "{compile_dir}/{cfilename}"
					self.toolcontext.info("new C source files to compile: {cfilepath}", 3)
					cfiles.add(cfilename)
					file = new FileWriter.open(cfilepath)
					file.write "#include \"{f.name}.0.h\"\n"
					count = total_lines
				end
				for l in vis.decl_lines do
					file.write l
					file.write "\n"
				end
				for l in vis.lines do
					file.write l
					file.write "\n"
				end
			end
			if file == null then continue
			file.close

			var cfilename = "{f.name}.0.h"
			var cfilepath = "{compile_dir}/{cfilename}"
			var hfile: nullable FileWriter = null
			hfile = new FileWriter.open(cfilepath)
			hfile.write "#include \"{hfilename}\"\n"
			for key in f.required_declarations do
				if not compiler.provided_declarations.has_key(key) then
					var node = compiler.requirers_of_declarations.get_or_null(key)
					if node != null then
						node.debug "No provided declaration for {key}"
					else
						print "No provided declaration for {key}"
					end
					abort
				end
				hfile.write compiler.provided_declarations[key]
				hfile.write "\n"
			end
			hfile.close
		end

		self.toolcontext.info("Total C source files to compile: {cfiles.length}", 2)
	end

	# Get the name of the Makefile to use
	fun makefile_name: String do return "{compiler.mainmodule.c_name}.mk"

	# Get the default name of the executable to produce
	fun default_outname: String
	do
		var mainmodule = compiler.mainmodule.first_real_mmodule
		return mainmodule.name
	end

	# Combine options and platform informations to get the final path of the outfile
	fun outfile(mainmodule: MModule): String
	do
		var res = self.toolcontext.opt_output.value
		if res != null then return res
		res = default_outname
		var dir = self.toolcontext.opt_dir.value
		if dir != null then return dir.join_path(res)
		return res
	end

	# Write the Makefile
	fun write_makefile(compile_dir: String, cfiles: Array[String])
	do
		var mainmodule = compiler.mainmodule
		var platform = compiler.target_platform

		var outname = outfile(mainmodule)

		var real_outpath = compile_dir.relpath(outname)
		var outpath = real_outpath.escape_to_mk
		if outpath != real_outpath then
			# If the name is crazy and need escaping, we will do an indirection
			# 1. generate the binary in the nit_compile dir under an escaped name
			# 2. copy the binary at the right place in the `all` goal.
			outpath = mainmodule.c_name
		end
		var makename = makefile_name
		var makepath = "{compile_dir}/{makename}"
		var makefile = new FileWriter.open(makepath)

		var linker_options = new HashSet[String]
		for m in mainmodule.in_importation.greaters do
			var libs = m.collect_linker_libs
			if libs != null then linker_options.add_all(libs)
		end
		var debug = toolcontext.opt_debug.value

		makefile.write """
ifeq ($(origin CC), default)
        CC = ccache cc
endif
ifeq ($(origin CXX), default)
        CXX = ccache c++
endif
CFLAGS ?= -g {{{if not debug then "-O2" else ""}}} -Wno-unused-value -Wno-switch -Wno-attributes -Wno-trigraphs
CINCL =
LDFLAGS ?=
LDLIBS  ?= -lm {{{linker_options.join(" ")}}}
\n"""

		if self.toolcontext.opt_trace.value then makefile.write "LDLIBS += -llttng-ust -ldl\n"

		if toolcontext.opt_shared_lib.value then
			makefile.write """
CFLAGS += -fPIC
LDFLAGS += -shared -Wl,-soname,{{{outname}}}
"""
		end

		makefile.write "\n# SPECIAL CONFIGURATION FLAGS\n"
		if platform.supports_libunwind then
			if toolcontext.opt_no_stacktrace.value then
				makefile.write "NO_STACKTRACE ?= True"
			else
				makefile.write "NO_STACKTRACE ?= # Set to `True` to enable"
			end
		end

		# Dynamic adaptations
		# While `platform` enable complex toolchains, they are statically applied
		# For a dynamic adaptsation of the compilation, the generated Makefile should check and adapt things itself
		makefile.write "\n\n"

		# Check and adapt the targeted system
		makefile.write("uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')\n")

		# Check and adapt for the compiler used
		# clang need an additionnal `-Qunused-arguments`
		makefile.write("clang_check := $(shell sh -c '$(CC) -v 2>&1 | grep -q clang; echo $$?')\nifeq ($(clang_check), 0)\n\tCFLAGS += -Qunused-arguments\nendif\n")

		if platform.supports_libunwind then
			makefile.write """
ifneq ($(NO_STACKTRACE), True)
  # Check and include lib-unwind in a portable way
  ifneq ($(uname_S),Darwin)
    # already included on macosx, but need to get the correct flags in other supported platforms.
    ifeq ($(shell pkg-config --exists 'libunwind'; echo $$?), 0)
      LDLIBS += `pkg-config --libs libunwind`
      CFLAGS += `pkg-config --cflags libunwind`
    else
      $(warning "[_] stack-traces disabled. Please install libunwind-dev.")
      CFLAGS += -D NO_STACKTRACE
    endif
  endif
else
  # Stacktraces disabled
  CFLAGS += -D NO_STACKTRACE
endif

"""
		else
			makefile.write("CFLAGS += -D NO_STACKTRACE\n\n")
		end

		makefile.write """
# Special configuration for Darwin
ifeq ($(uname_S),Darwin)
	# Remove POSIX flag -lrt
	LDLIBS := $(filter-out -lrt,$(LDLIBS))
endif

# Special configuration for Windows under mingw64
ifneq ($(findstring MINGW64,$(uname_S)),)
	# Use the pcreposix regex library
	LDLIBS += -lpcreposix

	# Remove POSIX flag -lrt
	LDLIBS := $(filter-out -lrt,$(LDLIBS))

	# Silence warnings when storing Int, Char and Bool as pointer address
	CFLAGS += -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast
endif

# Add the compilation dir to the Java CLASSPATH
ifeq ($(CLASSPATH),)
	CLASSPATH := .
else
	CLASSPATH := $(CLASSPATH):.
endif

"""

		makefile.write("all: {outpath}\n")
		if outpath != real_outpath then
			makefile.write("\tcp -- {outpath.escape_to_sh} {real_outpath.escape_to_sh.replace("$","$$")}")
		end
		makefile.write("\n")

		var ofiles = new Array[String]
		var dep_rules = new Array[String]
		# Compile each generated file
		for f in cfiles do
			var o = f.strip_extension(".c") + ".o"
			makefile.write("{o}: {f}\n\t$(CC) $(CFLAGS) $(CINCL) -c -o {o} {f}\n\n")
			ofiles.add(o)
			dep_rules.add(o)
		end

		# Generate linker script, if any
		if not compiler.linker_script.is_empty then
			var linker_script_path = "{compile_dir}/linker_script"
			ofiles.add "linker_script"
			var f = new FileWriter.open(linker_script_path)
			for l in compiler.linker_script do
				f.write l
				f.write "\n"
			end
			f.close
		end

		# pkg-config annotation support
		var pkgconfigs = new Array[String]
		for f in compiler.extern_bodies do
			pkgconfigs.add_all f.pkgconfigs
		end

		# Only test if pkg-config is used
		if not pkgconfigs.is_empty then

			# Check availability of pkg-config, silence the proc output
			toolcontext.check_pkgconfig_packages pkgconfigs

			# Double the check in the Makefile in case it's distributed
			makefile.write """
# does pkg-config exists?
ifneq ($(shell which pkg-config >/dev/null; echo $$?), 0)
$(error "Command `pkg-config` not found. Please install it")
endif
"""
			for p in pkgconfigs do
				makefile.write """
# Check for library {{{p}}}
ifneq ($(shell pkg-config --exists '{{{p}}}'; echo $$?), 0)
$(error "pkg-config: package {{{p}}} is not found.")
endif
"""
			end
		end

		# Compile each required extern body into a specific .o
		var java_files = new Array[ExternFile]
		for f in compiler.extern_bodies do
			var o = f.makefile_rule_name
			makefile.write("{o}: {f.filename}\n")
			makefile.write("\t{f.makefile_rule_content}\n\n")
			dep_rules.add(f.makefile_rule_name)

			if f.compiles_to_o_file then ofiles.add(o)
			if f.add_to_jar then java_files.add(f)
		end

		if not java_files.is_empty then
			var jar_file = "{outpath}.jar"

			var class_files_array = new Array[String]
			for f in java_files do class_files_array.add(f.makefile_rule_name)
			var class_files = class_files_array.join(" ")

			makefile.write("{jar_file}: {class_files}\n")
			makefile.write("\tjar cf {jar_file} {class_files}\n\n")
			dep_rules.add jar_file
		end

		# Link edition
		var pkg = ""
		if not pkgconfigs.is_empty then
			pkg = "`pkg-config --libs {pkgconfigs.join(" ")}`"
		end
		makefile.write("{outpath}: {dep_rules.join(" ")}\n\t$(CC) $(LDFLAGS) -o {outpath.escape_to_sh} {ofiles.join(" ")} $(LDLIBS) {pkg}\n\n")
		# Clean
		makefile.write("clean:\n\trm -f {ofiles.join(" ")} 2>/dev/null\n")
		if outpath != real_outpath then
			makefile.write("\trm -f -- {outpath.escape_to_sh} 2>/dev/null\n")
		end
		makefile.close
		self.toolcontext.info("Generated makefile: {makepath}", 2)

		makepath.file_copy_to "{compile_dir}/Makefile"
	end

	# The C code is generated, compile it to an executable
	fun compile_c_code(compile_dir: String)
	do
		var makename = makefile_name

		var makeflags = self.toolcontext.opt_make_flags.value
		if makeflags == null then makeflags = ""

		var command = "make -B -C {compile_dir} -f {makename} -j 4 {makeflags}"
		self.toolcontext.info(command, 2)

		var res
		if self.toolcontext.verbose_level >= 3 then
			res = sys.system("{command} 2>&1")
		else if is_windows then
			res = sys.system("{command} 2>&1 >nul")
		else
			res = sys.system("{command} 2>&1 >/dev/null")
		end
		if res != 0 then
			toolcontext.error(null, "Compilation Error: `make` failed with error code: {res}. The command was `{command}`.")
		end
	end
end
src/compiler/abstract_compiler.nit:166,1--598,3