Implementing a New Blender Fork¶
This guide documents the steps to add support for a new Blender fork to the launcher.
Overview¶
Adding a new fork requires changes across multiple layers:
- Scraping - Fetch build information from fork’s distribution source
- Settings - Add configuration options for visibility and updates
- UI - Create library and download pages
- Build Processing - Handle fork-specific executable names, versioning, and structure
- Extraction - Handle fork-specific archive formats and directory structures
1. Create Scraper¶
Create a new scraper in source/threads/scraping/<fork_name>.py:
Basic Scraper Structure¶
from modules.build_info import BuildInfo
from modules._platform import get_platform
from threads.scraping.base import BuildScraper
from modules.connection_manager import ConnectionManager
class Scraper<ForkName>(BuildScraper):
def __init__(self, man: ConnectionManager):
super().__init__()
self.manager = man
def scrape(self) -> Generator[BuildInfo, None, None]:
# Fetch builds from fork's API/website
# Yield BuildInfo objects for each build
pass
Key Implementation Details¶
For GitHub Releases:
GITHUB_API_URL = "https://api.github.com/repos/<owner>/<repo>/releases"
def scrape(self):
r = self.manager.request("GET", GITHUB_API_URL)
releases = json.loads(r.data)
for release in releases:
for asset in release.get("assets", []):
download_url = asset.get("browser_download_url")
# Filter by platform
if not self._matches_platform(asset["name"]):
continue
build_info = BuildInfo(
download_url,
str(version),
commit_hash,
commit_time,
branch="fork-branch-name",
custom_executable="fork-executable-name"
)
yield build_info
Version Parsing:
- Parse semantic versions from release tags or filenames
- Use parse_blender_ver() from modules.build_info for compatibility
- Map fork versions to Blender config versions if needed
2. Update Settings Module¶
Add settings functions in source/modules/settings.py:
Visibility Settings¶
# Show in Library tab
def get_show_fork_builds() -> bool:
return get_settings().value("show_fork_builds", defaultValue=True, type=bool)
def set_show_fork_builds(b: bool):
get_settings().setValue("show_fork_builds", b)
# Show in Downloads tab
def get_scrape_fork_builds() -> bool:
return get_settings().value("scrape_fork_builds", defaultValue=True, type=bool)
def set_scrape_fork_builds(b: bool):
get_settings().setValue("scrape_fork_builds", b)
Update Button Settings¶
def get_show_fork_update_button() -> bool:
return get_settings().value("show_fork_update_button", defaultValue=True, type=bool)
def set_show_fork_update_button(is_checked):
get_settings().setValue("show_fork_update_button", is_checked)
def get_fork_update_behavior() -> int:
return get_settings().value("fork_update_behavior", defaultValue=2, type=int)
def set_fork_update_behavior(behavior):
get_settings().setValue("fork_update_behavior", update_behavior[behavior])
Update Settings Dictionaries¶
library_pages = {
# ... existing entries
"<Fork Name>": 4, # Next available index
}
downloads_pages = {
# ... existing entries
"<Fork Name>": 4, # Next available index
}
minimum_version_table = [
# ... existing entries
"<fork-branch>",
]
3. Add Update Manager Support¶
Update source/modules/blender_update_manager.py:
def _branch_visibility(current_branch: str) -> bool:
# ... existing checks
fork_visibility = (
get_show_fork_update_button() if get_use_advanced_update_button()
else get_show_update_button()
)
if current_branch == "<fork-branch>" and fork_visibility:
return True
# ...
def _get_update_behavior(current_branch: str) -> int:
# ... existing checks
fork_behavior = (
get_fork_update_behavior() if get_use_advanced_update_button()
else get_update_behavior()
)
if current_branch == "<fork-branch>":
return fork_behavior
# ...
4. Integrate Scraper¶
Update source/threads/scraper.py:
from threads.scraping.fork import Scraperfork
class Scraper:
def __init__(self, parent, man: ConnectionManager, build_cache=False):
# ... existing code
self.scrape_fork = get_scrape_fork_builds()
self.scraper_fork = Scraperfork(self.manager)
def scrapers(self):
scrapers = []
# ... existing scrapers
if self.scrape_fork:
scrapers.append(self.scraper_fork)
return scrapers
def get_download_links(self):
# Add platform filter if needed
if "fork" in build.link.lower():
self.links.emit(build)
5. Update Library Drawer¶
Update source/threads/library_drawer.py:
@dataclass
class DrawLibraryTask(Task):
folders: Iterable[str | Path] = (
"stable",
"daily",
"experimental",
"bforartists",
"<fork-branch>", # Add fork folder
"custom",
)
Add executable detection in get_blender_builds():
has_fork_exe = (path / build / fork_exe).is_file()
yield (
folder / build,
has_blinfo or has_blender_exe or has_fork_exe,
)
6. Add UI Pages¶
Update source/windows/main_window.py:
Create Page Widgets¶
def draw(self, polish=False):
# Library page
self.LibraryforkPageWidget = BasePageWidget(
parent=self,
page_name="LibraryforkListWidget",
time_label="Commit Time",
info_text="Nothing to show yet",
extended_selection=True,
)
self.LibraryforkListWidget = self.LibraryToolBox.add_page_widget(
self.LibraryforkPageWidget, "<Fork Name>"
)
# Downloads page
self.DownloadsforkPageWidget = BasePageWidget(
parent=self,
page_name="DownloadsforkListWidget",
time_label="Upload Time",
info_text="No new builds available",
)
self.DownloadsforkListWidget = self.DownloadsToolBox.add_page_widget(
self.DownloadsforkPageWidget, "<Fork Name>"
)
Update Scraper Integration¶
def start_scraper(self, scrape_fork=None, ...):
if scrape_fork is None:
scrape_fork = get_scrape_fork_builds()
if scrape_fork:
self.DownloadsforkPageWidget.set_info_label_text("Checking for new builds")
else:
self.DownloadsforkPageWidget.set_info_label_text("Checking disabled")
self.DownloadsforkListWidget.clear_()
self.scraper.scrape_fork = scrape_fork
def draw_to_downloads(self, build_info: BuildInfo):
# ... existing branches
elif branch == "<fork-branch>":
downloads_list_widget = self.DownloadsforkListWidget
library_list_widget = self.LibraryforkListWidget
def draw_to_library(self, path: Path, ...):
# ... existing branches
elif branch == "<fork-branch>":
library = self.LibraryforkListWidget
Update Visibility Controls¶
def update_visible_lists(self, force_l_fork=False, force_s_fork=False, ...):
show_fork = force_l_fork or get_show_fork_builds()
scrape_fork = force_s_fork or get_scrape_fork_builds()
self.LibraryToolBox.setTabVisible(X, show_fork)
self.LibraryToolBox.setTabEnabled(X, show_fork)
self.DownloadsToolBox.setTabVisible(X, scrape_fork)
self.DownloadsToolBox.setTabEnabled(X, scrape_fork)
7. Update Settings UI¶
Update source/widgets/settings_window/blender_builds_tab.py:
def __init__(self, parent: BlenderLauncher):
# Connect signals
self.repo_group.fork_repo.library_changed.connect(set_show_fork_builds)
self.repo_group.fork_repo.download_changed.connect(set_scrape_fork_builds)
# Add update button checkbox
self.ShowforkUpdateButton = QCheckBox()
self.ShowforkUpdateButton.setText("Show fork Update Button")
self.ShowforkUpdateButton.clicked.connect(self.show_fork_update_button)
self.ShowforkUpdateButton.setChecked(get_show_fork_update_button())
# Add update behavior combo
self.UpdateforkBehavior = QComboBox()
self.UpdateforkBehavior.addItems(list(update_behavior.keys()))
self.UpdateforkBehavior.setCurrentIndex(get_fork_update_behavior())
self.UpdateforkBehavior.activated[int].connect(self.change_update_fork_behavior)
Add Repository Widget¶
Update source/widgets/repo_group.py:
def __init__(self, parent=None):
self.fork_repo = RepoUserView(
"<Fork Name>",
"Fork description",
library=get_show_fork_builds(),
download=get_scrape_fork_builds(),
parent=self,
)
self.repos = [
# ... existing repos
self.fork_repo,
]
8. Handle Download and Extraction¶
Update source/widgets/download_widget.py:
def init_extractor(self, source: Path) -> None:
# ... existing branches
elif self.build_info.branch == "<fork-branch>":
dist = library_folder / "<fork-branch>"
Handle Fork-Specific Archive Structures¶
If fork uses non-standard archive structure, update source/threads/extractor.py:
def _fix_fork_structure(build_path: Path) -> Path:
"""
Fix fork-specific directory structure after extraction.
Example: Move nested folders, flatten bin/Release structures, etc.
"""
# Implement fork-specific structure fixes
return build_path
# Call in ExtractTask.run():
result = _fix_fork_structure(result)
9. Handle Fork-Specific Versioning¶
If fork uses different versioning than Blender:
Add Version Matcher¶
In source/modules/build_info.py:
def fork_version_matcher(fork_version: Version) -> Version | None:
"""
Map fork version to Blender config version.
Example: UPBGE 0.40 -> Blender 4.0
Bforartists 5.0 -> Blender 5.1
"""
versions = read_blender_version_list()
# Implement version mapping logic
matching_version = Version(major, minor)
if matching_version in versions:
return matching_version
return None
Add to BuildInfo¶
@property
def fork_version_matcher(self):
return fork_version_matcher(self.semversion)
10. Handle Config Folders¶
Fork-specific config paths are centralized in source/modules/build_info.py using the FORK_CONFIG_PATHS dictionary.
Add Fork Config to FORK_CONFIG_PATHS¶
In source/modules/build_info.py, add your fork’s config paths:
FORK_CONFIG_PATHS = {
"bforartists": {
"config_folder": "bforartists",
"config_subfolder": "bforartists",
},
"upbge": {
"config_folder": "UPBGE",
"config_subfolder": {
"Windows": "Blender",
"Linux": "upbge",
"macOS": "UPBGE",
},
},
"<fork-branch>": {
"config_folder": "<Fork Foundation>",
"config_subfolder": "<fork>",
# OR for platform-specific subfolders:
# "config_subfolder": {
# "Windows": "<windows-subfolder>",
# "Linux": "<linux-subfolder>",
# "macOS": "<macos-subfolder>",
# },
},
}
Notes:
config_folder: Used only on Windows (e.g.,%APPDATA%\<config_folder>\)config_subfolder:- Simple string: Same subfolder for all platforms (Linux:
~/.config/<subfolder>/, macOS:~/Library/Application Support/<subfolder>/) - Platform dict: Different subfolder per platform (Windows uses it too:
%APPDATA%\<config_folder>\<subfolder>\)
- Simple string: Same subfolder for all platforms (Linux:
The get_fork_config_paths(branch) function retrieves these paths automatically.
Portable Mode Support¶
Update make_portable_path() in source/widgets/library_widget.py:
def make_portable_path(self) -> Path:
branch = self.build_info.branch
if branch == "<fork-branch>" and version >= "X.Y":
folder_name = "portable"
config_path = self.link / folder_name
# ... existing logic
11. Finding Config Paths in Fork Source Code¶
When implementing support for a fork, you need to determine where the fork stores its configuration files. This information is defined in the fork’s C++ source code in platform-specific system path files.
Location of System Path Files¶
Config paths are defined in these three files in the fork’s repository:
intern/ghost/intern/GHOST_SystemPathsUnix.cc # Linux
intern/ghost/intern/GHOST_SystemPathsWin32.cc # Windows
intern/ghost/intern/GHOST_SystemPathsCocoa.mm # macOS
What to Look For¶
In each file, look for the getUserDir() function. This function returns the config folder path.
Linux (GHOST_SystemPathsUnix.cc)¶
Look for getUserDir() function. The config path is typically set in one of these patterns:
// Pattern 1: XDG_CONFIG_HOME environment variable
home = getenv("XDG_CONFIG_HOME");
if (home) {
user_path = string(home) + "/blender/" + versionstr; // <-- "blender" is the folder name
}
// Pattern 2: Fallback to ~/.config
else {
home = home_dir_get();
if (home) {
user_path = string(home) + "/.config/blender/" + versionstr; // <-- "blender" is the folder name
}
}
What to extract: The folder name after / or /.config/ (e.g., "blender", "upbge", "bforartists")
Windows (GHOST_SystemPathsWin32.cc)¶
Look for getUserDir() function. The config path uses Windows roaming app data:
HRESULT hResult = SHGetKnownFolderPath(
FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, nullptr, &knownpath_16);
if (hResult == S_OK) {
conv_utf_16_to_8(knownpath_16, knownpath, MAX_PATH * 3);
strcat(knownpath, "\\Blender Foundation\\Blender\\"); // <-- "Blender Foundation" and "Blender" are the folder names
strcat(knownpath, versionstr);
user_dir = knownpath;
}
What to extract:
- The organization name (e.g., "Blender Foundation", "UPBGE Foundation")
- The application subfolder (e.g., "Blender", "upbge", "Bforartists")
Full path example: %APPDATA%\Blender Foundation\Blender\4.3\
macOS (GHOST_SystemPathsCocoa.mm)¶
Look for getUserDir() function. It calls GetApplicationSupportDir():
static const char *GetApplicationSupportDir(const char *versionstr,
const NSSearchPathDomainMask mask,
char *tempPath,
const std::size_t len_tempPath)
{
@autoreleasepool {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, mask, YES);
NSString *basePath = [paths objectAtIndex:0];
snprintf(tempPath,
len_tempPath,
"%s/Blender/%s", // <-- "Blender" is the folder name
[basePath cStringUsingEncoding:NSASCIIStringEncoding],
versionstr);
}
return tempPath;
}
What to extract: The folder name in the snprintf format string (e.g., "Blender", "upbge", "Bforartists")
Full path example: ~/Library/Application Support/Blender/4.3/
Real-World Examples¶
Official Blender¶
- Linux:
~/.config/blender/4.3/ - Windows:
%APPDATA%\Blender Foundation\Blender\4.3\ - macOS:
~/Library/Application Support/Blender/4.3/
UPBGE¶
- Linux:
~/.config/upbge/0.40/ - Windows:
%APPDATA%\UPBGE\Blender\0.40\ - macOS:
~/Library/Application Support/UPBGE/0.40/
Bforartists¶
- Linux:
~/.config/bforartists/5.1/ - Windows:
%APPDATA%\Bforartists\bforartists\5.1\ - macOS:
~/Library/Application Support/bforartists/5.1/
Using This Information in Implementation¶
Once you’ve determined the config paths from the fork’s source code (see section 11), add them to FORK_CONFIG_PATHS in source/modules/build_info.py:
Example 1 - Same subfolder on all platforms (Bforartists):
FORK_CONFIG_PATHS = {
# ... existing entries
"bforartists": {
"config_folder": "bforartists", # Windows: %APPDATA%\bforartists\
"config_subfolder": "bforartists", # All platforms use same subfolder
},
}
Results in:
- Windows: %APPDATA%\bforartists\bforartists\5.1\
- Linux: ~/.config/bforartists/5.1/
- macOS: ~/Library/Application Support/bforartists/5.1/
Example 2 - Platform-specific subfolders (UPBGE):
FORK_CONFIG_PATHS = {
# ... existing entries
"upbge": {
"config_folder": "UPBGE", # Windows: %APPDATA%\UPBGE\
"config_subfolder": {
"Windows": "Blender", # Windows uses different subfolder
"Linux": "upbge", # Linux uses lowercase
"macOS": "UPBGE", # macOS uses uppercase
},
},
}
Results in:
- Windows: %APPDATA%\UPBGE\Blender\0.40\
- Linux: ~/.config/upbge/0.40/
- macOS: ~/Library/Application Support/UPBGE/0.40/
Key Files Reference¶
| Component | File Path |
|---|---|
| Scraper | source/threads/scraping/fork.py |
| Settings | source/modules/settings.py |
| Update Manager | source/modules/blender_update_manager.py |
| Main Window | source/windows/main_window.py |
| Settings UI | source/widgets/settings_window/blender_builds_tab.py |
| Repository Group | source/widgets/repo_group.py |
| Library Drawer | source/threads/library_drawer.py |
| Build Info & Config Paths | source/modules/build_info.py |
| Extractor | source/threads/extractor.py |
| Download Widget | source/widgets/download_widget.py |
| Library Widget | source/widgets/library_widget.py |