# Unison file synchronizer: src/Makefile.OCaml
# See ../LICENSE for terms.

####################################################################
#         Makefile rules for compiling ocaml programs              #
####################################################################

####################################################################
### Compilers

OCAMLC=$(TOOL_PREFIX)ocamlc
OCAMLOPT=$(TOOL_PREFIX)ocamlopt

####################################################################
### Try to automatically guess OS

ifeq (${OSCOMP},cross) # Cross-compilation under Linux
  TOOL_PREFIX=x86_64-w64-mingw32-
endif

# Cygwin is for doing POSIX builds in Windows.
# MinGW is for doing native Windows builds in a minimal POSIX environment.
# MSVC is for native Windows builds without POSIX env (yet this Makefile
# requires a minimal POSIX environment anyway).
OCAML_OSTYPE:=$(shell $(OCAMLC) -config-var os_type)

ifeq (${OCAML_OSTYPE},Win32)        # Native Windows build
  OSARCH=Win32
else ifeq (${OCAML_OSTYPE},Cygwin)  # POSIX build for Windows
  OSARCH=Cygwin
else
  OSARCH:=$(shell uname)
  # Darwin is reported for macOS
  # SunOS is reported for Solaris, OpenSolaris and derivatives (illumos)
endif

####################################################################
### Try to automatically guess UI style

ifneq ($(strip $(UISTYLE)),)
  $(error UISTYLE is no longer used. See build instructions)
endif

OCAMLLIBDIR:=$(shell $(OCAMLC) -config-var standard_library)

# For Windows, an additional UI style modifier is available, `UI_WINOS`
#   Legal values are
#     UI_WINOS=         # *default*; builds unison purely as a Windows console ('text') or GUI ('gtk3') application
#     UI_WINOS=hybrid   # (with UISTYLE=gtk3) builds unison as a hybrid application (GUI application attached to a text console)
# * ref: <https://github.com/bcpierce00/unison/issues/778>
#
OCAMLFIND := $(shell command -v ocamlfind 2> /dev/null)

ifdef OCAMLFIND
  ifneq ($(strip $(shell $(OCAMLFIND) query lablgtk3 2> /dev/null)),)
    HAS_LABLGTK3=true
  endif
else
  LABLGTK3LIB=$(OCAMLLIBDIR)/lablgtk3
  ifeq ($(wildcard $(LABLGTK3LIB)),$(LABLGTK3LIB))
    HAS_LABLGTK3=true
  endif
endif

.PHONY: guimaybe
ifeq ($(HAS_LABLGTK3), true)
  guimaybe: gui
else
  guimaybe:
	$(info GUI library lablgtk not found. GTK GUI will not be built.)
endif

.PHONY: macuimaybe
ifeq ($(OSARCH), Darwin)
  # If XCode is not installed, xcodebuild is just a placeholder telling that XCode is not installed
  # and any invocation of xcodebuild results in a non 0 exit code.
  ifeq ($(shell xcodebuild -version > /dev/null; echo $$?), 0)
    macuimaybe: macui
  else
    macuimaybe:
	$(info Not building macOS native GUI because XCode is not installed.)
  endif
else
  macuimaybe:
	$(info Not on macOS. macOS native GUI will not be built.)
endif

####################################################################
### Default parameters

.PHONY: buildinfodebug
buildinfodebug:
	$(info $(building_for))
	$(info NATIVE = $(NATIVE))

buildexecutable: buildinfodebug

# Generate backtrace information for exceptions
CAMLFLAGS+=-g

ifneq ($(strip $(CFLAGS)),)
  CAMLCFLAGS+=-ccopt '$(CFLAGS)'
endif
ifneq ($(strip $(CPPFLAGS)),)
  CAMLCFLAGS+=-ccopt '$(CPPFLAGS)'
endif
ifneq ($(strip $(LDFLAGS)),)
  CAMLLDFLAGS+=-cclib '$(LDFLAGS)'
endif
ifneq ($(strip $(LDLIBS)),)
  CLIBS+=-cclib '$(LDLIBS)'
endif

INCLFLAGS=-I lwt -I ubase -I system
DEP_INCLFLAGS=-I lwt -I ubase -I system
CAMLFLAGS+=$(INCLFLAGS)
CAMLFLAGS+=-I system/$(SYSTEM) -I lwt/$(SYSTEM)

# The messy situation requiring the use of OUTPUT_SEL was fixed in OCaml 4.13.
# All usages of OUTPUT_SEL should be removed when 4.13 becomes a requirement.
ifeq ($(OSARCH),Win32)
  # Native Win32 build
  EXEC_EXT=.exe
  ifeq ($(shell $(OCAMLC) -config-var ext_obj),.obj)
    OBJ_EXT=.obj
  else
    OBJ_EXT=.o
  endif
  ifeq ($(shell $(OCAMLC) -config-var ccomp_type),msvc)
    OUTPUT_SEL=-Fo
    CLIBS+=-cclib shell32.lib -cclib user32.lib -cclib "-link win32rc/unison.res"
    buildexecutable: win32rc/unison.res
  else
    OUTPUT_SEL=-o
    CLIBS+=-cclib "-link win32rc/unison.res.lib"
    buildexecutable: win32rc/unison.res.lib
  endif
  # Make Windows GUI come up with no console window
  ifneq ($(UI_WINOS), hybrid)
    LDFLAGS_GUI+=-subsystem windows
    # -subsystem is a flexlink arg;
    # respective gcc args: -mwindows or -Wl,--subsystem,windows
    # respective MSVC linker arg: /SUBSYSTEM:WINDOWS
  endif
  CWD=.
  COBJS+=system/system_win_stubs$(OBJ_EXT) lwt/lwt_unix_stubs$(OBJ_EXT)
  WINOBJS=system/system_win.cmo
  SYSTEM=win
  building_for = Building for Windows
else
  # Unix build, or Cygwin POSIX (GNU C) build
  OBJ_EXT=.o
  OUTPUT_SEL="-o "
  WINOBJS=
  SYSTEM=generic
  # This is not strictly necessary as Cygwin can do a generic Unix build
  # (and is actually meant to).
  ifeq ($(OSARCH),Cygwin)
    CWD=.
    EXEC_EXT=.exe
    CLIBS+=-cclib win32rc/unison.res.lib
    buildexecutable: win32rc/unison.res.lib
    building_for = Building for Cygwin
  else
    CWD=$(shell pwd)
    EXEC_EXT=
    # openpty is in the libutil library
    ifneq ($(OSARCH),SunOS)
      ifneq ($(OSARCH),Darwin)
        CLIBS+=-cclib -lutil
      endif
    endif
    ifeq ($(OSARCH),SunOS)
      # ACL functions
      CLIBS+=-cclib -lsec
    endif
    building_for = Building for Unix
  endif
endif

ubase/projectInfo.ml: Makefile.ProjectInfo
	echo 'let myName = "'$(NAME)'";;' > $@
	echo 'let myVersion = "'$(VERSION)'";;' >> $@
	echo 'let myMajorVersion = "'$(MAJORVERSION)'";;' >> $@

clean::
	$(RM) ubase/projectInfo.ml

####################################################################
### Compilation boilerplate

ifeq ($(NATIVE), true)
  ## Set up for native code compilation

  CAMLC=$(OCAMLOPT)

  CAMLOBJS=$(subst .cmo,.cmx, $(subst .cma,.cmxa, $(OCAMLOBJS)))
  CAMLLIBS=$(subst .cma,.cmxa, $(OCAMLLIBS))

else
  ## Set up for bytecode compilation

  CAMLC=$(OCAMLC)
  # -output-complete-exe is available since OCaml 4.10
  ifneq ($(strip $(shell $(OCAMLC) -output-complete-exe 2>&1)),)
    CAMLLDFLAGS+=-custom
  else
    CAMLLDFLAGS+=-output-complete-exe  # can safely strip the binary
  endif

  CAMLOBJS=$(OCAMLOBJS)
  CAMLLIBS=$(OCAMLLIBS)

endif

WINDRES := $(or ${TOOL_PREFIX},$(filter i686-w64-mingw32- x86_64-w64-mingw32-,$(CC:gcc${EXEC_EXT}=)))windres
##$(info windres='${WINDRES}')

####################################################################
### Unison objects and libraries

# OCaml objects for the bytecode version
# File extensions will be substituted for the native code version

OCAMLOBJS += \
          ubase/umarshal.cmo \
          ubase/rx.cmo \
          \
          unicode_tables.cmo unicode.cmo bytearray.cmo \
          system/system_generic.cmo $(WINOBJS) \
          system/$(SYSTEM)/system_impl.cmo \
          system.cmo \
          \
          ubase/projectInfo.cmo ubase/myMap.cmo ubase/safelist.cmo \
          ubase/util.cmo ubase/uarg.cmo \
          ubase/prefs.cmo ubase/trace.cmo ubase/proplist.cmo \
          \
          lwt/pqueue.cmo lwt/lwt.cmo lwt/lwt_util.cmo \
          lwt/$(SYSTEM)/lwt_unix_impl.cmo lwt/lwt_unix.cmo \
          \
          features.cmo uutil.cmo case.cmo pred.cmo terminal.cmo \
          fileutil.cmo name.cmo path.cmo fspath.cmo fs.cmo fingerprint.cmo \
          abort.cmo osx.cmo fswatch.cmo propsdata.cmo \
          props.cmo fileinfo.cmo os.cmo lock.cmo clroot.cmo common.cmo \
          tree.cmo checksum.cmo transfer.cmo xferhint.cmo \
          remote.cmo external.cmo negotiate.cmo globals.cmo fswatchold.cmo \
          fpcache.cmo update.cmo copy.cmo stasher.cmo \
          files.cmo sortri.cmo recon.cmo transport.cmo \
          strings.cmo uicommon.cmo uitext.cmo test.cmo

OCAMLOBJS+=main.cmo

# OCaml libraries for the bytecode version
# File extensions will be substituted for the native code version
OCAMLLIBS+=unix.cma str.cma
INCLFLAGS+=-I +unix -I +str

# C objects for both bytecode and native versions
COBJS += osxsupport$(OBJ_EXT) pty$(OBJ_EXT) bytearray_stubs$(OBJ_EXT) \
         hash_compat$(OBJ_EXT) props_xattr$(OBJ_EXT) props_acl$(OBJ_EXT)

####################################################################
### User Interface setup

## Text UI
OCAMLOBJS_TUI+=linktext.cmo

## Mac UI
OCAMLOBJS_MAC+=uimacbridge.cmo
OCAMLLIBS_MAC+=threads.cma
INCLFLAGS_MAC+=-I +threads

## Graphic UI
ifndef OCAMLFIND
  CAMLFLAGS_GUI+=-I +lablgtk3 -I +cairo2
else
  CAMLFLAGS_GUI+=$(shell $(OCAMLFIND) query -i-format lablgtk3 )
  CAMLFLAGS_GUI+=$(shell $(OCAMLFIND) query -i-format cairo2 )
endif
OCAMLOBJS_GUI+=pixmaps.cmo uigtk3.cmo linkgtk3.cmo
OCAMLLIBS_GUI+=lablgtk3.cma cairo.cma

ifneq ($(strip $(LDFLAGS_GUI)),)
  CAMLLDFLAGS_GUI+=-cclib "$(LDFLAGS_GUI)"
endif

####################################################################
### Unison executables

NAME_GUI = $(NAME)-gui

.PHONY: tui
tui: buildexecutable $(NAME)$(EXEC_EXT)

.PHONY: gui
gui: buildexecutable $(NAME_GUI)$(EXEC_EXT)

.PHONY: macui
macui: buildexecutable macexecutable

UIMACDIR=uimac

ifeq ($(OSARCH),Darwin)
ifeq ($(strip $(XCODEFLAGS)),)
  XCODEFLAGS=-arch $(shell uname -m)  ## Prevent Xcode from trying to build universal binaries by default
endif
endif

# Note: The OCaml library names changed starting with OCaml 5.1.0
.PHONY: macexecutable
macexecutable:
	(cd $(UIMACDIR); \
	  LIB_SUFFIX=$$(ocaml -e 'if Sys.ocaml_release.major > 5 || Sys.ocaml_release.major = 5 && Sys.ocaml_release.minor >= 1 then print_string "nat"' 2> /dev/null); \
	  printf "MARKETING_VERSION = $(VERSION)\nOCAMLLIBDIR = $(OCAMLLIBDIR)\nOCAMLLIB_UNIX = -lunix$${LIB_SUFFIX}\nOCAMLLIB_STR = -lcamlstr$${LIB_SUFFIX}" > ExternalSettings.xcconfig)
	(cd $(UIMACDIR); xcodebuild $(XCODEFLAGS) SYMROOT=build)
	$(CC) $(CFLAGS) $(UIMACDIR)/cltool.c -o $(UIMACDIR)/build/Default/Unison.app/Contents/MacOS/cltool -framework Carbon
	codesign --remove-signature $(UIMACDIR)/build/Default/Unison.app
	codesign --force --sign - $(UIMACDIR)/build/Default/Unison.app/Contents/MacOS/cltool
	codesign --force --sign - --entitlements $(UIMACDIR)/build/uimac*.build/Default/uimac.build/Unison.app.xcent $(UIMACDIR)/build/Default/Unison.app
	codesign --verify --deep --strict $(UIMACDIR)/build/Default/Unison.app
# cltool was added into the .app after it was signed, so the signature is now
# broken. It must be removed, cltool separately signed, and then the entire
# .app (re-)signed.

clean::
	-(cd $(UIMACDIR); xcodebuild clean 2> /dev/null) || true
	-(cd $(UIMACDIR); $(RM) -r build ExternalSettings.xcconfig)

####################################################################
### Filesystem monitoring

NAME_FSM = $(NAME)-fsmonitor

ifeq ($(OSARCH),Linux)
  FSMDIR = fsmonitor/inotify
endif

ifneq ($(findstring $(OSARCH),FreeBSD OpenBSD NetBSD DragonFly),)
  LIBINOTIFY_LIB:=-cclib '$(shell pkg-config --libs libinotify 2> /dev/null || printf ' -linotify')'
  LIBINOTIFY_INC:=-ccopt '$(shell pkg-config --cflags libinotify 2> /dev/null)'
  FOUND_LIBINOTIFY := $(shell { printf '' > inotifytest__.ml ;\
    $(CAMLC) $(CAMLCFLAGS) $(CAMLLDFLAGS) $(LIBINOTIFY_LIB) \
      -o inotifytest__ inotifytest__.ml > /dev/null 2>&1 && printf true ; } ;\
    rm -f inotifytest__.ml inotifytest__.cm[oix] inotifytest__.o inotifytest__ > /dev/null 2>&1)
  ifeq ($(FOUND_LIBINOTIFY), true)
    FSMDIR = fsmonitor/inotify
    $(NAME_FSM)$(EXEC_EXT): CAMLCFLAGS += $(LIBINOTIFY_INC)
    $(NAME_FSM)$(EXEC_EXT): CLIBS += $(LIBINOTIFY_LIB)
  endif
endif

ifeq ($(OSARCH),SunOS)
  FSMDIR = fsmonitor/solaris
endif

ifeq ($(OSARCH),Win32)
  FSMDIR = fsmonitor/windows
endif

.PHONY: fsmonitor
ifdef FSMDIR
  fsmonitor: buildexecutable $(NAME_FSM)$(EXEC_EXT)
  -include $(FSMDIR)/Makefile
else
  fsmonitor:
	$(info fsmonitor implementation is not available or not configured for this system. fsmonitor will not be built.)
endif

DEP_INCLFLAGS+=-I fsmonitor -I fsmonitor/inotify -I fsmonitor/solaris -I fsmonitor/windows

####################################################################
### Dependencies

# Include an automatically generated list of dependencies
-include .depend
# Additional dependencies depending on the system
system.cmo fspath.cmo fs.cmo: system/$(SYSTEM)/system_impl.cmo
system.cmx fspath.cmx fs.cmx: system/$(SYSTEM)/system_impl.cmx
lwt/lwt_unix.cmo: lwt/$(SYSTEM)/lwt_unix_impl.cmo
lwt/lwt_unix.cmx: lwt/$(SYSTEM)/lwt_unix_impl.cmx
lwt/win/lwt_win.cmo: lwt/win/lwt_unix_impl.cmo system/system_win.cmo
lwt/win/lwt_win.cmx: lwt/win/lwt_unix_impl.cmx system/system_win.cmx

.PHONY: depend
depend:
	ocamlc -depend $(DEP_INCLFLAGS) *.mli *.ml */*.ml */*.mli */*/*.ml */*/*.mli > .depend

.PHONY: dependgraph
dependgraph: depend
	echo 'digraph G {' > .depend.dot.tmp
	echo '{ rank = same; "Fileinfo"; "Props"; "Fspath"; "Os"; "Path"; }'\
           >>.depend.dot.tmp
	echo '{ rank = same; "Uitext"; "Uigtk"; }'>>.depend.dot.tmp
	echo '{ rank = same; "Recon"; "Update"; "Transport"; "Files"; }'\
           >>.depend.dot.tmp
	echo '{ rank = same; "Tree"; "Safelist"; }'>>.depend.dot.tmp
	echo '{ rank = same; "Uarg"; "Prefs"; }'>>.depend.dot.tmp
	echo 'Attempting to use ocamldot and dot to produce a dependency graph:'
	-ocamldot .depend | tail -n +2 >> .depend.dot.tmp
	-dot -Tps -o DEPENDENCIES.ps .depend.dot.tmp

####################################################################
### Compilation

win32rc/unison.res: win32rc/unison.rc win32rc/U.ico
	rc win32rc/unison.rc

win32rc/unison.res.lib: win32rc/unison.rc win32rc/U.ico
	$(WINDRES) win32rc/unison.rc win32rc/unison.res
	$(WINDRES) win32rc/unison.res win32rc/unison.res.lib

%.ml: %.mll
	-$(RM) $@
	ocamllex $<

%.cmi : %.mli
	@echo "$(CAMLC): $< ---> $@"
	$(CAMLC) $(CAMLFLAGS) -c $(CWD)/$<

%.cmo: %.ml
	@echo "$(OCAMLC): $< ---> $@"
	$(OCAMLC) $(CAMLFLAGS) -c $(CWD)/$<

%.cmx: %.ml
	@echo "$(OCAMLOPT): $< ---> $@"
	$(OCAMLOPT) $(CAMLFLAGS) -c $(CWD)/$<

%$(OBJ_EXT): %.c
	@echo "$(CAMLC): $< ---> $@"
	$(CAMLC) $(CAMLFLAGS) $(CAMLCFLAGS) -ccopt $(OUTPUT_SEL)$(CWD)/$@ -c $(CWD)/$<

# Need secondary expansion to bypass the immediate evaluation of pre-requisites
.SECONDEXPANSION:

$(NAME)$(EXEC_EXT): OCAMLOBJS += $(OCAMLOBJS_TUI)

$(NAME_GUI)$(EXEC_EXT): OCAMLOBJS += $(OCAMLOBJS_GUI)
$(NAME_GUI)$(EXEC_EXT): OCAMLLIBS += $(OCAMLLIBS_GUI)
$(NAME_GUI)$(EXEC_EXT): CAMLFLAGS += $(CAMLFLAGS_GUI)
$(NAME_GUI)$(EXEC_EXT): CAMLLDFLAGS += $(CAMLLDFLAGS_GUI)

$(NAME_FSM)$(EXEC_EXT): OCAMLOBJS = $(FSMOCAMLOBJS)
$(NAME_FSM)$(EXEC_EXT): OCAMLLIBS = $(FSMOCAMLLIBS)
$(NAME_FSM)$(EXEC_EXT): CAMLFLAGS += -I fsmonitor -I $(FSMDIR)
$(NAME_FSM)$(EXEC_EXT): COBJS = $(FSMCOBJS)

$(NAME)$(EXEC_EXT) $(NAME_GUI)$(EXEC_EXT) $(NAME_FSM)$(EXEC_EXT): $$(CAMLOBJS) $$(COBJS)
	@echo Linking $@
	$(CAMLC) -verbose $(CAMLFLAGS) $(CAMLLDFLAGS) -o $@ $(CAMLCFLAGS) $(CAMLLIBS) $^ $(CLIBS)

$(NAME)-blob.o: OCAMLOBJS += $(OCAMLOBJS_MAC)
$(NAME)-blob.o: OCAMLLIBS += $(OCAMLLIBS_MAC)
$(NAME)-blob.o: INCLFLAGS += $(INCLFLAGS_MAC)
# Unfortunately -output-obj does not put .o files into the output, only .cmx
# files, so we have to use $(LD) to take care of COBJS.
# [2023-07] The limitations of -output-obj have been overcome by a new option
# -output-complete-obj, available since OCaml 4.02.2. Still, this option can't
# be used here because there is a bug which breaks linking on macOS when using
# threads (needed by uimacbridge): https://github.com/ocaml/ocaml/issues/7933
$(NAME)-blob.o: $$(CAMLOBJS) $(COBJS)
	@echo Linking $@
	$(CAMLC) -dstartup -output-obj -verbose -cclib -keep_private_externs $(CAMLFLAGS) -o u-b.o $(CAMLCFLAGS) $(CAMLLDFLAGS) $(CAMLLIBS) $(CLIBS) $(CAMLOBJS)
	$(LD) -r -keep_private_externs -o $@ u-b.o $(COBJS)
	$(RM) u-b.o

####################################################################
### Misc

clean::
	-$(RM) $(NAME) $(NAME).exe
	-$(RM) $(NAME_GUI) $(NAME_GUI).exe
	-$(RM) $(NAME_FSM) $(NAME_FSM).exe
	-$(RM) $(NAME)-blob.o
	-$(RM) -r *.cmi *.cmo *.cmx *.cma *.cmxa TAGS tags
	-$(RM) -r *.o core gmon.out *~ .*~
	-$(RM) -r *.obj *.lib *.exp
	-$(RM) -r *.tmp *.bak?.tmp .*.bak?.tmp
	-$(RM) system/*.cm[iox] system/*.o system/*.obj system/win/*~
	-$(RM) system/generic/*.cm[iox] system/generic/*.o system/generic/*.obj system/generic/*~
	-$(RM) system/win/*.cm[iox] system/win/*.o system/win/*.obj system/win/*~
	-$(RM) win32rc/unison.res win32rc/unison.res.lib
	-$(RM) fsmonitor/*.cm[iox] fsmonitor/*.o fsmonitor/*.obj fsmonitor/*~
ifdef FSMDIR
	-$(RM) $(FSMDIR)/*.cm[iox] $(FSMDIR)/*.o $(FSMDIR)/*~
endif
	-$(RM) .depend.dot.tmp DEPENDENCIES.ps
	-$(RM) ubase/*.cm[ioxa] ubase/*.cmxa ubase/*.a ubase/*.o ubase/*~ ubase/*.bak
	-$(RM) lwt/*.cm[ioxa] lwt/*.cmxa lwt/*.a lwt/*.o lwt/*.obj lwt/*~ lwt/*.bak
	-$(RM) lwt/generic/*.cm[ioxa] lwt/generic/*.cmxa lwt/generic/*.a lwt/generic/*.o lwt/generic/*.obj lwt/generic/*~ lwt/generic/*.bak
	-$(RM) lwt/win/*.cm[ioxa] lwt/win/*.cmxa lwt/win/*.a lwt/win/*.o lwt/win/*.obj lwt/win/*~ lwt/win/*.bak

.PHONY: paths
paths:
	@echo PATH = '$(PATH)'
	@echo OCAMLLIBDIR = '$(OCAMLLIBDIR)'
