Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/pages/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,20 @@ Shift-click selects multiple hierarchy objects. Dropping OBJ, FBX, glTF, GLB, or
| Command palette | Command Shift P |

The command palette lists available commands and their shortcuts and supports keyboard filtering, arrow navigation, and Enter. Scripts are watched for changes and the editor reloads the runtime automatically when JavaScript or TypeScript files change.

## Packaging Atlas Engine

Run the macOS packer from the repository root:

```shell
./scripts/package_app.py --debug --macOS
./scripts/package_app.py --release --macOS
```

The equivalent `just` recipes are `just package-debug-macos` and `just package-release-macos`. Products are written below `dist/macOS/<configuration>` and intermediate files are written below `build/package`; both directories are ignored by version control.

The package is self-contained and includes Qt, the Atlas CLI, and `runtime.dylib`. On first launch, Atlas Engine offers to install the bundled CLI and runtime into `~/Library/Application Support/Atlas Engine/toolchains/alpha9` and registers them in `~/.atlas/config.json`. Installation does not require administrator access and preserves other configured Atlas versions.

Debug packages use an ad-hoc signature. A release intended for GitHub requires `ATLAS_SIGNING_IDENTITY` to name a Developer ID Application certificate and `ATLAS_NOTARY_PROFILE` to name a `notarytool` keychain profile. The packer creates, signs, notarizes, staples, mounts, and validates a drag-to-Applications DMG. It refuses to create an accidentally unnotarized release unless `ATLAS_ALLOW_UNNOTARIZED_RELEASE=1` is explicitly set; that local-only artifact is named `UNNOTARIZED`.

The packer builds the host architecture by default. Set `ATLAS_MACOS_ARCHITECTURES='arm64;x86_64'` when the selected Qt installation contains both architectures to create a universal archive. `ATLAS_MACOS_DEPLOYMENT_TARGET` changes the default macOS 14.0 deployment target, and `ATLAS_MACDEPLOYQT` can select a specific `macdeployqt` executable.
61 changes: 54 additions & 7 deletions editor/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@ file(GLOB_RECURSE ATLAS_CLI_SOURCES CONFIGURE_DEPENDS
"${CMAKE_SOURCE_DIR}/cli/src/*.rs"
)

set(ATLAS_EDITOR_CLI "${CMAKE_SOURCE_DIR}/target/debug/atlas")
if (CMAKE_BUILD_TYPE STREQUAL "Release")
set(ATLAS_EDITOR_CARGO_ARGS --release)
set(ATLAS_EDITOR_CLI "${CMAKE_SOURCE_DIR}/target/release/atlas")
else ()
set(ATLAS_EDITOR_CARGO_ARGS)
set(ATLAS_EDITOR_CLI "${CMAKE_SOURCE_DIR}/target/debug/atlas")
endif ()

add_custom_command(
OUTPUT "${ATLAS_EDITOR_CLI}"
COMMAND
"${CMAKE_COMMAND}" -E env CARGO_TERM_COLOR=always
"${ATLAS_CARGO_EXECUTABLE}" build
${ATLAS_EDITOR_CARGO_ARGS}
--manifest-path "${CMAKE_SOURCE_DIR}/cli/Cargo.toml"
DEPENDS
"${CMAKE_SOURCE_DIR}/Cargo.toml"
Expand Down Expand Up @@ -75,15 +82,54 @@ add_subdirectory(
"${CMAKE_BINARY_DIR}/extern/QtDockingSystem"
)

if (APPLE)
set(ATLAS_APP_ICON
"${CMAKE_SOURCE_DIR}/editor/assets/iconFile-iOS-Dark-1024x1024@1x.png"
CACHE FILEPATH "Atlas Engine macOS bundle icon"
)
get_filename_component(ATLAS_APP_ICON_NAME "${ATLAS_APP_ICON}" NAME)
set_source_files_properties("${ATLAS_APP_ICON}" PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources"
)
list(APPEND EDITOR_FILES "${ATLAS_APP_ICON}")
endif ()

qt_add_executable(AtlasEditor ${EDITOR_FILES})
add_dependencies(AtlasEditor generate_atlas_themes atlas_editor_cli)

add_custom_command(TARGET AtlasEditor POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${ATLAS_EDITOR_CLI}"
"$<TARGET_FILE_DIR:AtlasEditor>/atlas"
VERBATIM
)
if (APPLE)
set_target_properties(AtlasEditor PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "Atlas Engine"
MACOSX_BUNDLE_GUI_IDENTIFIER "neutralsoftware.atlas"
MACOSX_BUNDLE_ICON_FILE "${ATLAS_APP_ICON_NAME}"
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/editor/macos/Info.plist.in"
MACOSX_BUNDLE_SHORT_VERSION_STRING "0.9.0"
MACOSX_BUNDLE_BUNDLE_VERSION "10"
OUTPUT_NAME "Atlas Engine"
BUILD_RPATH "@executable_path/../Frameworks"
INSTALL_RPATH "@executable_path/../Frameworks"
)
add_custom_command(TARGET AtlasEditor POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory
"$<TARGET_BUNDLE_DIR:AtlasEditor>/Contents/Helpers"
"$<TARGET_BUNDLE_DIR:AtlasEditor>/Contents/Frameworks"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${ATLAS_EDITOR_CLI}"
"$<TARGET_BUNDLE_DIR:AtlasEditor>/Contents/Helpers/atlas"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<TARGET_FILE:runtime_lib>"
"$<TARGET_BUNDLE_DIR:AtlasEditor>/Contents/Frameworks/runtime.dylib"
VERBATIM
)
else ()
add_custom_command(TARGET AtlasEditor POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${ATLAS_EDITOR_CLI}"
"$<TARGET_FILE_DIR:AtlasEditor>/atlas"
VERBATIM
)
endif ()

qt_add_resources(AtlasEditor "atlas_editor_assets"
PREFIX "/editor"
Expand All @@ -105,6 +151,7 @@ target_include_directories(AtlasEditor
string(TIMESTAMP ATLAS_EDITOR_BUILD_DATE "%Y%m%d")

target_compile_definitions(AtlasEditor PRIVATE
ATLAS_TOOLCHAIN_VERSION="alpha9"
"$<$<CONFIG:Debug>:ATLAS_DEBUG_BUILD>"
"$<$<CONFIG:Debug>:ATLAS_BUILD_STRING=\"${ATLAS_EDITOR_BUILD_DATE}\">"
)
Expand Down
235 changes: 235 additions & 0 deletions editor/application/toolchainInstaller.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
#include "editor/application/toolchainInstaller.h"

#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QPushButton>
#include <QSaveFile>
#include <QSettings>
#include <QStandardPaths>

namespace {
struct ToolchainPaths {
QString bundledCli;
QString bundledRuntime;
QString installedCli;
QString installedRuntime;
QString config;
};

QByteArray digest(const QString& path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return {};
QCryptographicHash hash(QCryptographicHash::Sha256);
if (!hash.addData(&file))
return {};
return hash.result();
}

bool filesMatch(const QString& left, const QString& right) {
const QFileInfo leftInfo(left);
const QFileInfo rightInfo(right);
return leftInfo.isFile() && rightInfo.isFile() &&
leftInfo.size() == rightInfo.size() && digest(left) == digest(right);
}

ToolchainPaths paths() {
const QDir contents(QCoreApplication::applicationDirPath() + "/..");
const QString installRoot =
QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
"/Atlas Engine/toolchains/" + ATLAS_TOOLCHAIN_VERSION;
const QString home = QDir::homePath();
return {
contents.filePath("Helpers/atlas"),
contents.filePath("Frameworks/runtime.dylib"),
installRoot + "/bin/atlas",
installRoot + "/lib/runtime.dylib",
home + "/.atlas/config.json",
};
}

QJsonObject readConfig(const QString& path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return {};
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
return document.isObject() ? document.object() : QJsonObject();
}

bool configMatches(const ToolchainPaths& toolchain) {
const QJsonObject root = readConfig(toolchain.config);
const QJsonObject entry = root.value(ATLAS_TOOLCHAIN_VERSION).toObject();
const QJsonObject onboarding = entry.value("onboardingData").toObject();
return onboarding.value("atlasExecutablePath").toString() ==
toolchain.installedCli &&
onboarding.value("runtimeLib").toString() ==
toolchain.installedRuntime;
}

bool copyFile(const QString& source, const QString& destination,
QString* error) {
QFile input(source);
if (!input.open(QIODevice::ReadOnly)) {
*error = input.errorString();
return false;
}
QSaveFile output(destination);
if (!output.open(QIODevice::WriteOnly)) {
*error = output.errorString();
return false;
}
while (!input.atEnd()) {
const QByteArray data = input.read(1024 * 1024);
if (data.isEmpty() && input.error() != QFileDevice::NoError) {
*error = input.errorString();
return false;
}
if (output.write(data) != data.size()) {
*error = output.errorString();
return false;
}
}
if (!output.commit()) {
*error = output.errorString();
return false;
}
if (!QFile::setPermissions(destination, QFile::permissions(source))) {
*error = QStringLiteral("Could not apply permissions to %1")
.arg(destination);
return false;
}
return true;
}

bool writeConfig(const ToolchainPaths& toolchain, QString* error) {
QJsonObject root = readConfig(toolchain.config);
QJsonObject entry = root.value(ATLAS_TOOLCHAIN_VERSION).toObject();
QJsonObject onboarding = entry.value("onboardingData").toObject();
onboarding.insert("atlasExecutablePath", toolchain.installedCli);
onboarding.insert("runtimeLib", toolchain.installedRuntime);
entry.insert("onboardingData", onboarding);
root.insert(ATLAS_TOOLCHAIN_VERSION, entry);

const QFileInfo configInfo(toolchain.config);
if (!QDir().mkpath(configInfo.absolutePath())) {
*error = QStringLiteral("Could not create %1")
.arg(configInfo.absolutePath());
return false;
}
QSaveFile output(toolchain.config);
if (!output.open(QIODevice::WriteOnly)) {
*error = output.errorString();
return false;
}
const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Indented);
if (output.write(data) != data.size() || !output.commit()) {
*error = output.errorString();
return false;
}
return true;
}

bool isInstalled(const ToolchainPaths& toolchain) {
return filesMatch(toolchain.bundledCli, toolchain.installedCli) &&
filesMatch(toolchain.bundledRuntime, toolchain.installedRuntime) &&
configMatches(toolchain);
}

bool promptDismissed() {
QSettings settings("Neutral Software", "Atlas Engine");
return settings.value("toolchain/installationPromptDismissed", false)
.toBool();
}

void setPromptDismissed(bool dismissed) {
QSettings settings("Neutral Software", "Atlas Engine");
settings.setValue("toolchain/installationPromptDismissed", dismissed);
settings.sync();
}

bool installToolchain(const ToolchainPaths& toolchain, QWidget* parent,
bool reportExistingInstallation) {
if (isInstalled(toolchain)) {
setPromptDismissed(false);
if (reportExistingInstallation) {
QMessageBox::information(
parent, "Atlas Toolchain Ready",
"The bundled Atlas toolchain is already installed for this user.");
}
return true;
}

const QFileInfo cliInfo(toolchain.installedCli);
const QFileInfo runtimeInfo(toolchain.installedRuntime);
QString error;
if (!QDir().mkpath(cliInfo.absolutePath()) ||
!QDir().mkpath(runtimeInfo.absolutePath())) {
error = "Could not create the Atlas toolchain directory.";
} else if (!copyFile(toolchain.bundledCli, toolchain.installedCli,
&error) ||
!copyFile(toolchain.bundledRuntime,
toolchain.installedRuntime, &error) ||
!writeConfig(toolchain, &error)) {
}
if (!error.isEmpty()) {
QMessageBox::critical(parent, "Toolchain Installation Failed", error);
return false;
}

setPromptDismissed(false);
QMessageBox::information(
parent, "Atlas Toolchain Installed",
"The Atlas toolchain is ready. Projects can now be created, run, and exported from Atlas Engine.");
return true;
}
}

bool ToolchainInstaller::ensureInstalled(QWidget* parent) {
const ToolchainPaths toolchain = paths();
if (!QFileInfo::exists(toolchain.bundledCli) ||
!QFileInfo::exists(toolchain.bundledRuntime))
return true;

if (isInstalled(toolchain)) {
setPromptDismissed(false);
return true;
}
if (promptDismissed())
return false;

QMessageBox prompt(parent);
prompt.setWindowTitle("Welcome to Atlas Engine");
prompt.setIcon(QMessageBox::Information);
prompt.setText("Install the Atlas toolchain for this user?");
prompt.setInformativeText(
"Atlas Engine includes the command-line tools and runtime required to create, run, and export projects. They will be installed in your user Library and do not require administrator access.");
auto* installButton = prompt.addButton("Install Toolchain",
QMessageBox::AcceptRole);
prompt.addButton("Not Now", QMessageBox::RejectRole);
prompt.setDefaultButton(installButton);
prompt.exec();
if (prompt.clickedButton() != installButton) {
setPromptDismissed(true);
return false;
}

return installToolchain(toolchain, parent, false);
}

bool ToolchainInstaller::install(QWidget* parent) {
const ToolchainPaths toolchain = paths();
if (!QFileInfo::exists(toolchain.bundledCli) ||
!QFileInfo::exists(toolchain.bundledRuntime)) {
QMessageBox::warning(
parent, "Atlas Toolchain Unavailable",
"This copy of Atlas Engine does not include the packaged toolchain.");
return false;
}
return installToolchain(toolchain, parent, true);
}
43 changes: 43 additions & 0 deletions editor/assets/AtlasEngine.icon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"fill": {
"linear-gradient": [
"srgb:1.00000,0.95276,0.98370,1.00000",
"srgb:0.69138,0.67074,0.76447,1.00000"
],
"orientation": {
"start": {
"x": 0.5,
"y": 0
},
"stop": {
"x": 0.5,
"y": 0.7
}
}
},
"groups": [
{
"layers": [
{
"glass": true,
"image-name": "atlas_ball_bright.png",
"name": "atlas_ball_bright"
}
],
"shadow": {
"kind": "neutral",
"opacity": 0.5
},
"translucency": {
"enabled": true,
"value": 0.5
}
}
],
"supported-platforms": {
"circles": [
"watchOS"
],
"squares": "shared"
}
}
Loading
Loading