static: replace install
All checks were successful
Static / Flake checks (push) Successful in 21s
Static / Create distribution (push) Successful in 44s

Also get rid of many unused pages and assets.
This commit is contained in:
2025-06-29 03:14:12 +09:00
parent ae2ce60893
commit ae9ca7d568
21 changed files with 53 additions and 2061 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,106 +0,0 @@
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
// Client-side redirects for fragments (anchors)
//
// It should be possible to do this with either HTML or server-side redirects, but it was never
// implemented or standardized. For reference:
//
// https://www.w3.org/People/Bos/redirect
// https://www.w3.org/Protocols/HTTP/Fragment/draft-bos-http-redirect-00.txt
const redirects = new Map([
// removed main page sections
["/#copyright-and-licensing", "/faq#copyright-and-licensing"],
["/#history", "/history"],
["/#roadmap", "/faq#roadmap"],
["/#upstream", "/faq#upstream"],
["/usage#default-connections", "/faq#default-connections"],
["/usage#sandboxed-google-play-esim", "/usage#esim-support"],
["/usage#sandboxed-play-services", "/usage#sandboxed-google-play"],
["/usage#sandboxed-play-services-installation", "/usage#sandboxed-google-play-installation"],
["/usage#sandboxed-play-services-limitations", "/usage#sandboxed-google-play-limitations"],
["/usage#google-camera", "/usage#pixel-camera"],
["/usage#usb-peripherals", "/usage#usb-c-port-and-pogo-pins-control"],
["/faq#dns", "/faq#custom-dns"],
["/faq#when-devices", "/faq#future-devices"],
["/features#usb-c-port-control", "/features#usb-c-port-and-pogo-pins-control"],
["/features#Two-factor-fingerprint-unlock", "/features#two-factor-fingerprint-unlock"],
["/hiring#qualitifations", "/hiring#qualifications"],
["/install/cli#fastboot-as-non-root", "/install/cli#flashing-as-non-root"],
["/install/cli#obtaining-signify", "/install/cli#obtaining-openssh"],
["/install/web#fastboot-as-non-root", "/install/web#flashing-as-non-root"],
["/install/cli#working-around-fwupd-bug-on-linux-distributions", "/install/cli#working-around-fwupd-bugs-on-linux-distributions"],
["/install/web#working-around-fwupd-bug-on-linux-distributions", "/install/web#working-around-fwupd-bugs-on-linux-distributions"],
["/build#enabling-updatable-apex-components", "/build#apex-components"],
["/build#kernel-6th-generation-pixels", "/build#kernel-6th-through-9th-generation-pixels"],
["/build#kernel-7th-generation-pixels", "/build#kernel-6th-through-9th-generation-pixels"],
["/build#kernel-6th-and-7th-generation-pixels", "/build#kernel-6th-through-9th-generation-pixels"],
["/build#kernel-8th-generation-pixels", "/build#kernel-6th-through-9th-generation-pixels"],
["/build#kernel-9th-generation-pixels", "/build#kernel-6th-through-9th-generation-pixels"],
// legacy devices
["/releases#marlin-stable", "/faq#legacy-devices"],
["/releases#marlin-beta", "/faq#legacy-devices"],
["/releases#sailfish-stable", "/faq#legacy-devices"],
["/releases#sailfish-beta", "/faq#legacy-devices"],
["/releases#taimen-stable", "/faq#legacy-devices"],
["/releases#taimen-beta", "/faq#legacy-devices"],
["/releases#walleye-stable", "/faq#legacy-devices"],
["/releases#walleye-beta", "/faq#legacy-devices"],
["/releases#bonito-stable", "/faq#legacy-devices"],
["/releases#bonito-beta", "/faq#legacy-devices"],
["/releases#sargo-stable", "/faq#legacy-devices"],
["/releases#sargo-beta", "/faq#legacy-devices"],
["/releases#crosshatch-stable", "/faq#legacy-devices"],
["/releases#crosshatch-beta", "/faq#legacy-devices"],
["/releases#blueline-stable", "/faq#legacy-devices"],
["/releases#blueline-beta", "/faq#legacy-devices"],
// legacy servers
["/articles/grapheneos-servers#apps.grapheneos.org", "/articles/grapheneos-servers#releases.grapheneos.org"],
["/articles/grapheneos-servers#time.grapheneos.org", "/articles/grapheneos-servers#grapheneos.network"],
// preserve links to CLI install guide from when it was /install
["/install/#prerequisites", "/install/cli#prerequisites"],
["/install/#enabling-oem-unlocking", "/install/cli#enabling-oem-unlocking"],
["/install/#opening-terminal", "/install/cli#opening-terminal"],
["/install/#obtaining-fastboot", "/install/cli#obtaining-fastboot"],
["/install/#standalone-platform-tools", "/install/cli#standalone-platform-tools"],
["/install/#checking-fastboot-version", "/install/cli#checking-fastboot-version"],
["/install/#fastboot-as-non-root", "/install/cli#flashing-as-non-root"],
["/install/#connecting-phone", "/install/cli#connecting-phone"],
["/install/#unlocking-the-bootloader", "/install/cli#unlocking-the-bootloader"],
["/install/#obtaining-signify", "/install/cli#obtaining-openssh"],
["/install/#obtaining-factory-images", "/install/cli#obtaining-factory-images"],
["/install/#flashing-factory-images", "/install/cli#flashing-factory-images"],
["/install/#troubleshooting", "/install/cli#troubleshooting"],
["/install/#locking-the-bootloader", "/install/cli#locking-the-bootloader"],
["/install/#post-installation", "/install/cli#post-installation"],
["/install/#booting", "/install/cli#booting"],
["/install/#disabling-oem-unlocking", "/install/cli#disabling-oem-unlocking"],
["/install/#replacing-grapheneos-with-the-stock-os", "/install/cli#replacing-grapheneos-with-the-stock-os"],
["/install/#further-information", "/install/cli#further-information"],
["/install/web#connecting-phone", "/install/web#connecting-device"],
["/install/cli#connecting-phone", "/install/cli#connecting-device"],
]);
function handleHash() {
if (window.location.hash) {
const redirect = redirects.get(window.location.pathname + window.location.hash);
if (redirect) {
window.location.replace(redirect);
}
}
}
handleHash();
addEventListener("hashchange", handleHash, false);
// @license-end

View File

@@ -1,49 +0,0 @@
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
const baseUrl = "https://releases.grapheneos.org/";
const devices = ["comet", "komodo", "caiman", "tokay", "akita", "husky", "shiba", "felix", "tangorpro", "lynx", "cheetah", "panther", "bluejay", "raven", "oriole", "barbet", "redfin", "bramble", "sunfish", "coral", "flame"];
const legacyFactoryDevices = new Set(["sunfish", "coral", "flame"]);
const channels = ["stable", "beta", "alpha"];
const delayMs = 1000 * 60 * 5;
async function updateReleases() {
const requests = [];
for (const channel of channels) {
for (const device of devices) {
requests.push(fetch(`${baseUrl}${device}-${channel}`).then(response => {
if (!response.ok) {
return Promise.reject();
}
return response.text();
}).then(text => {
const metadata = text.trim().split(" ");
const factoryFormat = legacyFactoryDevices.has(device) ? "factory" : "install";
const factoryFilename = `${device}-${factoryFormat}-${metadata[0]}.zip`;
const factoryUrl = baseUrl + factoryFilename;
const updateFilename = `${device}-ota_update-${metadata[0]}.zip`;
const updateUrl = baseUrl + updateFilename;
const release = document.getElementById(`${device}-${channel}`);
const links = release.querySelectorAll("a, span");
links[0].textContent = metadata[0];
if (links[0].nodeName == "A") {
links[0].setAttribute("href", "#" + metadata[0]);
}
links[1].setAttribute("href", factoryUrl);
links[2].setAttribute("href", factoryUrl + ".sig");
links[3].setAttribute("href", updateUrl);
}));
}
}
await Promise.allSettled(requests);
setTimeout(updateReleases, delayMs);
}
setTimeout(updateReleases, delayMs);
// @license-end

View File

@@ -1,475 +0,0 @@
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
import * as fastboot from "./fastboot/ffe7e270/fastboot.min.mjs";
const RELEASES_URL = "https://releases.grapheneos.org";
const CACHE_DB_NAME = "BlobStore";
const CACHE_DB_VERSION = 1;
const Buttons = {
UNLOCK_BOOTLOADER: "unlock-bootloader",
DOWNLOAD_RELEASE: "download-release",
FLASH_RELEASE: "flash-release",
LOCK_BOOTLOADER: "lock-bootloader",
REMOVE_CUSTOM_KEY: "remove-custom-key"
};
const InstallerState = {
DOWNLOADING_RELEASE: 0x1,
INSTALLING_RELEASE: 0x2
};
let wakeLock = null;
const requestWakeLock = async () => {
try {
wakeLock = await navigator.wakeLock.request("screen");
console.log("Wake lock has been set");
wakeLock.addEventListener("release", async () => {
console.log("Wake lock has been released");
});
} catch (err) {
// if wake lock request fails - usually system related, such as battery
throw new Error(`${err.name}, ${err.message}`);
}
};
const releaseWakeLock = async () => {
if (wakeLock !== null) {
wakeLock.release().then(() => {
wakeLock = null;
});
}
};
// reacquires the wake lock should the visibility of the document change and the wake lock is released
document.addEventListener("visibilitychange", async () => {
if (wakeLock !== null && document.visibilityState === "visible") {
await requestWakeLock();
}
});
// This wraps XHR because getting progress updates with fetch() is overly complicated.
function fetchBlobWithProgress(url, onProgress) {
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.send();
return new Promise((resolve, reject) => {
xhr.onload = () => {
resolve(xhr.response);
};
xhr.onprogress = (event) => {
onProgress(event.loaded / event.total);
};
xhr.onerror = () => {
reject(`${xhr.status} ${xhr.statusText}`);
};
});
}
function setButtonState({ id, enabled }) {
const button = document.getElementById(`${id}-button`);
button.disabled = !enabled;
return button;
}
class BlobStore {
constructor() {
this.db = null;
}
async _wrapReq(request, onUpgrade = null) {
return new Promise((resolve, reject) => {
request.onsuccess = () => {
resolve(request.result);
};
request.oncomplete = () => {
resolve(request.result);
};
request.onerror = (event) => {
reject(event);
};
if (onUpgrade !== null) {
request.onupgradeneeded = onUpgrade;
}
});
}
async init() {
if (this.db === null) {
this.db = await this._wrapReq(
indexedDB.open(CACHE_DB_NAME, CACHE_DB_VERSION),
(event) => {
let db = event.target.result;
db.createObjectStore("files", { keyPath: "name" });
/* no index needed for such a small database */
}
);
}
}
async saveFile(name, blob) {
await this._wrapReq(
this.db.transaction(["files"], "readwrite").objectStore("files").add({
name: name,
blob: blob,
})
);
}
async loadFile(name) {
try {
let obj = await this._wrapReq(
this.db.transaction("files").objectStore("files").get(name)
);
return obj.blob;
} catch {
return null;
}
}
async close() {
this.db.close();
}
async download(url, onProgress = () => {}) {
let filename = url.split("/").pop();
let blob = await this.loadFile(filename);
if (blob === null) {
console.log(`Downloading ${url}`);
let blob = await fetchBlobWithProgress(url, onProgress);
console.log("File downloaded, saving...");
await this.saveFile(filename, blob);
console.log("File saved");
} else {
console.log(
`Loaded ${filename} from blob store, skipping download`
);
}
return blob;
}
}
class ButtonController {
#map;
constructor() {
this.#map = new Map();
}
setEnabled(...ids) {
ids.forEach((id) => {
// Only enable button if it won't be disabled.
if (!this.#map.has(id)) {
this.#map.set(id, /* enabled = */ true);
}
});
}
setDisabled(...ids) {
ids.forEach((id) => this.#map.set(id, /* enabled = */ false));
}
applyState() {
this.#map.forEach((enabled, id) => {
setButtonState({ id, enabled });
});
this.#map.clear();
}
}
let installerState = 0;
let device = new fastboot.FastbootDevice();
let blobStore = new BlobStore();
let buttonController = new ButtonController();
async function ensureConnected(setProgress) {
if (!device.isConnected) {
setProgress("Connecting to device...");
await device.connect();
}
}
async function unlockBootloader(setProgress) {
await ensureConnected(setProgress);
// Trying to unlock when the bootloader is already unlocked results in a FAIL,
// so don't try to do it.
if (await device.getVariable("unlocked") === "yes") {
return "Bootloader is already unlocked.";
}
setProgress("Unlocking bootloader...");
try {
await device.runCommand("flashing unlock");
} catch (error) {
// FAIL = user rejected unlock
if (error instanceof fastboot.FastbootError && error.status === "FAIL") {
throw new Error("Bootloader was not unlocked, please try again!");
} else {
throw error;
}
}
return "Bootloader unlocking triggered successfully.";
}
const supportedDevices = ["tegu", "comet", "komodo", "caiman", "tokay", "akita", "husky", "shiba", "felix", "tangorpro", "lynx", "cheetah", "panther", "bluejay", "raven", "oriole", "barbet", "redfin", "bramble", "sunfish", "coral", "flame"];
const legacyQualcommDevices = ["sunfish", "coral", "flame"];
const day1SnapshotCancelDevices = ["tegu", "comet", "komodo", "caiman", "tokay", "akita", "husky", "shiba", "felix", "tangorpro", "lynx", "cheetah", "panther", "bluejay", "raven", "oriole", "barbet", "redfin", "bramble"];
function hasOptimizedFactoryImage(product) {
return !legacyQualcommDevices.includes(product);
}
async function getLatestRelease() {
let product = await device.getVariable("product");
if (!supportedDevices.includes(product)) {
throw new Error(`device model (${product}) is not supported by the GrapheneOS web installer`);
}
let metadataResp = await fetch(`${RELEASES_URL}/${product}-stable`);
let metadata = await metadataResp.text();
let releaseId = metadata.split(" ")[0];
return [`${product}-${hasOptimizedFactoryImage(product) ? "install" : "factory"}-${releaseId}.zip`, product];
}
async function downloadRelease(setProgress) {
await requestWakeLock();
await ensureConnected(setProgress);
setProgress("Finding latest release...");
let [latestZip,] = await getLatestRelease();
// Download and cache the zip as a blob
setInstallerState({ state: InstallerState.DOWNLOADING_RELEASE, active: true });
setProgress(`Downloading ${latestZip}...`);
await blobStore.init();
try {
await blobStore.download(`${RELEASES_URL}/${latestZip}`, (progress) => {
setProgress(`Downloading ${latestZip}...`, progress);
});
} finally {
setInstallerState({ state: InstallerState.DOWNLOADING_RELEASE, active: false });
await releaseWakeLock();
}
setProgress(`Downloaded ${latestZip} release.`, 1.0);
}
async function reconnectCallback() {
let statusField = document.getElementById("flash-release-status");
statusField.textContent =
"To continue flashing, reconnect the device by tapping here:";
let reconnectButton = document.getElementById("flash-reconnect-button");
let progressBar = document.getElementById("flash-release-progress");
// Hide progress bar while waiting for reconnection
progressBar.hidden = true;
reconnectButton.hidden = false;
reconnectButton.onclick = async () => {
await device.connect();
reconnectButton.hidden = true;
progressBar.hidden = false;
};
}
async function flashRelease(setProgress) {
await requestWakeLock();
await ensureConnected(setProgress);
// Need to do this again because the user may not have clicked download if
// it was cached
setProgress("Finding latest release...");
let [latestZip, product] = await getLatestRelease();
await blobStore.init();
let blob = await blobStore.loadFile(latestZip);
if (blob === null) {
throw new Error("You need to download a release first!");
}
setProgress("Cancelling any pending OTAs...");
// Cancel snapshot update if in progress on devices which support it on all bootloader versions
if (day1SnapshotCancelDevices.includes(product)) {
let snapshotStatus = await device.getVariable("snapshot-update-status");
if (snapshotStatus !== null && snapshotStatus !== "none") {
await device.runCommand("snapshot-update:cancel");
}
}
setProgress("Flashing release...");
setInstallerState({ state: InstallerState.INSTALLING_RELEASE, active: true });
try {
await device.flashFactoryZip(blob, true, reconnectCallback,
(action, item, progress) => {
let userAction = fastboot.USER_ACTION_MAP[action];
let userItem = item === "avb_custom_key" ? "verified boot key" : item;
setProgress(`${userAction} ${userItem}...`, progress);
}
);
if (legacyQualcommDevices.includes(product)) {
setProgress("Disabling UART...");
// See https://android.googlesource.com/platform/system/core/+/eclair-release/fastboot/fastboot.c#532
// for context as to why the trailing space is needed.
await device.runCommand("oem uart disable ");
setProgress("Erasing apdp...");
// Both slots are wiped as even apdp on an inactive slot will modify /proc/cmdline
await device.runCommand("erase:apdp_a");
await device.runCommand("erase:apdp_b");
setProgress("Erasing msadp...");
await device.runCommand("erase:msadp_a");
await device.runCommand("erase:msadp_b");
}
} finally {
setInstallerState({ state: InstallerState.INSTALLING_RELEASE, active: false });
await releaseWakeLock();
}
return `Flashed ${latestZip} to device.`;
}
async function eraseNonStockKey(setProgress) {
await ensureConnected(setProgress);
setProgress("Erasing key...");
try {
await device.runCommand("erase:avb_custom_key");
} catch (error) {
console.log(error);
throw error;
}
return "Key erased.";
}
async function lockBootloader(setProgress) {
await ensureConnected(setProgress);
setProgress("Locking bootloader...");
try {
await device.runCommand("flashing lock");
} catch (error) {
// FAIL = user rejected lock
if (error instanceof fastboot.FastbootError && error.status === "FAIL") {
throw new Error("Bootloader was not locked, please try again!");
} else {
throw error;
}
}
return "Bootloader locking triggered successfully.";
}
function addButtonHook(id, callback) {
let statusContainer = document.getElementById(`${id}-status-container`);
let statusField = document.getElementById(`${id}-status`);
let progressBar = document.getElementById(`${id}-progress`);
let statusCallback = (status, progress) => {
if (statusContainer !== null) {
statusContainer.hidden = false;
}
statusField.className = "";
statusField.textContent = status;
if (progress !== undefined) {
progressBar.hidden = false;
progressBar.value = progress;
}
};
let button = setButtonState({ id, enabled: true });
button.onclick = async () => {
try {
let finalStatus = await callback(statusCallback);
if (finalStatus !== undefined) {
statusCallback(finalStatus);
}
} catch (error) {
statusCallback(`Error: ${error.message}`);
statusField.className = "error-text";
await releaseWakeLock();
// Rethrow the error so it shows up in the console
throw error;
}
};
}
function setInstallerState({ state, active }) {
if (active) {
installerState |= state;
} else {
installerState &= ~state;
}
invalidateInstallerState();
}
function isInstallerStateActive(state) {
return (installerState & state) === state;
}
function invalidateInstallerState() {
if (isInstallerStateActive(InstallerState.DOWNLOADING_RELEASE)) {
buttonController.setDisabled(Buttons.DOWNLOAD_RELEASE);
} else {
buttonController.setEnabled(Buttons.DOWNLOAD_RELEASE);
}
let disableWhileInstalling = [
Buttons.DOWNLOAD_RELEASE,
Buttons.FLASH_RELEASE,
Buttons.LOCK_BOOTLOADER,
Buttons.REMOVE_CUSTOM_KEY,
];
if (isInstallerStateActive(InstallerState.INSTALLING_RELEASE)) {
buttonController.setDisabled(...disableWhileInstalling);
} else {
buttonController.setEnabled(...disableWhileInstalling);
}
buttonController.applyState();
}
function safeToLeave() {
return installerState === 0;
}
// This doesn't really hurt, and because this page is exclusively for web install,
// we can tolerate extra logging in the console in case something goes wrong.
fastboot.setDebugLevel(2);
fastboot.configureZip({
workerScripts: {
inflate: ["/js/fastboot/ffe7e270/vendor/z-worker-pako.js", "pako_inflate.min.js"],
},
});
if ("usb" in navigator) {
addButtonHook(Buttons.UNLOCK_BOOTLOADER, unlockBootloader);
addButtonHook(Buttons.DOWNLOAD_RELEASE, downloadRelease);
addButtonHook(Buttons.FLASH_RELEASE, flashRelease);
addButtonHook(Buttons.LOCK_BOOTLOADER, lockBootloader);
addButtonHook(Buttons.REMOVE_CUSTOM_KEY, eraseNonStockKey);
} else {
console.log("WebUSB unavailable");
}
// This will create an alert box to stop the user from leaving the page during actions
window.addEventListener("beforeunload", event => {
if (!safeToLeave()) {
console.log("User tried to leave the page whilst unsafe to leave!");
event.returnValue = "";
}
});
// @license-end