# ===================================================================== config
APP := ears
VERSION := 1.0.0
MAINTAINER := Kazi <kazi.m@chiba-u.jp>
SUMMARY := Auscultation training software
HOMEPAGE = https://nlab.tms.chiba-u.jp/
YEAR := $(shell date +%Y)
DATESTAMP := $(shell date -R)
# literal '#', so make never eats shebangs or comment lines as its own comments
H := \#
PREFIX ?= /usr
DESTDIR ?=
LIBDIR := $(PREFIX)/lib/$(APP)
BINDIR := $(PREFIX)/bin
MCS := mcs
PKGS := -pkg:gtk-sharp-3.0
MCSFLAGS := -optimize+ -langversion:latest -warn:4
# ------------------------------------------------------------- source discovery
# Uses src/ and data/ when present, otherwise the project root.
SRCDIR ?= $(if $(wildcard src/*.cs),src,.)
DATADIR ?= $(if $(wildcard data/*.glade),data,.)
# .cs files in SRCDIR that must NOT be compiled (old WinForms code, scratch files)
EXCLUDE ?= Form1.cs Form2.cs
SRC := $(filter-out $(addprefix $(SRCDIR)/,$(EXCLUDE)),$(wildcard $(SRCDIR)/*.cs))
DATA := $(wildcard $(DATADIR)/*.glade) $(wildcard $(DATADIR)/*.css) \
$(wildcard $(DATADIR)/*.csv) $(wildcard $(DATADIR)/*.png)
# ---------------------------------------------------------------- output layout
OUT := bin
LOUT := $(OUT)/linux
LINST := $(LOUT)/installer
LGEN := $(LINST)/gen
LROOT := $(LINST)/root
EXE := $(LOUT)/$(APP).exe
DEB := $(LINST)/$(APP)_$(VERSION)_all.deb
WOUT := $(OUT)/windows
WBUNDLE := $(WOUT)/$(APP)
WINST := $(WOUT)/installer
WGEN := $(WINST)/gen
DBGEXE := $(WBUNDLE)/$(APP)-debug.exe
WINZIP := $(WOUT)/$(APP)-$(VERSION)-windows.zip
SETUP := $(WINST)/$(APP)-$(VERSION)-setup.exe
# gtk-sharp managed assemblies + their dllmap .config files (for the Win bundle)
GTKSHARP := $(shell pkg-config --variable=Libraries gtk-sharp-3.0 2>/dev/null)
ifeq ($(strip $(GTKSHARP)),)
GTKSHARP := $(wildcard /usr/lib/cli/*/*.dll)
endif
GTKSHARP_CFG := $(wildcard $(addsuffix .config,$(GTKSHARP)))
MSYS2 ?= /c/msys64
MINGW := $(MSYS2)/mingw64
ISCC ?= iscc
.DEFAULT_GOAL := all
.PHONY: all build run debug clean distclean dep-check show
.PHONY: gen install uninstall deb deb-install deb-remove deb-test lint
.PHONY: win win-gtk installer everything
all: deb
build: $(EXE)
everything: deb win
$(LOUT) $(LGEN) $(LROOT) $(WBUNDLE) $(WGEN):
mkdir -p $@
# ================================================================== 1. compile
$(EXE): $(SRC) | $(LOUT)
@test -n "$(strip $(SRC))" || { \
echo "ERROR: no .cs files found (SRCDIR=$(SRCDIR)). Run 'make show'."; exit 1; }
$(MCS) -target:winexe $(PKGS) $(MCSFLAGS) -out:$@ $(SRC)
# console build: -target:exe keeps Console.WriteLine visible on Windows
$(DBGEXE): $(SRC) | $(WBUNDLE)
@test -n "$(strip $(SRC))" || { \
echo "ERROR: no .cs files found (SRCDIR=$(SRCDIR)). Run 'make show'."; exit 1; }
$(MCS) -target:exe -debug -define:DEBUG $(PKGS) -out:$@ $(SRC)
debug: $(DBGEXE)
run: build
EARS_DATADIR=$(CURDIR)/$(DATADIR) mono $(EXE)
clean:
rm -rf $(OUT)
distclean: clean
show:
@echo "SRCDIR = $(SRCDIR)"
@echo "DATADIR = $(DATADIR)"
@echo "SRC = $(words $(SRC)) file(s)"
@$(foreach f,$(SRC),echo " $(f)";)
@echo "DATA = $(words $(DATA)) file(s)"
@$(foreach f,$(DATA),echo " $(f)";)
@echo "EXE = $(EXE)"
@echo "DEB = $(DEB)"
@echo "GTK# = $(words $(GTKSHARP)) managed assemblies"
dep-check:
@command -v $(MCS) >/dev/null || { echo "missing: mono-devel (mcs)"; exit 1; }
@pkg-config --exists gtk-sharp-3.0 || { echo "missing: gtk-sharp3"; exit 1; }
@command -v dpkg-deb >/dev/null || { echo "missing: dpkg-dev"; exit 1; }
@echo "toolchain OK"
# ========================================================== 2. generated files
define GEN_WRAPPER
$(H)!/bin/sh
$(H) $(APP) launcher - generated by make, do not edit
: "$${EARS_DATADIR:=$(LIBDIR)}"
export EARS_DATADIR
exec /usr/bin/mono "$(LIBDIR)/$(APP).exe" "$$@"
endef
define GEN_DESKTOP
[Desktop Entry]
Type=Application
Name=EARS
GenericName=Serial control panel
Comment=$(SUMMARY)
Exec=$(APP)
Icon=$(APP)
Terminal=false
Categories=Utility;Electronics;
Keywords=serial;esp32;arduino;
StartupNotify=true
endef
define GEN_ICON
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<rect x="8" y="8" width="112" height="112" rx="24" fill="rgb(31,111,235)"/>
<g fill="none" stroke="rgb(255,255,255)" stroke-width="8" stroke-linecap="round">
<path d="M40 82 V54 a24 24 0 0 1 48 0 V82"/>
<path d="M34 82 h20 M74 82 h20"/>
</g>
<circle cx="64" cy="100" r="7" fill="rgb(255,255,255)"/>
</svg>
endef
define GEN_UDEV
$(H) generated by make - serial adapters used by $(APP)
$(H) TAG+="uaccess" gives the device to the active desktop user with no group.
$(H) GROUP="dialout" is the fallback for ssh / headless / WSL sessions.
ACTION!="add", GOTO="ears_end"
SUBSYSTEM!="tty", GOTO="ears_end"
ATTRS{idVendor}=="303a", MODE="0660", GROUP="dialout", TAG+="uaccess"
ATTRS{idVendor}=="10c4", MODE="0660", GROUP="dialout", TAG+="uaccess"
ATTRS{idVendor}=="1a86", MODE="0660", GROUP="dialout", TAG+="uaccess"
ATTRS{idVendor}=="0403", MODE="0660", GROUP="dialout", TAG+="uaccess"
ATTRS{idVendor}=="2341", MODE="0660", GROUP="dialout", TAG+="uaccess"
LABEL="ears_end"
endef
define GEN_CONTROL
Package: $(APP)
Version: $(VERSION)
Section: utils
Priority: optional
Architecture: all
Depends: mono-runtime (>= 6.0), gtk-sharp3, libgtk-3-0 | libgtk-3-0t64, libmono-system4.0-cil | mono-complete
Recommends: mono-complete
Installed-Size: @SIZE@
Maintainer: $(MAINTAINER)
Homepage: $(HOMEPAGE)
Description: $(SUMMARY)
GTK$(H)3 front end that talks to an ESP32 over a serial port, with
English/Japanese language switching and a live device log.
.
Architecture is "all" because the payload is architecture-neutral CIL
executed by the Mono runtime.
endef
define GEN_POSTINST
$(H)!/bin/sh
set -e
case "$$1" in
configure)
if command -v udevadm >/dev/null 2>&1; then
udevadm control --reload-rules >/dev/null 2>&1 || true
udevadm trigger --subsystem-match=tty >/dev/null 2>&1 || true
fi
if [ -n "$$SUDO_USER" ] && getent group dialout >/dev/null 2>&1; then
if ! id -nG "$$SUDO_USER" 2>/dev/null | tr ' ' '\n' | grep -qx dialout; then
usermod -aG dialout "$$SUDO_USER" >/dev/null 2>&1 || true
echo "$(APP): added '$$SUDO_USER' to group 'dialout'."
echo "$(APP): log out and back in for serial access to take effect."
fi
fi
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -qtf /usr/share/icons/hicolor || true
fi
;;
esac
exit 0
endef
define GEN_POSTRM
$(H)!/bin/sh
set -e
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -qtf /usr/share/icons/hicolor || true
fi
exit 0
endef
define GEN_CHANGELOG
$(APP) ($(VERSION)) stable; urgency=medium
* Package generated by make.
-- $(MAINTAINER) $(DATESTAMP)
endef
define GEN_COPYRIGHT
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: $(APP)
Files: *
Copyright: $(YEAR) $(MAINTAINER)
License: proprietary
All rights reserved.
endef
gen: | $(LGEN)
@$(file >$(LGEN)/$(APP).sh,$(GEN_WRAPPER))
@$(file >$(LGEN)/$(APP).desktop,$(GEN_DESKTOP))
@$(file >$(LGEN)/$(APP).svg,$(GEN_ICON))
@$(file >$(LGEN)/60-$(APP)-serial.rules,$(GEN_UDEV))
@$(file >$(LGEN)/control,$(GEN_CONTROL))
@$(file >$(LGEN)/postinst,$(GEN_POSTINST))
@$(file >$(LGEN)/postrm,$(GEN_POSTRM))
@$(file >$(LGEN)/changelog,$(GEN_CHANGELOG))
@$(file >$(LGEN)/copyright,$(GEN_COPYRIGHT))
@chmod 755 $(LGEN)/postinst $(LGEN)/postrm $(LGEN)/$(APP).sh
@echo "generated packaging sources in $(LGEN)"
# =========================================================== 3. make install
install: build gen
install -d $(DESTDIR)$(LIBDIR) $(DESTDIR)$(BINDIR)
install -m 644 $(EXE) $(DESTDIR)$(LIBDIR)/$(APP).exe
@if [ -n "$(strip $(DATA))" ]; then \
echo "install -m 644 <$(words $(DATA)) data files> $(DESTDIR)$(LIBDIR)/"; \
install -m 644 $(DATA) $(DESTDIR)$(LIBDIR)/; \
else \
echo "WARNING: no data files found in '$(DATADIR)'"; \
fi
install -m 755 $(LGEN)/$(APP).sh $(DESTDIR)$(BINDIR)/$(APP)
install -D -m 644 $(LGEN)/$(APP).desktop \
$(DESTDIR)$(PREFIX)/share/applications/$(APP).desktop
install -D -m 644 $(LGEN)/$(APP).svg \
$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/$(APP).svg
install -D -m 644 $(LGEN)/60-$(APP)-serial.rules \
$(DESTDIR)/lib/udev/rules.d/60-$(APP)-serial.rules
install -D -m 644 $(LGEN)/copyright \
$(DESTDIR)$(PREFIX)/share/doc/$(APP)/copyright
gzip -9nc $(LGEN)/changelog > $(LGEN)/changelog.gz
install -D -m 644 $(LGEN)/changelog.gz \
$(DESTDIR)$(PREFIX)/share/doc/$(APP)/changelog.Debian.gz
uninstall:
rm -rf $(DESTDIR)$(LIBDIR) $(DESTDIR)$(PREFIX)/share/doc/$(APP)
rm -f $(DESTDIR)$(BINDIR)/$(APP) \
$(DESTDIR)$(PREFIX)/share/applications/$(APP).desktop \
$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/$(APP).svg \
$(DESTDIR)/lib/udev/rules.d/60-$(APP)-serial.rules
# ==================================================================== 4. .deb
deb: build gen
rm -rf $(LROOT)
$(MAKE) --no-print-directory install DESTDIR=$(CURDIR)/$(LROOT) PREFIX=/usr
install -d $(LROOT)/DEBIAN
install -m 755 $(LGEN)/postinst $(LGEN)/postrm $(LROOT)/DEBIAN/
sed "s|@SIZE@|$$(du -ks --exclude=DEBIAN $(LROOT) | cut -f1)|" \
$(LGEN)/control > $(LROOT)/DEBIAN/control
cd $(LROOT) && find . -type f ! -path './DEBIAN/*' -printf '%P\0' \
| LC_ALL=C sort -z | xargs -0 md5sum > DEBIAN/md5sums
find $(LROOT) -type d -exec chmod 755 {} +
dpkg-deb --build --root-owner-group $(LROOT) $(DEB)
@echo
@echo "==> $(DEB)"
@echo " sudo apt install ./$(DEB) # the ./ is required"
deb-install: deb
sudo apt install -y ./$(DEB)
deb-remove:
sudo apt remove -y $(APP)
lint: deb
-lintian --no-tag-display-limit $(DEB)
deb-test: deb
docker run --rm -v "$(CURDIR)/$(LINST)":/pkg:ro ubuntu:24.04 sh -c '\
set -e; \
apt-get update -qq; \
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
/pkg/$(notdir $(DEB)) xvfb >/dev/null; \
echo "--- files:"; dpkg -L $(APP) | head -20; \
echo "--- runtime:"; mono --version | head -1; \
echo "--- smoke test:"; \
timeout 15 xvfb-run -a $(APP) || echo "exit=$$? (124 = window stayed open, OK)"'
# ================================================================= 5. windows
define GEN_WINBAT
@echo off
setlocal
set "HERE=%~dp0"
set "EARS_DATADIR=%HERE%"
if exist "%HERE%gtk\bin" set "PATH=%HERE%gtk\bin;%PATH%"
if exist "%HERE%gtk\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache" set "GDK_PIXBUF_MODULE_FILE=%HERE%gtk\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache"
if exist "%HERE%gtk\share\glib-2.0\schemas" set "GSETTINGS_SCHEMA_DIR=%HERE%gtk\share\glib-2.0\schemas"
if exist "%HERE%gtk\share" set "XDG_DATA_DIRS=%HERE%gtk\share;%XDG_DATA_DIRS%"
set "MONO=mono"
where mono >nul 2>&1 || set "MONO=%ProgramFiles%\Mono\bin\mono.exe"
if /i not "%MONO%"=="mono" if not exist "%MONO%" (
echo Mono for Windows was not found.
echo Install it from https://www.mono-project.com/download/stable/
pause
exit /b 1
)
start "" "%MONO%" "%HERE%$(APP).exe" %*
endef
define GEN_WINBATDBG
@echo off
setlocal
set "HERE=%~dp0"
set "EARS_DATADIR=%HERE%"
if exist "%HERE%gtk\bin" set "PATH=%HERE%gtk\bin;%PATH%"
if exist "%HERE%gtk\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache" set "GDK_PIXBUF_MODULE_FILE=%HERE%gtk\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache"
if exist "%HERE%gtk\share\glib-2.0\schemas" set "GSETTINGS_SCHEMA_DIR=%HERE%gtk\share\glib-2.0\schemas"
if exist "%HERE%gtk\share" set "XDG_DATA_DIRS=%HERE%gtk\share;%XDG_DATA_DIRS%"
set "MONO=mono"
where mono >nul 2>&1 || set "MONO=%ProgramFiles%\Mono\bin\mono.exe"
set "MONO_LOG_LEVEL=warning"
echo Running $(APP) with console output. Close this window to quit.
"%MONO%" "%HERE%$(APP)-debug.exe" %*
echo.
echo exit code %ERRORLEVEL%
pause
endef
win: build debug | $(WBUNDLE)
cp $(EXE) $(WBUNDLE)/$(APP).exe
@if [ -n "$(strip $(DATA))" ]; then cp $(DATA) $(WBUNDLE)/; \
else echo "WARNING: no data files found in '$(DATADIR)'"; fi
@if [ -n "$(strip $(GTKSHARP))" ]; then \
cp $(GTKSHARP) $(WBUNDLE)/; \
echo "copied $(words $(GTKSHARP)) gtk-sharp assemblies"; \
else echo "WARNING: gtk-sharp managed assemblies not found"; fi
@if [ -n "$(strip $(GTKSHARP_CFG))" ]; then cp $(GTKSHARP_CFG) $(WBUNDLE)/; fi
@$(file >$(WBUNDLE)/$(APP).bat,$(GEN_WINBAT))
@$(file >$(WBUNDLE)/$(APP)-debug.bat,$(GEN_WINBATDBG))
-$(MAKE) --no-print-directory win-gtk MSYS2=$(MSYS2)
cd $(WOUT) && rm -f $(notdir $(WINZIP)) && zip -qr $(notdir $(WINZIP)) $(APP)
@echo "==> $(WINZIP)"
win-gtk: | $(WBUNDLE)
@test -d $(MINGW)/bin || { \
echo "MSYS2 not found at $(MINGW) - shipping without native GTK."; \
echo " pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-librsvg mingw-w64-x86_64-ntldd"; \
echo " then: make win MSYS2=/path/to/msys64"; exit 1; }
rm -rf $(WBUNDLE)/gtk
mkdir -p $(WBUNDLE)/gtk/bin $(WBUNDLE)/gtk/share $(WBUNDLE)/gtk/lib
@echo "resolving native GTK dependencies..."
@if command -v ntldd >/dev/null 2>&1; then \
for root in libgtk-3-0.dll librsvg-2-2.dll libgdk_pixbuf-2.0-0.dll; do \
[ -f $(MINGW)/bin/$$root ] || continue; \
ntldd -R $(MINGW)/bin/$$root \
| grep -io '[a-z]:[\\/][^ ]*mingw64[\\/]bin[\\/][^ ]*\.dll' \
| tr '\\' '/' | sort -u \
| while read -r dll; do cp -u "$$dll" $(WBUNDLE)/gtk/bin/ 2>/dev/null || true; done; \
cp -u $(MINGW)/bin/$$root $(WBUNDLE)/gtk/bin/; \
done; \
else \
echo "ntldd missing - copying all of mingw64/bin (larger bundle)"; \
cp -u $(MINGW)/bin/*.dll $(WBUNDLE)/gtk/bin/; \
fi
-cp -r $(MINGW)/lib/gdk-pixbuf-2.0 $(WBUNDLE)/gtk/lib/
-cp -r $(MINGW)/share/glib-2.0 $(WBUNDLE)/gtk/share/
-cp -r $(MINGW)/share/icons $(WBUNDLE)/gtk/share/
-cp -r $(MINGW)/share/themes $(WBUNDLE)/gtk/share/
@echo "gtk runtime staged ($$(du -sh $(WBUNDLE)/gtk | cut -f1))"
define GEN_ISS
[Setup]
AppName=$(APP)
AppVersion=$(VERSION)
AppPublisher=$(MAINTAINER)
DefaultDirName={autopf}\$(APP)
DefaultGroupName=$(APP)
OutputBaseFilename=$(APP)-$(VERSION)-setup
OutputDir=.
Compression=lzma2/max
SolidCompression=yes
ArchitecturesInstallIn64BitMode=x64compatible
PrivilegesRequired=admin
UninstallDisplayIcon={app}\$(APP).exe
WizardStyle=modern
[Files]
Source: "..\..\$(APP)\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion
[Icons]
Name: "{group}\$(APP)"; Filename: "{app}\$(APP).bat"; IconFilename: "{app}\$(APP).exe"
Name: "{group}\$(APP) (console)"; Filename: "{app}\$(APP)-debug.bat"; IconFilename: "{app}\$(APP).exe"
Name: "{autodesktop}\$(APP)"; Filename: "{app}\$(APP).bat"; IconFilename: "{app}\$(APP).exe"; Tasks: desktopicon
[Tasks]
Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional icons:"
[Run]
Filename: "{app}\$(APP).bat"; Description: "Launch $(APP)"; Flags: postinstall nowait skipifsilent shellexec
[Code]
function MonoPresent(): Boolean;
begin
Result := FileExists(ExpandConstant('{commonpf}\Mono\bin\mono.exe')) or
FileExists(ExpandConstant('{commonpf32}\Mono\bin\mono.exe')) or
RegKeyExists(HKLM, 'SOFTWARE\Novell\Mono') or
RegKeyExists(HKLM, 'SOFTWARE\WOW6432Node\Novell\Mono');
end;
function InitializeSetup(): Boolean;
var Code: Integer;
begin
Result := True;
if not MonoPresent() then
if MsgBox('Mono for Windows was not detected. Open the download page now?',
mbConfirmation, MB_YESNO) = IDYES then
ShellExec('open', 'https://www.mono-project.com/download/stable/',
'', '', SW_SHOW, ewNoWait, Code);
end;
endef
installer: win | $(WGEN)
@$(file >$(WGEN)/$(APP).iss,$(GEN_ISS))
@command -v $(ISCC) >/dev/null 2>&1 || { \
echo "Inno Setup compiler '$(ISCC)' not found."; \
echo " Windows: make installer ISCC=\"/c/Program Files (x86)/Inno Setup 6/ISCC.exe\""; \
echo " Linux: make installer ISCC=\"wine /path/to/ISCC.exe\""; exit 1; }
cd $(WGEN) && $(ISCC) $(APP).iss
mv $(WGEN)/$(APP)-$(VERSION)-setup.exe $(SETUP)
@echo "==> $(SETUP)"