From 8d48a70ac5b6309c87e58fff74c845bb1b0683f6 Mon Sep 17 00:00:00 2001 From: Thomas Cherryhomes Date: Thu, 15 Oct 2026 12:27:45 -0500 Subject: [PATCH 1/3] [mekkogx] Update to upstream 53afcc4 Brings in h89-hdos.mk, and an fnlib.py that builds just the platform it needs from a git FUJINET_LIB. hirestxt-mod-lib.mk's HIRESTXT_LIB is now HIRESTXT_CACHE: upstream's common.mk takes HIRESTXT_LIB as its own CoCo hirestxt switch. Rebuilt adam, apple2, atari, c64, coco and vic20 before and after: the programs are byte-identical apart from build timestamps. The Adam's r2r now holds nc.ddp (upstream adds -create-app) instead of nc.prg. Co-Authored-By: Claude Opus 5.5 --- hirestxt-mod-lib.mk | 12 +- mekkogx/README.md | 53 ++++++++- mekkogx/common.mk | 104 ++++++++++++++-- mekkogx/fnlib.py | 49 +++++--- mekkogx/hirestxtlib.py | 210 +++++++++++++++++++++++++++++++++ mekkogx/platforms/adam.mk | 5 +- mekkogx/platforms/adam_cpm.mk | 6 +- mekkogx/platforms/apple2.mk | 21 +++- mekkogx/platforms/atari-pas.mk | 2 +- mekkogx/platforms/atari.mk | 16 ++- mekkogx/platforms/atarixl.mk | 42 +++++++ mekkogx/platforms/c16.mk | 2 +- mekkogx/platforms/c64.mk | 2 +- mekkogx/platforms/coco.mk | 6 +- mekkogx/platforms/dragon.mk | 8 +- mekkogx/platforms/h89-cpm.mk | 7 +- mekkogx/platforms/h89-hdos.mk | 13 ++ mekkogx/platforms/lynx.mk | 9 ++ mekkogx/platforms/model2.mk | 46 ++++++++ mekkogx/platforms/msdos.mk | 6 +- mekkogx/platforms/msx.mk | 5 +- mekkogx/platforms/msxrom.mk | 5 +- mekkogx/platforms/pc6001.mk | 2 +- mekkogx/platforms/plus4.mk | 9 ++ mekkogx/platforms/pmd85.mk | 14 ++- mekkogx/platforms/rc2014.mk | 2 +- mekkogx/platforms/vic20.mk | 2 +- mekkogx/tc-common.mk | 19 ++- mekkogx/toolchains/cc65.mk | 7 +- mekkogx/toolchains/cmoc.mk | 7 +- mekkogx/toolchains/ow2.mk | 9 +- mekkogx/toolchains/z88dk.mk | 7 +- mekkogx/toplevel-rules.mk | 31 +++-- 33 files changed, 655 insertions(+), 83 deletions(-) create mode 100755 mekkogx/hirestxtlib.py create mode 100644 mekkogx/platforms/atarixl.mk create mode 100644 mekkogx/platforms/h89-hdos.mk create mode 100644 mekkogx/platforms/lynx.mk create mode 100644 mekkogx/platforms/model2.mk create mode 100644 mekkogx/platforms/plus4.mk diff --git a/hirestxt-mod-lib.mk b/hirestxt-mod-lib.mk index bcc0286..88cb2f2 100644 --- a/hirestxt-mod-lib.mk +++ b/hirestxt-mod-lib.mk @@ -1,23 +1,23 @@ HIRESTXT_LIB_VERSION := CURRENT_TARGET = coco -HIRESTXT_LIB = _cache +HIRESTXT_CACHE = _cache # A directory that already holds libhirestxt.a (e.g. a local hirestxt-mod # build) is used as is; anything else gets the release downloaded into it. -ifneq ($(wildcard $(HIRESTXT_LIB)/libhirestxt.a),) +ifneq ($(wildcard $(HIRESTXT_CACHE)/libhirestxt.a),) HIRESTXT_LIB_LOCAL = 1 -HIRESTXT_LIB_VERSION_DIR = $(HIRESTXT_LIB) +HIRESTXT_LIB_VERSION_DIR = $(HIRESTXT_CACHE) else # Blank version means the latest release. ifeq ($(strip $(HIRESTXT_LIB_VERSION)),) override HIRESTXT_LIB_VERSION := $(shell curl -s https://api.github.com/repos/RichStephens/hirestxt-mod/releases/latest | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p') endif -HIRESTXT_LIB_VERSION_DIR = $(HIRESTXT_LIB)/hirestxt-$(HIRESTXT_LIB_VERSION)-$(CURRENT_TARGET) +HIRESTXT_LIB_VERSION_DIR = $(HIRESTXT_CACHE)/hirestxt-$(HIRESTXT_LIB_VERSION)-$(CURRENT_TARGET) endif HIRESTXT_LIB_PATH = $(HIRESTXT_LIB_VERSION_DIR) HIRESTXT_LIB_DOWNLOAD_URL = https://github.com/RichStephens/hirestxt-mod/releases/download/$(HIRESTXT_LIB_VERSION)/hirestxt-mod-bin-$(HIRESTXT_LIB_VERSION).tar.gz -HIRESTXT_LIB_DOWNLOAD_FILE = $(HIRESTXT_LIB)/hirestxt-mod-bin-$(HIRESTXT_LIB_VERSION).tar.gz +HIRESTXT_LIB_DOWNLOAD_FILE = $(HIRESTXT_CACHE)/hirestxt-mod-bin-$(HIRESTXT_LIB_VERSION).tar.gz HIRESTXT_LIB_BASENAME := $(notdir $(HIRESTXT_LIB_PATH)) HIRESTXT_LIB_SYMLINK := libhirestxt.a @@ -38,7 +38,7 @@ ifndef HIRESTXT_LIB_LOCAL exit 1; \ fi; \ echo "Downloading hirestxt-mod for $(CURRENT_TARGET) version $(HIRESTXT_LIB_VERSION) from $(HIRESTXT_LIB_DOWNLOAD_URL)"; \ - mkdir -p $(HIRESTXT_LIB); \ + mkdir -p $(HIRESTXT_CACHE); \ mkdir -p $(HIRESTXT_LIB_VERSION_DIR); \ curl -sL $(HIRESTXT_LIB_DOWNLOAD_URL) -o $(HIRESTXT_LIB_DOWNLOAD_FILE); \ echo "Unzipping to $(HIRESTXT_LIB_VERSION_DIR)"; \ diff --git a/mekkogx/README.md b/mekkogx/README.md index 750444d..7f58c48 100644 --- a/mekkogx/README.md +++ b/mekkogx/README.md @@ -34,7 +34,8 @@ build, and the top-level Makefile defines *what* to build. The **project’s Makefile** is the only file you should need to edit when starting a new project. It contains: -- Your project’s `PRODUCT` name. +- Your project’s `PRODUCT` name, or `PRODUCTS` if several binaries + share one disk image. - The `PLATFORMS` you want to build for. - The `SRC_DIRS` containing your source code. - Optional project-specific rules or custom targets. @@ -157,6 +158,56 @@ With `SRC_DIRS = src src/%PLATFORM%`, building for `c64` would expand `%PLATFORM - `src/commodore` - `src/eightbit` +### Multiple binaries on one disk (`PRODUCTS`) + +`PRODUCT` names a single binary, and everything found in `SRC_DIRS` is +compiled into it. To build several binaries and put them all onto one +disk image, list them in `PRODUCTS` and name the shared image with +`PRODUCTS_DISK_NAME`: + +``` +PRODUCTS = hello world +PRODUCTS_DISK_NAME = games +``` + +Each product is built by its own pass with its own object directory, so +products can be compiled with different settings. Any variable prefixed +with a product name is appended to the matching variable while that +product is being built: + +``` +SRC_DIRS = src src/%PLATFORM% + +hello_SRC_DIRS = src/hello +hello_CFLAGS = -DGREETING=1 + +world_SRC_DIRS = src/world +world_CFLAGS = -DGREETING=2 +``` + +Notes: + +* Products are built in the order listed, so a library can be declared + ahead of the binaries that link against it. +* A product ending in `.lib` is built as a library and is *not* copied + onto the disk image. +* A single-product disk boots straight into that product. A + multi-product disk boots a selector instead, so any of the products + can be chosen: the ProDOS selector (Bitsy Bye) on Apple II, and the + MyPicoDos menu on Atari. Other platforms load programs by name and + are unaffected. +* On Apple II `BASIC.SYSTEM` is left off a multi-product disk. While it + is present ProDOS hands control to BASIC when a program quits; + without it control returns to the selector, so the next product can + be picked straight away. Set `DISK_BASIC_SYSTEM = BASIC.SYSTEM` to + put it back. +* Platforms that have no disk image simply build every binary into + `r2r//`. +* If a product needs a completely different set of sources rather than + extra ones, leave the shared `SRC_DIRS` unset and give every product + its own. +* Projects that use `PRODUCT` on its own are unaffected. + ### The `r2r` "Ready 2 Run" Target The `r2r` target is the **default build output** for a platform. It diff --git a/mekkogx/common.mk b/mekkogx/common.mk index d252f95..9039dec 100644 --- a/mekkogx/common.mk +++ b/mekkogx/common.mk @@ -22,15 +22,37 @@ PLATFORM := $(basename $(notdir $(lastword $(PLATFORM_MK)))) PLATFORM_UC := $(shell echo "$(PLATFORM)" | tr '[:lower:]' '[:upper:]') $(info Building for PLATFORM=$(PLATFORM)) -include $(CURDIR)/Makefile +MEKKO_CONFIG ?= Makefile +$(info MEKKO_CONFIG=$(MEKKO_CONFIG)) +include $(CURDIR)/$(MEKKO_CONFIG) # Define GIT_VERSION to be used in macro define to CFLAGS, includes # tag if available, short commit hash, appends '*' if changes haven't # been commited GIT_VERSION := $(shell git rev-parse --short HEAD)$(shell git status --porcelain | grep -q '^[ MADRCU]' && echo '*') +# A pass with MEKKO_PRODUCT set builds that one product, without it the +# pass builds the disk from all of PRODUCTS +ifeq ($(strip $(MEKKO_PRODUCT)),) + MEKKO_MULTI := $(if $(strip $(PRODUCTS)),1,) +else + PRODUCT := $(MEKKO_PRODUCT) +endif + +# _CFLAGS is appended to CFLAGS, and so on for any variable +ifneq ($(MEKKO_MULTI),1) + ifneq ($(strip $(PRODUCT)),) + $(foreach v,$(filter $(PRODUCT)_%,$(.VARIABLES)), \ + $(eval $(patsubst $(PRODUCT)_%,%,$(v)) += $(value $(v)))) + endif +endif + IS_LIBRARY := $(if $(filter %.lib,$(PRODUCT)),1,0) -ifeq ($(IS_LIBRARY),1) +ifeq ($(MEKKO_MULTI),1) + PRODUCT := $(PRODUCTS_DISK_NAME) + PRODUCT_BASE = $(PRODUCT) + BUILD_DISK = $(DISK) +else ifeq ($(IS_LIBRARY),1) PRODUCT_BASE = $(basename $(PRODUCT)) BUILD_LIB = $(LIBRARY) else @@ -57,16 +79,34 @@ PC_DEFAULT = $(PC_$(TOOLCHAIN_UC)) endif R2R_PD := $(R2R_DIR)/$(PLATFORM) -OBJ_DIR := $(BUILD_DIR)/$(PLATFORM) +OBJ_DIR := $(BUILD_DIR)/$(PRODUCT)/$(PLATFORM) CACHE_PLATFORM := $(CACHE_DIR)/$(PLATFORM) MKDIR_P ?= mkdir -p +# Lets the disk pass name executables it did not build itself +EXECUTABLE ?= $(R2R_PD)/$(PRODUCT_BASE)$(EXEC_SUFFIX) + +# Library products are built but not copied onto the disk +ifeq ($(MEKKO_MULTI),1) + DISK_EXECUTABLES ?= $(foreach p,$(filter-out %.lib,$(PRODUCTS)),$(R2R_PD)/$(p)$(EXEC_SUFFIX)) +else + DISK_EXECUTABLES ?= $(BUILD_EXEC) +endif + +DISK_BOOT_EXEC ?= $(firstword $(DISK_EXECUTABLES)) + # Expand PLATFORM_COMBOS entries into a lookup form # c64+=commodore,eightbit -> c64 commodore eightbit # PLATFORM_COMBOS is a flat list of entries like "dragon+=coco" # $1 = the platform to expand -get_combos = $(foreach e,$(PLATFORM_COMBOS),\ - $(if $(filter $1+=%, $(e)), $(lastword $(subst +=, ,$(e))))) +comma := , +define get_combos +$(foreach e,$(PLATFORM_COMBOS), \ + $(if $(filter $1+=%,$(e)), \ + $(subst $(comma), ,$(lastword $(subst +=, ,$(e)))) \ + ) \ +) +endef # Expands patterns with %PLATFORM% to the platform + its combos expand_platform_pattern = \ @@ -146,15 +186,35 @@ vpath %.s $(SRC_DIRS_EXPANDED) vpath %.asm $(SRC_DIRS_EXPANDED) vpath %.pas $(SRC_DIRS_EXPANDED) -.PHONY: clean debug r2r $(PLATFORM)/r2r disk $(PLATFORM)/disk +.PHONY: clean debug r2r $(PLATFORM)/r2r disk $(PLATFORM)/disk $(PLATFORM)/release clean:: - rm -rf $(OBJ_DIR) $(CACHE_PLATFORM) $(R2R_PD) + rm -rf $(OBJ_DIR) $(CACHE_PLATFORM) $(R2R_PD) $(RELEASE_DIR) debug:: echo 'What should debug target do?' exit 1 +.PHONY: product products +product: $(BUILD_EXEC) $(BUILD_LIB) + +# Built in the order PRODUCTS lists them so a library can come first +products: + @for p in $(PRODUCTS); do \ + $(MAKE) -f $(PLATFORM_MK) MEKKO_CONFIG=$(MEKKO_CONFIG) MEKKO_PRODUCT=$$p product \ + || exit 1; \ + done + +ifeq ($(MEKKO_MULTI),1) +r2r:: products +ifneq ($(strip $(BUILD_DISK)),) +$(BUILD_DISK): products +endif + +clean:: + rm -rf $(foreach p,$(PRODUCTS),$(BUILD_DIR)/$(p)/$(PLATFORM)) +endif + # These targets allow adding platform-specific steps from the top-level Makefile. # Examples: # coco/r2r:: coco/custom-step1 @@ -164,6 +224,7 @@ debug:: # The double-colon form appends without overwriting existing deps. r2r:: $(PLATFORM)/r2r disk:: $(PLATFORM)/disk +release:: $(PLATFORM)/release # Fallback rule so every /disk-post target exists. # Does nothing by default (@:). @@ -185,6 +246,21 @@ $(PLATFORM)/executable-post:: $(PLATFORM)/library-post:: @: +RELEASE_INCLUDES ?= $(foreach dir,$(INCLUDE_DIRS),$(wildcard $(dir)/*.h $(dir)/*.inc)) +RELEASE_INCLUDES += $(wildcard Changelog.md) +RELEASE_VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null || echo dev) +PRODUCT_DASH = $(subst .,-,$(PRODUCT)) +VERSION_CLEAN = $(patsubst v%,%,$(RELEASE_VERSION)) +RELEASE_ZIP = $(RELEASE_DIR)/$(PRODUCT_DASH)-$(PLATFORM)-$(VERSION_CLEAN).zip +RELEASE_LIB = $(RELEASE_DIR)/$(PRODUCT_BASE)-$(PLATFORM)-$(VERSION_CLEAN).lib + +$(PLATFORM)/release: $(RELEASE_ZIP) +$(RELEASE_ZIP):: $(LIBRARY) + $(MKDIR_P) $(RELEASE_DIR) + cp $(BUILD_LIB) $(RELEASE_LIB) + zip -j $@ $(RELEASE_INCLUDES) $(RELEASE_LIB) + rm $(RELEASE_LIB) + # include autodeps DEPS := $(OBJS:.o=.d) -include $(DEPS) @@ -206,9 +282,21 @@ else $(FUJINET_LIB) | tr '\n' '|'))) ifeq ($(strip $(FUJINET_LIB_LDLIB)),) ifeq ($(FUJINET_LIB_OPTIONAL),) - $(error fujinet-lib not available) + $(error fujinet-lib not available for $(PLATFORM)) else $(info fujinet-lib not available, but skipping because FUJINET_LIB_SKIP_MISSING is set) endif endif endif # FUJINET_LIB + +HIRESTXT_LIB ?= __UNDEFINED__ +ifeq ($(HIRESTXT_LIB),__UNDEFINED__) + $(info HIRESTXT_LIB not defined) +else + ifneq ($(filter coco dragon,$(PLATFORM)),) + $(eval $(subst |,$(_newline),$(shell PLATFORM=$(PLATFORM) CACHE_DIR=$(CACHE_DIR) \ + $(MWD)/hirestxtlib.py $(HIRESTXT_LIB) | tr '\n' '|'))) + else + $(info HIRESTXT_LIB ignored for PLATFORM=$(PLATFORM) (coco/dragon only)) + endif +endif # HIRESTXT_LIB diff --git a/mekkogx/fnlib.py b/mekkogx/fnlib.py index beab523..edbabe1 100755 --- a/mekkogx/fnlib.py +++ b/mekkogx/fnlib.py @@ -118,9 +118,11 @@ def __init__(self, FUJINET_LIB, PLATFORM, COMBOS, skipIfMissing=False): if not self.MV.FUJINET_LIB_DIR: self.getDirectory() - if not self.MV.FUJINET_LIB_FILE \ - and (not self.MV.FUJINET_LIB_ZIP or not os.path.exists(self.MV.FUJINET_LIB_ZIP)): - self.downloadZip() + if not self.MV.FUJINET_LIB_FILE: + if not self.MV.FUJINET_LIB_ZIP or not os.path.exists(self.MV.FUJINET_LIB_ZIP): + self.downloadZip() + with zipfile.ZipFile(self.MV.FUJINET_LIB_ZIP, "r") as zf: + zf.extractall(self.MV.FUJINET_LIB_DIR) if not self.MV.FUJINET_LIB_INCLUDE: self.getInclude() @@ -208,7 +210,7 @@ def getVersion(self): exit(0) #error_exit(f"No library found for \"{self.PLATFORM}\"") if not self.MV.FUJINET_LIB_FILE: - raise ValueError + raise ValueError("No library found for", self.PLATFORM) return # No version was specified, so any version is fine @@ -267,9 +269,6 @@ def downloadZip(self): except: continue - with zipfile.ZipFile(self.MV.FUJINET_LIB_ZIP, "r") as zf: - zf.extractall(self.MV.FUJINET_LIB_DIR) - return #error_exit("Unable to download FujiNet library from", release_url) @@ -294,14 +293,24 @@ def gitClone(self, url): self.findLibraryDir(repoDir) if not self.MV.FUJINET_LIB_FILE: - cmd = ["make", ] - subprocess.run(cmd, cwd=repoDir, check=True, stdout=sys.stderr) + with open(os.path.join(repoDir, "Makefile")) as _mf: + _mk = _mf.read() + repo_platforms = " ".join(re.findall(r'^PLATFORMS\s*[?:+]?=\s*(.+)', _mk, re.MULTILINE)).split() + build_platform = next((p for p in self.possiblePlatforms if p in repo_platforms), self.PLATFORM) + if re.search(r'^TARGETS\s*[?:+]?=', _mk, re.MULTILINE): + cmd = ["make", f"TARGETS={build_platform}"] + else: + cmd = ["make", f"{build_platform}/r2r"] + clean_env = {k: v for k, v in os.environ.items() if k != 'FUJINET_LIB'} + clean_env['MAKEFLAGS'] = re.sub(r'\bFUJINET_LIB=\S*', '', clean_env.get('MAKEFLAGS', '')).strip() + subprocess.run(cmd, cwd=repoDir, check=True, stdout=sys.stderr, env=clean_env) self.findLibraryDir(repoDir) return def findLibraryDir(self, baseDir): - dirsToCheck = ["", "build", *[f"r2r/{p}" for p in self.possiblePlatforms]] + dirsToCheck = ["", "build", *[f"r2r/{p}" for p in self.possiblePlatforms], + *[f"build/{p}" for p in self.possiblePlatforms]] for pdir in dirsToCheck: pdir = os.path.join(baseDir, pdir) if os.path.isdir(pdir): @@ -315,10 +324,12 @@ def findLibraryDir(self, baseDir): return None def getInclude(self): - parent = os.path.dirname(self.MV.FUJINET_LIB_DIR.rstrip("/")) + parent = self.MV.FUJINET_LIB_DIR.rstrip("/") + if '/' in parent: + parent = os.path.dirname(parent) checkDirs = [self.MV.FUJINET_LIB_DIR, parent, os.path.join(parent, "include")] components = self.MV.FUJINET_LIB_DIR.split(os.path.sep) - if components[-1] == self.MV.FUJINET_LIB_PLATFORM and components[-2] == "r2r": + if components[-1] == self.MV.FUJINET_LIB_PLATFORM and components[-2] in ("r2r", "build"): checkDirs.append(os.path.join(os.path.dirname(parent), "include")) for idir in checkDirs: if os.path.exists(os.path.join(idir, "fujinet-fuji.h")): @@ -331,8 +342,12 @@ def getInclude(self): def printMakeVariables(self): self.MV.printValues() + flags = {} if self.MV.FUJINET_LIB_LDLIB: - print(f"CFLAGS_EXTRA_{self.PLATFORM.upper()}+=-DUSING_FUJINET_LIB") + flags['USING_FUJINET_LIB'] = 1 + if flags: + fmt_flags = " ".join(f"-D{name}={val}" for name, val in flags.items()) + print(f"CFLAGS_EXTRA_{self.PLATFORM.upper()}+={fmt_flags}") return @staticmethod @@ -358,7 +373,7 @@ def error_exit(*args): exit(1) def main(): - global CACHE_DIR, FUJINET_CACHE_DIR + global CACHE_DIR, FUJINET_CACHE_DIR, FUJINET_REPO args = build_argparser().parse_args() @@ -375,12 +390,18 @@ def main(): if not FUJINET_LIB: FUJINET_LIB = os.getenv("FUJINET_LIB") + repo = os.getenv("FUJINET_REPO") + if repo: + FUJINET_REPO = repo + env_cache_dir = os.getenv("CACHE_DIR") if env_cache_dir: CACHE_DIR = env_cache_dir FUJINET_CACHE_DIR = os.path.join(CACHE_DIR, os.path.basename(FUJINET_CACHE_DIR)) fujinetLib = LibLocator(FUJINET_LIB, PLATFORM, PLATFORM_COMBOS, args.skip_if_missing) + if fujinetLib.MV.FUJINET_LIB_VERSION: + fujinetLib.MV.FUJINET_LIB_VERSION = "v" + fujinetLib.MV.FUJINET_LIB_VERSION fujinetLib.printMakeVariables() return diff --git a/mekkogx/hirestxtlib.py b/mekkogx/hirestxtlib.py new file mode 100755 index 0000000..33ae520 --- /dev/null +++ b/mekkogx/hirestxtlib.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +import argparse +import os, sys +import re +import tarfile +import urllib.request +import json +import subprocess + +sys.path.insert(0, os.path.dirname(__file__)) +from fnlib import MakeVariables, error_exit + +HIRESTXT_REPO = "RichStephens/hirestxt-mod" +GITHUB_API = "https://api.github.com/repos" +GITHUB_URL = "https://github.com" +CACHE_DIR = "_cache" +HIRESTXT_CACHE_DIR = os.path.join(CACHE_DIR, "hirestxt-mod") + +VERSION_NUM_RE = r"([0-9]+(?:[.][0-9]+)+)" +VERSION_NAME_RE = fr"v?{VERSION_NUM_RE}" + +LIBRARY_FILE = "libhirestxt.a" +HEADER_FILE = "hirestxt.h" + +VT52_SUFFIX = "+vt52" +# First release that ships a separate no-VT52 archive +NOVT52_MIN_VERSION = (0, 5, 1, 7) + + +def build_argparser(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("file", nargs="?", help="input file") + parser.add_argument("--platform", help="platform building for") + return parser + + +class HirestxtLocator: + def __init__(self, HIRESTXT_LIB): + self.MV = MakeVariables([ + "HIRESTXT_LIB_DIR", + "HIRESTXT_LIB_INCLUDE", + "HIRESTXT_LIB_LDLIB", + "HIRESTXT_LIB_VERSION", + "HIRESTXT_LIB_NO_VT52", + ]) + + self.version = "" + self.vt52 = False + + if HIRESTXT_LIB in ("vt52", VT52_SUFFIX): + self.vt52 = True + HIRESTXT_LIB = "" + + if HIRESTXT_LIB: + rxm = re.fullmatch(fr"{VERSION_NAME_RE}({re.escape(VT52_SUFFIX)})?", HIRESTXT_LIB) + if rxm: + self.version = rxm.group(1) + self.vt52 = bool(rxm.group(2)) + elif any(sub in HIRESTXT_LIB for sub in ("://", "@")): + self.gitClone(HIRESTXT_LIB) + elif os.path.isdir(HIRESTXT_LIB): + self.findLibrary(HIRESTXT_LIB) + if not self.MV.HIRESTXT_LIB_DIR: + error_exit(f"\"{HIRESTXT_LIB}\" does not appear to contain {LIBRARY_FILE}") + else: + error_exit(f"Unrecognized HIRESTXT_LIB \"{HIRESTXT_LIB}\"") + + if not self.version and not self.MV.HIRESTXT_LIB_DIR: + self.getLatestVersion() + + if self.version and not self.MV.HIRESTXT_LIB_DIR: + self.downloadRelease() + + if not self.MV.HIRESTXT_LIB_INCLUDE: + self.getInclude() + + if self.MV.HIRESTXT_LIB_DIR: + self.MV.HIRESTXT_LIB_LDLIB = "hirestxt" + + self.MV.HIRESTXT_LIB_VERSION = self.version + + def findLibrary(self, baseDir): + for subdir in ["", "build"]: + path = os.path.join(baseDir, subdir) + if os.path.isdir(path) and os.path.exists(os.path.join(path, LIBRARY_FILE)): + self.MV.HIRESTXT_LIB_DIR = path + return True + return False + + def getLatestVersion(self): + latest_url = f"{GITHUB_API}/{HIRESTXT_REPO}/releases/latest" + with urllib.request.urlopen(latest_url) as response: + release_info = json.loads(response.read().decode("UTF-8")) + + latest_version = release_info.get("tag_name") or release_info.get("name") + if not latest_version: + error_exit("Can't find latest hirestxt-mod version") + + rxm = re.match(VERSION_NAME_RE, latest_version) + if not rxm: + error_exit("Not a valid hirestxt-mod version:", latest_version) + + self.version = rxm.group(1) + + def downloadRelease(self): + global HIRESTXT_CACHE_DIR + versionTuple = tuple(int(x) for x in self.version.split(".")) + variant = "" + if not self.vt52 and versionTuple >= NOVT52_MIN_VERSION: + variant = "novt52-" + self.MV.HIRESTXT_LIB_NO_VT52 = "1" + + versionDir = os.path.join(HIRESTXT_CACHE_DIR, variant + self.version) + + if os.path.exists(os.path.join(versionDir, LIBRARY_FILE)): + self.MV.HIRESTXT_LIB_DIR = versionDir + return + + os.makedirs(HIRESTXT_CACHE_DIR, exist_ok=True) + + tarball_name = f"hirestxt-mod-bin-{variant}{self.version}.tar.gz" + tarball_path = os.path.join(HIRESTXT_CACHE_DIR, tarball_name) + + if not os.path.exists(tarball_path): + release_url = f"{GITHUB_URL}/{HIRESTXT_REPO}/releases/download" \ + f"/{self.version}/{tarball_name}" + try: + urllib.request.urlretrieve(release_url, tarball_path) + except Exception as e: + error_exit(f"Unable to download hirestxt-mod {self.version}:", e) + + os.makedirs(versionDir, exist_ok=True) + with tarfile.open(tarball_path, "r:gz") as tf: + tf.extractall(versionDir) + + self.MV.HIRESTXT_LIB_DIR = versionDir + + def gitClone(self, url): + global HIRESTXT_CACHE_DIR + os.makedirs(HIRESTXT_CACHE_DIR, exist_ok=True) + branch = "" + if "#" in url: + url, branch = url.split("#") + base = url.rstrip("/").split("/")[-1] + if base.endswith(".git"): + base = base.rsplit(".", 1)[0] + repoDir = os.path.join(HIRESTXT_CACHE_DIR, base) + + if not os.path.exists(repoDir): + cmd = ["git", "clone", url] + if branch: + cmd.extend(["-b", branch]) + subprocess.run(cmd, cwd=HIRESTXT_CACHE_DIR, check=True, stdout=sys.stderr) + + if not self.findLibrary(repoDir): + clean_env = {k: v for k, v in os.environ.items() if k != 'HIRESTXT_LIB'} + clean_env['MAKEFLAGS'] = re.sub( + r'\bHIRESTXT_LIB=\S*', '', clean_env.get('MAKEFLAGS', '') + ).strip() + subprocess.run( + ["make", LIBRARY_FILE], cwd=repoDir, check=True, stdout=sys.stderr, env=clean_env + ) + self.findLibrary(repoDir) + + def getInclude(self): + if not self.MV.HIRESTXT_LIB_DIR: + return + checkDirs = [self.MV.HIRESTXT_LIB_DIR, + os.path.dirname(self.MV.HIRESTXT_LIB_DIR.rstrip("/")), + os.path.join(self.MV.HIRESTXT_LIB_DIR, "include")] + for idir in checkDirs: + if os.path.exists(os.path.join(idir, HEADER_FILE)): + self.MV.HIRESTXT_LIB_INCLUDE = idir + return + error_exit("Unable to find hirestxt.h in", self.MV.HIRESTXT_LIB_DIR) + + def printMakeVariables(self): + self.MV.printValues() + + +def main(): + global CACHE_DIR, HIRESTXT_CACHE_DIR + + args = build_argparser().parse_args() + + PLATFORM = os.getenv("PLATFORM") + if args.platform: + PLATFORM = args.platform + + if not PLATFORM: + error_exit("Please specify PLATFORM") + + if PLATFORM not in ("coco", "dragon"): + error_exit("hirestxt-mod is only available for coco/dragon, not", PLATFORM) + + HIRESTXT_LIB = args.file + if not HIRESTXT_LIB: + HIRESTXT_LIB = os.getenv("HIRESTXT_LIB") + + env_cache_dir = os.getenv("CACHE_DIR") + if env_cache_dir: + CACHE_DIR = env_cache_dir + HIRESTXT_CACHE_DIR = os.path.join(CACHE_DIR, os.path.basename(HIRESTXT_CACHE_DIR)) + + locator = HirestxtLocator(HIRESTXT_LIB) + locator.printMakeVariables() + + +if __name__ == "__main__": + exit(main() or 0) diff --git a/mekkogx/platforms/adam.mk b/mekkogx/platforms/adam.mk index 4fb7fd5..9b24a38 100644 --- a/mekkogx/platforms/adam.mk +++ b/mekkogx/platforms/adam.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).prg +EXEC_SUFFIX = .ddp LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) @@ -9,6 +9,9 @@ ADAM_FLAGS = +coleco -subtype=adam CFLAGS += $(ADAM_FLAGS) LDFLAGS += $(ADAM_FLAGS) LIBS += -leos.lib -lsmartkeys.lib +ifneq ($(IS_LIBRARY),1) + LDFLAGS += -create-app +endif r2r:: $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS) make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post diff --git a/mekkogx/platforms/adam_cpm.mk b/mekkogx/platforms/adam_cpm.mk index d72b0cc..7d8eaa3 100644 --- a/mekkogx/platforms/adam_cpm.mk +++ b/mekkogx/platforms/adam_cpm.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).com +EXEC_SUFFIX = .com DISK = $(R2R_PD)/$(PRODUCT_BASE).dsk LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib DISK_TOOL = cpmcp @@ -19,10 +19,10 @@ r2r:: $(BUILD_DISK) $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS) CPM_BLANK = blank160-bootable.dsk CPM_DISK = $(CACHE_PLATFORM)/$(CPM_BLANK) -$(BUILD_DISK): $(BUILD_EXEC) $(CPM_DISK) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) +$(BUILD_DISK): $(DISK_EXECUTABLES) $(CPM_DISK) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) $(call require,$(DISK_TOOL),$(DISK_TOOL_INFO)) cp $(CPM_DISK) $@ - $(foreach f,$< $(DISK_EXTRA_FILES),$(call copy-to-disk,,$(f),0:,$@);) + $(foreach f,$(DISK_EXECUTABLES) $(DISK_EXTRA_FILES),$(call copy-to-disk,,$(f),0:,$@);) make -f $(PLATFORM_MK) $(PLATFORM)/disk-post CPM_DISK_URL = https://retrobattlestations.com/Coleco-Adam diff --git a/mekkogx/platforms/apple2.mk b/mekkogx/platforms/apple2.mk index cf7fb4a..b838d24 100644 --- a/mekkogx/platforms/apple2.mk +++ b/mekkogx/platforms/apple2.mk @@ -1,9 +1,10 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).a2s +EXEC_SUFFIX = .a2s DISK = $(R2R_PD)/$(PRODUCT_BASE).po LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib DISK_TOOL = ac DISK_TOOL_X = acx DISK_TOOL_INFO = https://github.com/AppleCommander/AppleCommander/releases/ +DISK_SIZE ?= 140kb MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) include $(MWD)/common.mk @@ -14,15 +15,25 @@ r2r:: $(BUILD_DISK) $(BUILD_LIB) $(R2R_EXTRA_DEPS) PRODOS_VERSION = 2.4.3 PRODOS8_DISK ?= $(CACHE_PLATFORM)/PRODOS8-$(PRODOS_VERSION).po + +# ProDOS boots the first .SYSTEM file, so a multi product disk leads with +# the selector (Bitsy Bye on 2.4.x) to let any product be chosen +ifeq ($(MEKKO_MULTI),1) + DISK_BOOT_SYSTEM ?= QUIT.SYSTEM +endif +DISK_BASIC_SYSTEM ?= BASIC.SYSTEM CC65_UTILS_DIR := $(shell cl65 --print-target-path --target $(PLATFORM))/$(PLATFORM)/util LOADER_SYSTEM := loader.system -$(BUILD_DISK): $(BUILD_EXEC) $(PRODOS8_DISK) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) +$(BUILD_DISK): $(DISK_EXECUTABLES) $(PRODOS8_DISK) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) $(call require,$(DISK_TOOL),$(DISK_TOOL_INFO)) $(call require,$(DISK_TOOL_X),$(DISK_TOOL_INFO)) - $(DISK_TOOL_X) create -d $@ --format $(PRODOS8_DISK) --prodos --size=140kb --name=$(PRODUCT_BASE) - $(call copy-to-disk,-as,$<,$(PRODUCT_BASE),$@) - $(call copy-to-disk,-p,$(CC65_UTILS_DIR)/$(LOADER_SYSTEM),$(PRODUCT_BASE).SYSTEM SYS 0x2000,$@) + $(DISK_TOOL_X) create -d $@ --format $(PRODOS8_DISK) --prodos --size=$(DISK_SIZE) --name=$(PRODUCT_BASE) + $(DISK_TOOL_X) export --as -d $@ BASIC.SYSTEM > $(CACHE_PLATFORM)/BASIC.SYSTEM + $(DISK_TOOL) -d $@ BASIC.SYSTEM + $(if $(DISK_BOOT_SYSTEM),$(DISK_TOOL_X) export --as -d $(PRODOS8_DISK) $(DISK_BOOT_SYSTEM) > $(CACHE_PLATFORM)/$(DISK_BOOT_SYSTEM); $(call copy-to-disk,-as,$(CACHE_PLATFORM)/$(DISK_BOOT_SYSTEM),$(DISK_BOOT_SYSTEM),$@);) + $(foreach e,$(DISK_EXECUTABLES),$(call copy-to-disk,-as,$(e),$(basename $(notdir $(e))),$@); $(call copy-to-disk,-p,$(CC65_UTILS_DIR)/$(LOADER_SYSTEM),$(basename $(notdir $(e))).SYSTEM SYS 0x2000,$@);) + $(if $(DISK_BASIC_SYSTEM),$(call copy-to-disk,-as,$(CACHE_PLATFORM)/BASIC.SYSTEM,$(DISK_BASIC_SYSTEM),$@);) $(foreach f,$(DISK_EXTRA_FILES),$(call copy-to-disk,-ptx,$(f),$(notdir $(f)),$@);) make -f $(PLATFORM_MK) $(PLATFORM)/disk-post diff --git a/mekkogx/platforms/atari-pas.mk b/mekkogx/platforms/atari-pas.mk index 7e8158d..6cadbfe 100644 --- a/mekkogx/platforms/atari-pas.mk +++ b/mekkogx/platforms/atari-pas.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).xex +EXEC_SUFFIX = .xex LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) diff --git a/mekkogx/platforms/atari.mk b/mekkogx/platforms/atari.mk index a7b7fc1..c884bf0 100644 --- a/mekkogx/platforms/atari.mk +++ b/mekkogx/platforms/atari.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).com +EXEC_SUFFIX = .com DISK = $(R2R_PD)/$(PRODUCT_BASE).atr LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib DISK_TOOL = dir2atr @@ -15,13 +15,21 @@ r2r:: $(BUILD_DISK) $(BUILD_LIB) $(R2R_EXTRA_DEPS) PICOBOOT_BIN = picoboot.bin ATRBOOT := $(CACHE_PLATFORM)/$(PICOBOOT_BIN) -DISK_TOOL_FLAGS_ATARI ?= -S -m -B $(ATRBOOT) -$(BUILD_DISK): $(BUILD_EXEC) $(ATRBOOT) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) +# picoboot runs a single program, so multiple products boot a menu +ATARI_MENU_DOS ?= MyPicoDos406 +ifeq ($(MEKKO_MULTI),1) + DISK_TOOL_FLAGS_ATARI ?= -S -m -b $(ATARI_MENU_DOS) +else + DISK_TOOL_FLAGS_ATARI ?= -S -m -B $(ATRBOOT) +endif + +$(BUILD_DISK): $(DISK_EXECUTABLES) $(ATRBOOT) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) $(RM) $@ $(RM) -rf $(CACHE_PLATFORM)/disk $(MKDIR_P) $(CACHE_PLATFORM)/disk - cp $< $(CACHE_PLATFORM)/disk/$(EXEC_NAME_ATARI) + cp $(DISK_BOOT_EXEC) $(CACHE_PLATFORM)/disk/$(EXEC_NAME_ATARI) + $(foreach e,$(filter-out $(DISK_BOOT_EXEC),$(DISK_EXECUTABLES)),cp $(e) $(CACHE_PLATFORM)/disk ; ) $(call require,$(DISK_TOOL),$(DISK_TOOL_INFO)) $(call require,$(DISK_TOOL_COPY),$(DISK_TOOL_COPY_INFO)) $(foreach f,$(DISK_EXTRA_FILES),cp $(f) $(CACHE_PLATFORM)/disk ; ) diff --git a/mekkogx/platforms/atarixl.mk b/mekkogx/platforms/atarixl.mk new file mode 100644 index 0000000..a7b7fc1 --- /dev/null +++ b/mekkogx/platforms/atarixl.mk @@ -0,0 +1,42 @@ +EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).com +DISK = $(R2R_PD)/$(PRODUCT_BASE).atr +LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib +DISK_TOOL = dir2atr +DISK_TOOL_INFO = https://www.horus.com/~hias/atari/\#atarisio (Linux) or https://github.com/EricCarrGH/atari-dist/releases/ (Mac) +DISK_TOOL_COPY = atr +DISK_TOOL_COPY_INFO = https://github.com/jhallen/atari-tools + +MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) +include $(MWD)/common.mk +include $(MWD)/toolchains/cc65.mk + +r2r:: $(BUILD_DISK) $(BUILD_LIB) $(R2R_EXTRA_DEPS) + make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post + +PICOBOOT_BIN = picoboot.bin +ATRBOOT := $(CACHE_PLATFORM)/$(PICOBOOT_BIN) +DISK_TOOL_FLAGS_ATARI ?= -S -m -B $(ATRBOOT) + +$(BUILD_DISK): $(BUILD_EXEC) $(ATRBOOT) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) + $(RM) $@ + $(RM) -rf $(CACHE_PLATFORM)/disk + $(MKDIR_P) $(CACHE_PLATFORM)/disk + cp $< $(CACHE_PLATFORM)/disk/$(EXEC_NAME_ATARI) + $(call require,$(DISK_TOOL),$(DISK_TOOL_INFO)) + $(call require,$(DISK_TOOL_COPY),$(DISK_TOOL_COPY_INFO)) + $(foreach f,$(DISK_EXTRA_FILES),cp $(f) $(CACHE_PLATFORM)/disk ; ) + $(DISK_TOOL) $(DISK_TOOL_FLAGS_ATARI) $@ $(CACHE_PLATFORM)/disk + make -f $(PLATFORM_MK) $(PLATFORM)/disk-post + +PICOBOOT_DOWNLOAD_URL = https://github.com/FujiNetWIFI/assets/releases/download/picobin +$(ATRBOOT): | $(CACHE_PLATFORM) + curl -L -o $@ $(PICOBOOT_DOWNLOAD_URL)/$(PICOBOOT_BIN) + +# Arguments: +# $1 == atr flags +# $2 == source file +# $3 == destination name +# $4 == disk image +define copy-to-disk + atr $4 put $1 $2 $3 +endef diff --git a/mekkogx/platforms/c16.mk b/mekkogx/platforms/c16.mk index 5ed117d..867bc5d 100644 --- a/mekkogx/platforms/c16.mk +++ b/mekkogx/platforms/c16.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).prg +EXEC_SUFFIX = .prg LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) diff --git a/mekkogx/platforms/c64.mk b/mekkogx/platforms/c64.mk index 5ed117d..867bc5d 100644 --- a/mekkogx/platforms/c64.mk +++ b/mekkogx/platforms/c64.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).prg +EXEC_SUFFIX = .prg LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) diff --git a/mekkogx/platforms/coco.mk b/mekkogx/platforms/coco.mk index 08a721d..5f4a633 100644 --- a/mekkogx/platforms/coco.mk +++ b/mekkogx/platforms/coco.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).bin +EXEC_SUFFIX = .bin DISK = $(R2R_PD)/$(PRODUCT_BASE).dsk LIBRARY = $(R2R_PD)/lib$(PRODUCT_BASE).$(PLATFORM).a DISK_TOOL = decb @@ -11,11 +11,11 @@ include $(MWD)/toolchains/cmoc.mk r2r:: $(BUILD_DISK) $(BUILD_LIB) $(R2R_EXTRA_DEPS) @make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post -$(BUILD_DISK): $(BUILD_EXEC) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) +$(BUILD_DISK): $(DISK_EXECUTABLES) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) $(RM) $@ $(call require,$(DISK_TOOL),$(DISK_TOOL_INFO)) $(DISK_TOOL) dskini $@ - $(call copy-to-disk,-b -2,$<,$(shell echo $(notdir $<) | tr '[:lower:]' '[:upper:]'),$@) + $(foreach e,$(DISK_EXECUTABLES),$(call copy-to-disk,-b -2,$(e),$(shell echo $(notdir $(e)) | tr '[:lower:]' '[:upper:]'),$@);) $(foreach f,$(DISK_EXTRA_FILES),$(call copy-to-disk,-b,$(f),$(shell echo $(notdir $(f)) | tr '[:lower:]' '[:upper:]'),$@);) @make -f $(PLATFORM_MK) $(PLATFORM)/disk-post diff --git a/mekkogx/platforms/dragon.mk b/mekkogx/platforms/dragon.mk index 0089265..ba1ab1e 100644 --- a/mekkogx/platforms/dragon.mk +++ b/mekkogx/platforms/dragon.mk @@ -1,8 +1,8 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).bin +EXEC_SUFFIX = .bin DISK = $(R2R_PD)/$(PRODUCT_BASE).vdk LIBRARY = $(R2R_PD)/lib$(PRODUCT_BASE).$(PLATFORM).a DISK_TOOL = dragondos -DISK_TOOL_INFO = https://www.rolfmichelsen.com/dragontools/ +DISK_TOOL_INFO = https://github.com/robcfg/retrotools MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) include $(MWD)/common.mk @@ -11,11 +11,11 @@ include $(MWD)/toolchains/cmoc.mk r2r:: $(BUILD_DISK) $(BUILD_LIB) $(R2R_EXTRA_DEPS) @make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post -$(BUILD_DISK): $(BUILD_EXEC) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) +$(BUILD_DISK): $(DISK_EXECUTABLES) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) $(RM) $@ $(call require,$(DISK_TOOL),$(DISK_TOOL_INFO)) $(DISK_TOOL) new $@ 360 - $(call copy-to-disk,0x2601 0x2601,$<,,$@) + $(foreach e,$(DISK_EXECUTABLES),$(call copy-to-disk,0x2601 0x2601,$(e),,$@);) $(foreach f,$(DISK_EXTRA_FILES),$(call copy-to-disk,,$(f),,$@);) @make -f $(PLATFORM_MK) $(PLATFORM)/disk-post diff --git a/mekkogx/platforms/h89-cpm.mk b/mekkogx/platforms/h89-cpm.mk index 23aa45d..e0fc60c 100644 --- a/mekkogx/platforms/h89-cpm.mk +++ b/mekkogx/platforms/h89-cpm.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).com +EXEC_SUFFIX = .com LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) @@ -6,8 +6,11 @@ include $(MWD)/common.mk include $(MWD)/toolchains/z88dk.mk MSX_FLAGS = +cpm -create-app -CFLAGS += $(MSX_FLAGS) +CFLAGS += $(MSX_FLAGS) -D__H89__ LDFLAGS += $(MSX_FLAGS) +ifneq ($(IS_LIBRARY),1) + LDFLAGS += -create-app +endif r2r:: $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS) make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post diff --git a/mekkogx/platforms/h89-hdos.mk b/mekkogx/platforms/h89-hdos.mk new file mode 100644 index 0000000..6e070b7 --- /dev/null +++ b/mekkogx/platforms/h89-hdos.mk @@ -0,0 +1,13 @@ +EXEC_SUFFIX = .abs +LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib + +MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) +include $(MWD)/common.mk +include $(MWD)/toolchains/z88dk.mk + +MSX_FLAGS = +hdos -create-app -startup=3 -Cz--abs -pragma-define:CLIB_DISABLE_FGETS_CURSOR=1 +CFLAGS += $(MSX_FLAGS) -D__H89__ +LDFLAGS += $(MSX_FLAGS) + +r2r:: $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS) + make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post diff --git a/mekkogx/platforms/lynx.mk b/mekkogx/platforms/lynx.mk new file mode 100644 index 0000000..561c74a --- /dev/null +++ b/mekkogx/platforms/lynx.mk @@ -0,0 +1,9 @@ +EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).lnx +LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib + +MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) +include $(MWD)/common.mk +include $(MWD)/toolchains/cc65.mk + +r2r:: $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS_$(PLATFORM_UC)) + make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post diff --git a/mekkogx/platforms/model2.mk b/mekkogx/platforms/model2.mk new file mode 100644 index 0000000..fbe238d --- /dev/null +++ b/mekkogx/platforms/model2.mk @@ -0,0 +1,46 @@ +EXEC_SUFFIX = .com +DISK = $(R2R_PD)/$(PRODUCT_BASE).imd +LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib +DISK_TOOL = cpmcp +MKFS_TOOL = mkfs.cpm +CONVERT_TOOL = raw2imd +DISKDEF = ibm-3740 + +MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) +include $(MWD)/common.mk +include $(MWD)/toolchains/z88dk.mk + +MODEL2_FLAGS = +cpm +CFLAGS += $(MODEL2_FLAGS) -D__TRS80M2__ +LDFLAGS += $(MODEL2_FLAGS) + +r2r:: $(BUILD_DISK) $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS) + make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post + +CPM_BLANK = blank-pt.img +CPM_DISK = $(CACHE_PLATFORM)/$(CPM_BLANK) + +$(CPM_DISK): | $(CACHE_PLATFORM) + $(call require,$(MKFS_TOOL),$(DISK_TOOL_INFO)) + dd if=/dev/zero of=$@.tmp bs=128 count=2002 + $(MKFS_TOOL) -f $(DISKDEF) $@.tmp + mv $@.tmp $@ + + +$(BUILD_DISK): $(DISK_EXECUTABLES) $(CPM_DISK) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) + $(call require,$(DISK_TOOL),$(DISK_TOOL_INFO)) + $(call require,$(CONVERT_TOOL),https://www.seasip.info/Unix/LibDsk/) + cp $(CPM_DISK) $@.raw.tmp + $(foreach f,$(DISK_EXECUTABLES) $(DISK_EXTRA_FILES),$(call copy-to-disk,,$(f),0:,$@.raw.tmp);) + $(CONVERT_TOOL) -8 -c 77 -h 1 -s 26 -l 128 $@.raw.tmp $@ + rm -f $@.raw.tmp + make -f $(PLATFORM_MK) $(PLATFORM)/disk-post + +# Arguments: +# $1 == DISK_TOOL flags +# $2 == source file +# $3 == destination name +# $4 == temporary raw disk target +define copy-to-disk + $(DISK_TOOL) -f $(DISKDEF) $1 $4 $2 $3 +endef diff --git a/mekkogx/platforms/msdos.mk b/mekkogx/platforms/msdos.mk index 8a3797b..160b7c1 100644 --- a/mekkogx/platforms/msdos.mk +++ b/mekkogx/platforms/msdos.mk @@ -1,5 +1,5 @@ PRODUCT_BASE = $(PRODUCT_BASE) -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).exe +EXEC_SUFFIX = .exe DISK = $(R2R_PD)/$(PRODUCT_BASE).img LIBRARY = $(R2R_PD)/lib$(PRODUCT_BASE).$(PLATFORM).a DISK_TOOL_CREATE = dd @@ -16,11 +16,11 @@ CFLAGS += -D__MSDOS__ r2r:: $(BUILD_DISK) $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS) make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post -$(BUILD_DISK): $(BUILD_EXEC) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) +$(BUILD_DISK): $(DISK_EXECUTABLES) $(DISK_EXTRA_DEPS) $(DISK_EXTRA_FILES) | $(R2R_PD) $(RM) $@ $(RM) -rf $(CACHE_PLATFORM)/disk $(MKDIR_P) $(CACHE_PLATFORM)/disk - cp $< $(CACHE_PLATFORM)/disk + cp $(DISK_EXECUTABLES) $(CACHE_PLATFORM)/disk $(call require,$(DISK_TOOL_FORMAT),) $(call require,$(DISK_TOOL_COPY),) $(DISK_TOOL_CREATE) if=/dev/zero of=$@ bs=1024 count=$(DISK_SIZE_KB) diff --git a/mekkogx/platforms/msx.mk b/mekkogx/platforms/msx.mk index f54c166..2b6fcef 100644 --- a/mekkogx/platforms/msx.mk +++ b/mekkogx/platforms/msx.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).com +EXEC_SUFFIX = .com LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) @@ -8,6 +8,9 @@ include $(MWD)/toolchains/z88dk.mk MSX_FLAGS = +msx CFLAGS += $(MSX_FLAGS) LDFLAGS += $(MSX_FLAGS) +ifneq ($(IS_LIBRARY),1) + LDFLAGS += -create-app +endif r2r:: $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS) make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post diff --git a/mekkogx/platforms/msxrom.mk b/mekkogx/platforms/msxrom.mk index dc80a7f..8068c81 100644 --- a/mekkogx/platforms/msxrom.mk +++ b/mekkogx/platforms/msxrom.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).rom +EXEC_SUFFIX = .rom LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) @@ -8,6 +8,9 @@ include $(MWD)/toolchains/z88dk.mk MSXROM_FLAGS = +msx -subtype=rom CFLAGS += $(MSXROM_FLAGS) LDFLAGS += $(MSXROM_FLAGS) +ifneq ($(IS_LIBRARY),1) + LDFLAGS += -create-app +endif r2r:: $(EXECUTABLE) $(R2R_EXTRA_DEPS) make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post diff --git a/mekkogx/platforms/pc6001.mk b/mekkogx/platforms/pc6001.mk index 74ed25f..5352cc7 100644 --- a/mekkogx/platforms/pc6001.mk +++ b/mekkogx/platforms/pc6001.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).ptp +EXEC_SUFFIX = .ptp LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib AS_DEFAULT = zcc diff --git a/mekkogx/platforms/plus4.mk b/mekkogx/platforms/plus4.mk new file mode 100644 index 0000000..867bc5d --- /dev/null +++ b/mekkogx/platforms/plus4.mk @@ -0,0 +1,9 @@ +EXEC_SUFFIX = .prg +LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib + +MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) +include $(MWD)/common.mk +include $(MWD)/toolchains/cc65.mk + +r2r:: $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS) + make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post diff --git a/mekkogx/platforms/pmd85.mk b/mekkogx/platforms/pmd85.mk index 74ed25f..401fb68 100644 --- a/mekkogx/platforms/pmd85.mk +++ b/mekkogx/platforms/pmd85.mk @@ -1,15 +1,17 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).ptp +EXEC_SUFFIX = .bin LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib -AS_DEFAULT = zcc MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) include $(MWD)/common.mk include $(MWD)/toolchains/z88dk.mk -$(PLATFORM_UC)_FLAGS = +$(PLATFORM) -CFLAGS += $($(PLATFORM_UC)_FLAGS) -ASFLAGS += $($(PLATFORM_UC)_FLAGS) -c -LDFLAGS += $($(PLATFORM_UC)_FLAGS) +PMD85_FLAGS = +pmd85 +CFLAGS += $(PMD85_FLAGS) +ASFLAGS += -m=8080 +LDFLAGS += $(PMD85_FLAGS) +ifneq ($(IS_LIBRARY),1) + LDFLAGS += -create-app +endif r2r:: $(BUILD_EXEC) $(BUILD_LIB) $(R2R_EXTRA_DEPS) make -f $(PLATFORM_MK) $(PLATFORM)/r2r-post diff --git a/mekkogx/platforms/rc2014.mk b/mekkogx/platforms/rc2014.mk index 97faf83..d51c450 100644 --- a/mekkogx/platforms/rc2014.mk +++ b/mekkogx/platforms/rc2014.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).ptp +EXEC_SUFFIX = .ptp LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib AS_DEFAULT = zcc diff --git a/mekkogx/platforms/vic20.mk b/mekkogx/platforms/vic20.mk index 5ed117d..867bc5d 100644 --- a/mekkogx/platforms/vic20.mk +++ b/mekkogx/platforms/vic20.mk @@ -1,4 +1,4 @@ -EXECUTABLE = $(R2R_PD)/$(PRODUCT_BASE).prg +EXEC_SUFFIX = .prg LIBRARY = $(R2R_PD)/$(PRODUCT_BASE).$(PLATFORM).lib MWD := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))..) diff --git a/mekkogx/tc-common.mk b/mekkogx/tc-common.mk index 284a1c8..def139f 100644 --- a/mekkogx/tc-common.mk +++ b/mekkogx/tc-common.mk @@ -16,14 +16,16 @@ LD := $(LD_$(TOOLCHAIN_UC)) AR := $(AR_$(TOOLCHAIN_UC)) PC := $(PC_$(TOOLCHAIN_UC)) -CFLAGS += $(CFLAGS_EXTRA_$(TOOLCHAIN_UC)) $(CFLAGS_EXTRA_$(PLATFORM_UC)) -ASFLAGS += $(ASFLAGS_EXTRA_$(TOOLCHAIN_UC)) $(ASFLAGS_EXTRA_$(PLATFORM_UC)) -LDFLAGS += $(LDFLAGS_EXTRA_$(TOOLCHAIN_UC)) $(LDFLAGS_EXTRA_$(PLATFORM_UC)) +CFLAGS += $(CFLAGS_EXTRA) $(CFLAGS_EXTRA_$(TOOLCHAIN_UC)) $(CFLAGS_EXTRA_$(PLATFORM_UC)) +ASFLAGS += $(ASFLAGS_EXTRA) $(ASFLAGS_EXTRA_$(TOOLCHAIN_UC)) $(ASFLAGS_EXTRA_$(PLATFORM_UC)) +LDFLAGS += $(LDFLAGS_EXTRA) $(LDFLAGS_EXTRA_$(TOOLCHAIN_UC)) $(LDFLAGS_EXTRA_$(PLATFORM_UC)) EXTRA_INCLUDE_$(TOOLCHAIN_UC) += $(EXTRA_INCLUDE) $(EXTRA_INCLUDE_$(PLATFORM_UC)) CFLAGS += $(foreach incdir,$(EXTRA_INCLUDE_$(TOOLCHAIN_UC)),$(call include-dir-flag,$(incdir))) ASFLAGS += $(foreach incdir,$(EXTRA_INCLUDE_$(TOOLCHAIN_UC)),$(call asm-include-dir-flag,$(incdir))) +LIBS += $(LIBS_EXTRA_$(PLATFORM_UC)) + ifdef FUJINET_LIB_INCLUDE CFLAGS += $(call include-dir-flag,$(FUJINET_LIB_INCLUDE)) ASFLAGS += $(call asm-include-dir-flag,$(FUJINET_LIB_INCLUDE)) @@ -31,3 +33,14 @@ endif ifdef FUJINET_LIB_DIR LIBS += $(call library-dir-flag,$(FUJINET_LIB_DIR)) $(call library-flag,$(FUJINET_LIB_LDLIB)) endif + +ifdef HIRESTXT_LIB_INCLUDE + CFLAGS += $(call include-dir-flag,$(HIRESTXT_LIB_INCLUDE)) + ASFLAGS += $(call asm-include-dir-flag,$(HIRESTXT_LIB_INCLUDE)) +endif +ifdef HIRESTXT_LIB_NO_VT52 + CFLAGS += -DHIRESTEXT_NO_VT52 +endif +ifdef HIRESTXT_LIB_DIR + LIBS += $(call library-dir-flag,$(HIRESTXT_LIB_DIR)) $(call library-flag,$(HIRESTXT_LIB_LDLIB)) +endif diff --git a/mekkogx/toolchains/cc65.mk b/mekkogx/toolchains/cc65.mk index 42b1e3b..b8f319a 100644 --- a/mekkogx/toolchains/cc65.mk +++ b/mekkogx/toolchains/cc65.mk @@ -9,7 +9,12 @@ CFLAGS += -O --cpu 6502 ASFLAGS += --cpu 6502 LDFLAGS += -CFLAGS += -DGIT_VERSION='"$(GIT_VERSION)"' +DSTRING_OPEN = '" +DSTRING_CLOSE = "' +CFLAGS += -DGIT_VERSION=$(DSTRING_OPEN)$(GIT_VERSION)$(DSTRING_CLOSE) +ifneq ($(FUJINET_LIB_VERSION),) + CFLAGS += -DFNLIB_VERSION_FULL='"$(FUJINET_LIB_VERSION)"' +endif define include-dir-flag --include-dir $1 diff --git a/mekkogx/toolchains/cmoc.mk b/mekkogx/toolchains/cmoc.mk index 63c3e6a..4403745 100644 --- a/mekkogx/toolchains/cmoc.mk +++ b/mekkogx/toolchains/cmoc.mk @@ -9,7 +9,12 @@ CFLAGS += --intdir=$(OBJ_DIR) ASFLAGS += LDFLAGS += -CFLAGS += -DGIT_VERSION='"$(GIT_VERSION)"' +DSTRING_OPEN = '" +DSTRING_CLOSE = "' +CFLAGS += -DGIT_VERSION=$(DSTRING_OPEN)$(GIT_VERSION)$(DSTRING_CLOSE) +ifneq ($(FUJINET_LIB_VERSION),) + CFLAGS += -DFNLIB_VERSION_FULL='"$(FUJINET_LIB_VERSION)"' +endif # Needed because of using sed on error messages SHELL = /bin/bash -o pipefail diff --git a/mekkogx/toolchains/ow2.mk b/mekkogx/toolchains/ow2.mk index 397ba7a..5dde8f0 100644 --- a/mekkogx/toolchains/ow2.mk +++ b/mekkogx/toolchains/ow2.mk @@ -5,14 +5,19 @@ AR_DEFAULT ?= wlib include $(MWD)/tc-common.mk -CFLAGS += -0 -bt=dos -ms -s -osh -zu -fr=$(basename $@).err +CFLAGS += -0 -bt=dos -fr=$(basename $@).err ASFLAGS += LDFLAGS += SYSTEM dos ifneq ($(FUJINET_LIB),__UNDEFINED__) LDFLAGS += LIBPATH $(FUJINET_LIB_DIR) endif -CFLAGS += -DGIT_VERSION=\"$(GIT_VERSION)\" +DSTRING_OPEN = \" +DSTRING_CLOSE = \" +CFLAGS += -DGIT_VERSION=$(DSTRING_OPEN)$(GIT_VERSION)$(DSTRING_CLOSE) +ifneq ($(FUJINET_LIB_VERSION),) + CFLAGS += -DFNLIB_VERSION_FULL=\"$(FUJINET_LIB_VERSION)\" +endif define include-dir-flag -I$1 diff --git a/mekkogx/toolchains/z88dk.mk b/mekkogx/toolchains/z88dk.mk index bb0e5d0..aeae424 100644 --- a/mekkogx/toolchains/z88dk.mk +++ b/mekkogx/toolchains/z88dk.mk @@ -9,7 +9,12 @@ CFLAGS += ASFLAGS += LDFLAGS += -CFLAGS += -DGIT_VERSION='\"$(GIT_VERSION)\"' +DSTRING_OPEN = '\" +DSTRING_CLOSE = \"' +CFLAGS += -DGIT_VERSION=$(DSTRING_OPEN)$(GIT_VERSION)$(DSTRING_CLOSE) +ifneq ($(FUJINET_LIB_VERSION),) + CFLAGS += -DFNLIB_VERSION_FULL='\"$(FUJINET_LIB_VERSION)\"' +endif define include-dir-flag -I$1 diff --git a/mekkogx/toplevel-rules.mk b/mekkogx/toplevel-rules.mk index f673e9c..4ef5eaf 100644 --- a/mekkogx/toplevel-rules.mk +++ b/mekkogx/toplevel-rules.mk @@ -3,26 +3,42 @@ R2R_DIR = r2r BUILD_DIR = build CACHE_DIR = _cache +RELEASE_DIR = release -MAKEFILE_DIR = $(realpath $(dir $(lastword $(MAKEFILE_LIST)))) +MAKEFILE_DIR := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))) + +# PRODUCTS names several binaries sharing one disk, PRODUCTS_DISK_NAME +ifneq ($(strip $(PRODUCTS)),) + PRODUCTS_DISK_NAME ?= $(if $(strip $(PRODUCT)),$(PRODUCT),$(firstword $(filter-out %.lib,$(PRODUCTS)))) + MEKKO_TARGET := $(PRODUCTS_DISK_NAME) +else + MEKKO_TARGET := $(PRODUCT) +endif # Make a list of the things we want to build which combine R2R dir, app name, and platform -APP_TARGETS := $(foreach p, $(PLATFORMS), $(R2R_DIR)/$(p)/$(PRODUCT)) +APP_TARGETS := $(foreach p, $(PLATFORMS), $(R2R_DIR)/$(p)/$(MEKKO_TARGET)) -.PHONY: all clean FORCE +.PHONY: all clean release FORCE all:: $(APP_TARGETS) clean:: rm -rf $(R2R_DIR) $(BUILD_DIR) $(CACHE_DIR) +ifeq ($(PLATFORM),) +RELEASE_TARGETS := $(foreach p,$(PLATFORMS),$(p)/release) +$(info RELEASE_TARGETS=$(RELEASE_TARGETS)) +release:: $(RELEASE_TARGETS) +endif + # Use % wildcard match to platform specific app so we don't have to # spell out every single platform variation -$(R2R_DIR)/%/$(PRODUCT): FORCE - $(MAKE) -f $(MAKEFILE_DIR)/platforms/$*.mk r2r +$(R2R_DIR)/%/$(MEKKO_TARGET): FORCE + $(MAKE) -f $(MAKEFILE_DIR)/platforms/$*.mk \ + $(if $(MEKKO_CONFIG),MEKKO_CONFIG=$(MEKKO_CONFIG)) r2r # Convenience: allow `make coco` (or apple2) as a shortcut -$(PLATFORMS): %: $(R2R_DIR)/%/$(PRODUCT) +$(PLATFORMS): %: $(R2R_DIR)/%/$(MEKKO_TARGET) # ------------------------------------------------------------------------ # Pattern rule to support "make /" syntax. @@ -43,6 +59,7 @@ $(PLATFORMS): %: $(R2R_DIR)/%/$(PRODUCT) @target="$@" ; case "$@" in \ */*/*) echo "No rule to make target '$@'"; exit 1;; \ */*) platform=$${target%/*}; target=$${target##*/}; \ - $(MAKE) -f $(MAKEFILE_DIR)/platforms/$${platform}.mk $${target} ;; \ + $(MAKE) -f $(MAKEFILE_DIR)/platforms/$${platform}.mk \ + $(if $(MEKKO_CONFIG),MEKKO_CONFIG=$(MEKKO_CONFIG)) $${target} ;; \ *) echo "No rule to make target '$@'"; exit 1;; \ esac From bc66e1feb64a6621efa72391c6a5b570d5b3a682 Mon Sep 17 00:00:00 2001 From: Thomas Cherryhomes Date: Thu, 15 Oct 2026 12:27:45 -0500 Subject: [PATCH 2/3] [h89] NetCat for the Heathkit H89, CP/M and HDOS NC.COM and NC.ABS from one source, each on an .h8d disk. The URL comes from the command line, or from a prompt with an example. The H19 is the terminal: host bytes go straight to it, keys straight to the host, and TELNET/SSH get ?term=h19&cols=80&rows=24. Other protocols get a CR before a bare LF. f5 hangs up. fujinet-lib comes from FujiNetWIFI/fujinet-lib via git, as no release ships H89 yet. CI builds both on defoogi 1.4.8 (1.4.2 has no +hdos). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 17 +- Makefile | 45 ++++- src/h89/con.c | 191 +++++++++++++++++++++ src/h89/con.h | 31 ++++ src/h89/main.c | 317 +++++++++++++++++++++++++++++++++++ support/h89/cpm-dir-fixup.py | 30 ++++ 6 files changed, 623 insertions(+), 8 deletions(-) create mode 100644 src/h89/con.c create mode 100644 src/h89/con.h create mode 100644 src/h89/main.c create mode 100755 support/h89/cpm-dir-fixup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7d8c05..3d32e6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,15 +13,18 @@ jobs: strategy: fail-fast: false matrix: - platform: - - adam - - apple2 - - atari - - vic20 - - coco + # The H89 builds need a z88dk with +hdos, which 1.4.2 predates + include: + - { platform: adam, image: "fozztexx/defoogi:1.4.2" } + - { platform: apple2, image: "fozztexx/defoogi:1.4.2" } + - { platform: atari, image: "fozztexx/defoogi:1.4.2" } + - { platform: vic20, image: "fozztexx/defoogi:1.4.2" } + - { platform: coco, image: "fozztexx/defoogi:1.4.2" } + - { platform: h89-cpm, image: "fozztexx/defoogi:1.4.8" } + - { platform: h89-hdos, image: "fozztexx/defoogi:1.4.8" } container: - image: fozztexx/defoogi:1.4.2 + image: ${{ matrix.image }} steps: - uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index da11436..0c52230 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,8 @@ PLATFORMS += apple2 PLATFORMS += atari PLATFORMS += c64 PLATFORMS += coco +PLATFORMS += h89-cpm +PLATFORMS += h89-hdos PLATFORMS += msx #PLATFORMS += rc2014 PLATFORMS += vic20 @@ -27,6 +29,12 @@ SRC_DIRS = src src/%PLATFORM% # - undefined, no fujinet-lib will be used FUJINET_LIB = https://github.com/FozzTexx/fujinet-lib-experimental.git +# The H89's bus is only in fujinet-lib itself, which has no H89 release +# archive yet, so both H89 builds take it from git. +ifneq ($(filter h89-cpm h89-hdos,$(PLATFORM)),) + FUJINET_LIB = https://github.com/FujiNetWIFI/fujinet-lib.git +endif + # Some platforms don’t use FUJINET_LIB; set this to allow builds to continue # even if the library isn’t present. FUJINET_LIB_OPTIONAL = 1 @@ -37,7 +45,20 @@ PLATFORM_COMBOS = \ c64+=commodore \ atarixe+=atari \ msxrom+=msx \ - msxdos+=msx + msxdos+=msx \ + h89-cpm+=h89 \ + h89-hdos+=h89 + +# H89: no stdio or file handles, since src/h89/con.c talks to the console +# itself. The command line stays on: the URL can come from it. -m leaves the +# map beside the program. +H89_LDFLAGS = -m \ + -pragma-define:CRT_ENABLE_STDIO=0 \ + -pragma-define:CLIB_FOPEN_MAX=0 \ + -pragma-define:CLIB_OPEN_MAX=0 \ + -pragma-define:CLIB_EXIT_STACK_SIZE=0 +LDFLAGS_EXTRA_H89-CPM = $(H89_LDFLAGS) +LDFLAGS_EXTRA_H89-HDOS = $(H89_LDFLAGS) include hirestxt-mod-lib.mk include mekkogx/toplevel-rules.mk @@ -49,3 +70,25 @@ include mekkogx/toplevel-rules.mk # apple2/disk: apple2/custom-step1 apple2/custom-step2 EXTRA_C_DEPS_COCO = .get_hirestxt_lib + +# H89: the program on a 100K H17 data disk. +PRODUCT_UPPER = $(shell echo $(PRODUCT) | tr '[:lower:]' '[:upper:]') +H89_APPMAKE ?= z88dk-appmake + +# HDOS: appmake +hdos wants the bare program (the .abs less its 8-byte +# header: FF 00, load, length, entry) and the load address. It names the +# image after the input file and the file on it after -o, hence the cd. +h89-hdos/r2r-post:: + tail -c +9 $(EXECUTABLE) > $(R2R_PD)/$(PRODUCT).bin + cd $(R2R_PD) && $(H89_APPMAKE) +hdos -b $(PRODUCT).bin \ + --org $$(od -An -tu2 -j2 -N2 $(notdir $(EXECUTABLE)) | tr -d ' ') -o $(PRODUCT_UPPER) + rm -f $(R2R_PD)/$(PRODUCT).bin + @echo "$(R2R_PD)/$(PRODUCT).h8d: MOUNT SY1: then SY1:$(PRODUCT_UPPER) [URL]" + +# CP/M: the hz89 format (H17, 40 tracks, 3 boot tracks), then mark the empty +# directory entries appmake leaves zeroed as erased (support/h89). +h89-cpm/r2r-post:: + cd $(R2R_PD) && $(H89_APPMAKE) +cpmdisk -f hz89 --container raw --no-com-file \ + -b $(notdir $(EXECUTABLE)) -o $(PRODUCT).h8d + python3 -I support/h89/cpm-dir-fixup.py $(R2R_PD)/$(PRODUCT).h8d + @echo "$(R2R_PD)/$(PRODUCT).h8d: B:$(PRODUCT_UPPER) [URL]" diff --git a/src/h89/con.c b/src/h89/con.c new file mode 100644 index 0000000..32367af --- /dev/null +++ b/src/h89/con.c @@ -0,0 +1,191 @@ +/** + * @brief Raw H19 console - Heathkit H89 + * @author Thomas Cherryhomes + * @email thom dot cherryhomes at gmail dot com + * @license gpl v. 3, see LICENSE for details. + * + * The H19 is the terminal, so bytes go to it exactly as they come from the + * host, and keys go to the host exactly as the H19 sends them. That means + * going under the OS's console handling: HDOS's .SCOUT/.SCIN with the + * console driver in raw character mode, or the CP/M BIOS's CONOUT/CONST/CONIN + * straight from the jump table (the BDOS console calls expand tabs and catch + * ^C, ^S and ^P). + * + * From fujinet-battleship's H89 port (add-h89, src/h89/console.c). + */ + +#include +#include "con.h" + +#ifdef __HDOS__ + +// HDOS system calls: RST 38H, then the function byte. HDOS is 8080 code and +// leaves IX and IY alone. +void con_out(uint8_t c) __z88dk_fastcall __naked +{ +#asm + ld a,l + rst 38h + defb 2 ; .SCOUT + ret +#endasm +} + +int con_in(void) __naked +{ +#asm + rst 38h + defb 1 ; .SCIN - carry set when nothing is waiting + ld hl,-1 + ret c + ld l,a + ld h,0 + ret +#endasm +} + +// .CONSL: A = index of the console byte, B = the bits to set, C = the mask of +// bits to change +uint8_t h89ConslIndex, h89ConslBits, h89ConslMask; + +static void consl(void) __naked +{ +#asm + ld a,(_h89ConslBits) + ld b,a + ld a,(_h89ConslMask) + ld c,a + ld a,(_h89ConslIndex) + rst 38h + defb 6 ; .CONSL + ret +#endasm +} + +static void consl_set(uint8_t index, uint8_t bits, uint8_t mask) +{ + h89ConslIndex = index; + h89ConslBits = bits; + h89ConslMask = mask; + consl(); +} + +// The five console cells (S.CSLMD..S.CONFL) live at 040.326A in HDOS 2.0's +// system values (HOSDEF: S.VAL at 040.277A) +#define S_CSLMD ((volatile uint8_t *)0x20D6) +#define I_CSLMD 0 +#define I_CONTY 1 +#define I_CUSOR 2 +#define I_CONWI 3 +#define I_CONFL 4 + +#define CSL_ECH 0x80 // suppress echo +#define CSL_RAW 0x04 // raw mode I/O (no CTL-C/S/O/P processing) +#define CSL_WRP 0x02 // wrap lines at width +#define CSL_CHR 0x01 // character mode +#define CTP_MLI 0x20 // map lower case to upper on input +#define CTP_MLO 0x10 // map lower case to upper on output +#define CTP_BKM 0x02 // map backspace to rubout on input +#define CO_FLG 0x01 // CTL-O (discard output) flag +#define CS_FLG 0x80 // CTL-S (hold output) flag + +static uint8_t saved[5]; + +void con_init(void) +{ + memcpy(saved, (void *)S_CSLMD, sizeof(saved)); + + // Raw character mode with no echo, so ^C, ^S, ^O and ^P go to the host; + // no wrap and no width, because the H19 wraps for itself and HDOS would + // count escape sequences as columns; and no case mapping either way. + consl_set(I_CSLMD, CSL_ECH | CSL_RAW | CSL_CHR, CSL_ECH | CSL_RAW | CSL_WRP | CSL_CHR); + consl_set(I_CONTY, 0, CTP_MLI | CTP_MLO | CTP_BKM); + consl_set(I_CONWI, 255, 0xFF); + consl_set(I_CONFL, 0, CO_FLG | CS_FLG); +} + +void con_exit(void) +{ + consl_set(I_CSLMD, saved[I_CSLMD], 0xFF); + consl_set(I_CONTY, saved[I_CONTY], 0xFF); + consl_set(I_CONWI, saved[I_CONWI], 0xFF); + consl_set(I_CUSOR, 0, 0xFF); // every exit path ends a line +} + +#else /* CP/M */ + +// The BIOS jump table: (0001) points at its WBOOT entry, base+3. CONST is at +// +3 from there, CONIN +6, CONOUT +9. The BIOS may use any register, so IX +// and IY are kept for the C side. +void con_out(uint8_t c) __z88dk_fastcall __naked +{ +#asm + EXTERN l_dcal + push ix + push iy + ld c,l + ld hl,(1) + ld de,9 + add hl,de + call l_dcal ; CONOUT + pop iy + pop ix + ret +#endasm +} + +int con_in(void) __naked +{ +#asm + EXTERN l_dcal + push ix + push iy + ld hl,(1) + ld de,3 + add hl,de + call l_dcal ; CONST: A = FF when a character is waiting + or a + ld hl,-1 + jr z,con_in_done + ld hl,(1) + ld de,6 + add hl,de + call l_dcal ; CONIN + ld l,a + ld h,0 +con_in_done: + pop iy + pop ix + ret +#endasm +} + +void con_init(void) +{ +} + +void con_exit(void) +{ +} + +#endif /* __HDOS__ */ + +void con_puts(const char *s) +{ + while (*s) + con_out(*s++); +} + +void con_putu(uint16_t n) +{ + char d[6]; + uint8_t i = sizeof(d) - 1; + + d[i] = 0; + do + { + d[--i] = '0' + n % 10; + n /= 10; + } while (n); + con_puts(&d[i]); +} diff --git a/src/h89/con.h b/src/h89/con.h new file mode 100644 index 0000000..91f9c6a --- /dev/null +++ b/src/h89/con.h @@ -0,0 +1,31 @@ +/** + * @brief Raw H19 console - Heathkit H89 + * @author Thomas Cherryhomes + * @email thom dot cherryhomes at gmail dot com + * @license gpl v. 3, see LICENSE for details. + */ + +#ifndef CON_H +#define CON_H + +#include + +#define CRLF "\015\012" + +/** Put the OS console into raw mode (HDOS); call before any other con_ call */ +void con_init(void); + +/** Put the console back the way it was found */ +void con_exit(void); + +/** Send one byte to the H19, untouched */ +void con_out(uint8_t c) __z88dk_fastcall; + +/** The next key, or -1 if none is waiting */ +int con_in(void); + +void con_puts(const char *s); + +void con_putu(uint16_t n); + +#endif /* CON_H */ diff --git a/src/h89/main.c b/src/h89/main.c new file mode 100644 index 0000000..5d2c566 --- /dev/null +++ b/src/h89/main.c @@ -0,0 +1,317 @@ +/** + * @brief NetCat - a dumb terminal for the Heathkit H89 + * @author Thomas Cherryhomes + * @email thom dot cherryhomes at gmail dot com + * @license gpl v. 3, see LICENSE for details. + * @verbose Main Program + * + * One program for CP/M (NC.COM) and HDOS (NC.ABS). The H19 is already a + * terminal, so this is a pipe: bytes from the host go straight to it, keys go + * straight to the host, and TELNET and SSH are told term=h19 so the far end + * draws for it. Everything else (TCP, HTTP, ...) is more likely a text file + * with bare LF line ends, so there a LF gets the CR the H19 needs in front of + * it. f5 (ESC W) hangs up. + */ + +#include +#include +#include +#include "con.h" + +#define ESC 0x1B +#define CTRL_C 0x03 +#define BS 0x08 +#define DEL 0x7F + +// f5 sends ESC W. The H19 sends the pair back to back at 9600 baud, so an +// ESC with nothing behind it after this many polls is the ESC key itself +// (each poll is a BIOS or HDOS call, roughly 0.1-0.25 ms). +#define KEY_HANGUP 'W' +#define ESC_POLLS 200 + +// How many unhappy STATUS polls in a row mean the connection has gone. +// Patient while dialling (OPEN returns before SSH has finished its +// handshake), brisk once up. A poll is happy when it says SUCCESS or has +// bytes waiting: a connection already closed can still have a screenful. +#define GRACE_POLLS 2000 +#define DROP_POLLS 16 + +// Read for the screen in pieces this size. The console is 9600 baud, so +// this is half a second of output. +#define RX_CHUNK 512 + +#define TERM_PARAMS "?term=h19&cols=80&rows=24" + +static char line[256]; +static char spec[2 + sizeof(line) + sizeof(TERM_PARAMS)]; +static uint8_t rxbuf[RX_CHUNK]; + +// Keys waiting to go to the host +static uint8_t txq[64]; +static uint8_t txn; +static uint8_t hangup; + +// Not a terminal session (TELNET/SSH): give a bare LF a CR +static uint8_t add_cr; + +static const char banner[] = + "NetCat for the Heathkit H89 - a FujiNet terminal" CRLF + CRLF + "Usage: NC f5 hangs up." CRLF + "The command line arrives in upper case; type a case-sensitive URL here." CRLF + CRLF + "Enter URL, e.g. N:TELNET://BBS.FOZZTEXX.COM/ (RETURN alone quits)" CRLF + "URL> "; + +/** The byte after an ESC, or -1 if none comes in time */ +static int esc_follower(void) +{ + uint8_t i; + int c; + + for (i = 0; i < ESC_POLLS; i++) + { + c = con_in(); + if (c >= 0) + return c; + } + return -1; +} + +/** Read a line into line[]; returns its length, 0 to quit */ +static uint8_t read_line(void) +{ + uint8_t n = 0; + int c; + + for (;;) + { + c = con_in(); + if (c < 0) + continue; + c &= 0x7F; + + if (c == '\r' || c == '\n') + break; + if (c == CTRL_C) + { + n = 0; + break; + } + if (c == ESC) + { + // an arrow or function key: not part of a URL + esc_follower(); + continue; + } + if (c == BS || c == DEL) + { + if (n) + { + n--; + con_puts("\010 \010"); + } + continue; + } + if (c >= ' ' && n < sizeof(line) - 1) + { + line[n++] = c; + con_out(c); + } + } + + line[n] = 0; + con_puts(CRLF); + return n; +} + +static char upper(char c) +{ + return (c >= 'a' && c <= 'z') ? c - 'a' + 'A' : c; +} + +/** Does s begin with prefix (upper case), ignoring case? */ +static uint8_t starts_with(const char *s, const char *prefix) +{ + while (*prefix) + if (upper(*s++) != *prefix++) + return 0; + return 1; +} + +/** N:, or N1: to N8: */ +static uint8_t has_device(const char *s) +{ + if (upper(s[0]) != 'N') + return 0; + if (s[1] == ':') + return 1; + return s[1] >= '1' && s[1] <= '8' && s[2] == ':'; +} + +/** + * The devicespec from what was typed: N: in front if it has no device, and + * the H19's terminal type and size on TELNET and SSH URLs that carry no query + * of their own (firmware 1.6.1 and later read them there). + */ +static void make_spec(const char *url) +{ + const char *u; + + while (*url == ' ') + url++; + + spec[0] = 0; + if (!has_device(url)) + strcpy(spec, "N:"); + strcat(spec, url); + + u = strchr(spec, ':') + 1; + if (!starts_with(u, "TELNET://") && !starts_with(u, "SSH://")) + add_cr = 1; + else if (!strchr(spec, '?')) + strcat(spec, TERM_PARAMS); +} + +/** Queue one key for the host, f5 sets hangup instead; 0 if none taken */ +static uint8_t take_key(void) +{ + int c, n; + + if (hangup || txn > sizeof(txq) - 2) + return 0; + + c = con_in(); + if (c < 0) + return 0; + + if (c == ESC) + { + n = esc_follower(); + if (n == KEY_HANGUP) + { + hangup = 1; + return 0; + } + txq[txn++] = ESC; + if (n >= 0) + txq[txn++] = n; + return 1; + } + txq[txn++] = c; + return 1; +} + +static void send_keys(void) +{ + while (take_key()) + ; + + if (txn) + { + network_write(spec, txq, txn); + txn = 0; + } +} + +/** Show what the host sent; returns 0 when the connection has gone */ +static uint8_t from_host(void) +{ + static uint16_t bad; + static uint8_t up, last; + uint16_t bw, i, n; + uint8_t c, err; + + if (network_status(spec, &bw, &c, &err) != FN_ERR_OK) + bw = err = 0; + + if (err == NETWORK_SUCCESS || bw) + { + up = 1; + bad = 0; + } + else if (++bad > (up ? DROP_POLLS : GRACE_POLLS)) + return 0; + + if (bw) + { + n = bw > sizeof(rxbuf) ? sizeof(rxbuf) : bw; + if (network_read(spec, rxbuf, n) > 0) + { + for (i = 0; i < n; i++) + { + c = rxbuf[i]; + if (add_cr && c == '\n' && last != '\r') + con_out('\r'); + con_out(c); + last = c; + // keys typed while a long screen draws go with the next send + take_key(); + } + } + } + return 1; +} + +static void session(void) +{ + uint16_t bw; + uint8_t c, err; + + con_puts("Connecting to "); + con_puts(spec); + con_puts(" (f5 hangs up)" CRLF); + + if (network_open(spec, OPEN_MODE_RW, OPEN_TRANS_NONE) != FN_ERR_OK) + { + con_puts("Could not connect"); + if (network_status(spec, &bw, &c, &err) == FN_ERR_OK && err > NETWORK_SUCCESS) + { + con_puts(": error "); + con_putu(err); + } + con_puts("." CRLF); + return; + } + + for (;;) + { + send_keys(); + if (hangup) + { + network_close(spec); + con_puts(CRLF "Hung up." CRLF); + return; + } + if (!from_host()) + { + network_close(spec); + con_puts(CRLF "Disconnected." CRLF); + return; + } + } +} + +int main(int argc, char **argv) +{ + con_init(); + network_init(); + + if (argc > 1) + make_spec(argv[1]); + else + { + con_puts(banner); + if (!read_line()) + { + con_exit(); + return 0; + } + make_spec(line); + } + + session(); + + con_exit(); + return 0; +} diff --git a/support/h89/cpm-dir-fixup.py b/support/h89/cpm-dir-fixup.py new file mode 100755 index 0000000..42a0a0e --- /dev/null +++ b/support/h89/cpm-dir-fixup.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +""" +Mark the zeroed directory entries in an hz89 CP/M disk image as erased. + +appmake's cpm_write_file gives a file of exactly 16 blocks a second, empty +directory entry -- all zeros -- which DIR lists as a nameless file. ERA marks +an entry erased with E5 in its first byte only: Heath's CP/M 2.2.04 ends its +directory search at an entry that is all E5 (never used), so a fully blanked +entry would hide every file after it. The directory is the first 8 logical +sectors after the 3 boot tracks (10 x 256-byte sectors a track), in the +image's skewed order. + +From fujinet-go-h89-desktop's tools/disks/make-fujinet-disks.sh. + + cpm-dir-fixup.py DISK.h8d +""" +import sys + +SKEW = [0, 4, 8, 2, 6, 1, 5, 9, 3, 7] + +with open(sys.argv[1], 'r+b') as f: + for ls in range(8): + off = (3 * 10 + SKEW[ls]) * 256 + f.seek(off) + sec = bytearray(f.read(256)) + for e in range(0, 256, 32): + if not any(sec[e:e + 32]): + sec[e] = 0xE5 + f.seek(off) + f.write(sec) From 96fc7031852242e876dd4ea384b6bb276c30b47e Mon Sep 17 00:00:00 2001 From: Thomas Cherryhomes Date: Thu, 15 Oct 2026 12:30:29 -0500 Subject: [PATCH 3/3] [ci] Build on defoogi 1.4.8 1.4.2 has no atr (atari), its zcc chokes on fujinet-lib's -MMD (adam), and it has no +hdos (h89). 1.4.8 builds all seven. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d32e6e..515bf30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,18 +13,17 @@ jobs: strategy: fail-fast: false matrix: - # The H89 builds need a z88dk with +hdos, which 1.4.2 predates - include: - - { platform: adam, image: "fozztexx/defoogi:1.4.2" } - - { platform: apple2, image: "fozztexx/defoogi:1.4.2" } - - { platform: atari, image: "fozztexx/defoogi:1.4.2" } - - { platform: vic20, image: "fozztexx/defoogi:1.4.2" } - - { platform: coco, image: "fozztexx/defoogi:1.4.2" } - - { platform: h89-cpm, image: "fozztexx/defoogi:1.4.8" } - - { platform: h89-hdos, image: "fozztexx/defoogi:1.4.8" } + platform: + - adam + - apple2 + - atari + - vic20 + - coco + - h89-cpm + - h89-hdos container: - image: ${{ matrix.image }} + image: fozztexx/defoogi:1.4.8 steps: - uses: actions/checkout@v4