Use Gitea Releases for installation and ship VS Code highlighting

This commit is contained in:
Antigravity committed 2026-10-06 14:12:58 +00:00
1 parent a46bd6e132
commit 201bdee372
243 files changed
+3289 -12521

No files matched your search

+5 -4
View File
@@ -46,13 +46,14 @@ pub fn execute_in(layout: &Layout, arguments: &[OsString]) -> Result<CommandResu
}
[command, requested] if command == "install" => {
let requested = utf8(requested, "version")?;
super::release_source::prepare(layout, requested)?;
let version = manager.install_requested(requested)?;
let version = super::release_source::prepare(layout, requested)?;
let version = manager.install_requested(version.as_str())?;
Ok(CommandResult::Message(format!("installed Dever {version}")))
}
[command] if command == "update" => {
super::release_source::prepare(layout, "latest")?;
let version = manager.update()?;
let source = super::release_source::ReleaseSource::official();
let version = super::release_source::prepare_from(layout, "latest", &source)?;
let version = manager.update_version_with(&version, &source)?;
Ok(CommandResult::Message(format!(
"updated Dever and dever-language skill to {version}"
)))
+13 -5
View File
@@ -318,27 +318,35 @@ impl MachineManager {
/// Uses the ordinary install transaction with an explicitly supplied author transport.
pub fn update_with(&self, source: &dyn super::ExtensionSource) -> Result<Version, String> {
let version = self.releases.latest()?;
self.update_version_with(&version, source)
}
pub(super) fn update_version_with(
&self,
version: &Version,
source: &dyn super::ExtensionSource,
) -> Result<Version, String> {
let _lock = InstallLock::acquire(&self.layout)?;
#[cfg(target_os = "linux")]
super::bootstrap::require_no_pending(&self.layout)?;
if self
.active_version()?
.is_some_and(|active| version < active)
.is_some_and(|active| version < &active)
{
return Err(format!(
"refusing to downgrade active Dever with 'dever update' to {version}; use 'dever use {version}' for an explicit installed-version switch"
));
}
self.cleanup_staging()?;
self.install_locked(&version)?;
self.install_locked(version)?;
if let Some(active) = self.active_version()? {
let resources = super::extensions::SignedResources::load(&self.layout, &active)?;
for extension in resources.installed_extensions()? {
super::extensions::ensure_locked(&self.layout, &version, &extension, source)?;
super::extensions::ensure_locked(&self.layout, version, &extension, source)?;
}
}
self.activate_locked(&version)?;
Ok(version)
self.activate_locked(version)?;
Ok(version.clone())
}
pub fn activate(&self, version: &Version) -> Result<(), String> {
+177 -48
View File
@@ -1,13 +1,15 @@
//! Explicit installation/update downloads. Project commands never enter here.
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use super::extensions::Extension;
#[cfg(test)]
use super::extensions::PAYLOAD_LIMIT as RELEASE_LIMIT;
#[cfg(test)]
use super::release::ReleaseManifest;
@@ -18,14 +20,87 @@ use super::release::{
};
use super::validate_catalog;
const OFFICIAL_RELEASES: &str = "https://git.dever.cc/shemic/dever/dist";
const OFFICIAL_RELEASES: &str = "https://git.dever.cc/api/v1/repos/shemic/dever/releases";
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(300);
const METADATA_LIMIT: u64 = 2 * 1024 * 1024;
const ASSET_LIMIT: usize = 512;
static NEXT_DOWNLOAD: AtomicU64 = AtomicU64::new(0);
pub(super) struct ReleaseSource {
base: String,
base: url::Url,
agent: ureq::Agent,
releases: RefCell<BTreeMap<String, Rc<ResolvedRelease>>>,
}
#[derive(serde::Deserialize)]
struct ReleaseMetadata {
tag_name: String,
draft: bool,
prerelease: bool,
assets: Vec<ReleaseAsset>,
}
#[derive(serde::Deserialize)]
struct ReleaseAsset {
name: String,
browser_download_url: String,
size: u64,
}
struct ResolvedRelease {
version: Version,
assets: Vec<ReleaseAsset>,
}
impl ResolvedRelease {
fn parse(bytes: &[u8], requested: &str) -> Result<Self, String> {
let metadata: ReleaseMetadata =
serde_json::from_slice(bytes).map_err(|_| "invalid official release metadata")?;
if metadata.draft || metadata.prerelease {
return Err("official release must be published and stable".into());
}
let version = Version::parse(
metadata
.tag_name
.strip_prefix('v')
.ok_or("invalid official release tag")?,
)?;
if requested != "latest" && version.as_str() != requested {
return Err("official release tag does not match the requested version".into());
}
if metadata.assets.is_empty() || metadata.assets.len() > ASSET_LIMIT {
return Err("official release asset count is outside its limits".into());
}
let mut names = BTreeSet::new();
for asset in &metadata.assets {
if asset.name.is_empty()
|| asset.name.len() > 255
|| !asset
.name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"-_.".contains(&byte))
|| asset.size == 0
|| asset.size > RELEASE_LIMIT
{
return Err("invalid official release asset metadata".into());
}
if !names.insert(&asset.name) {
return Err("official release contains duplicate asset names".into());
}
}
Ok(Self {
version,
assets: metadata.assets,
})
}
fn asset(&self, suffix: &str) -> Result<&ReleaseAsset, String> {
let name = format!("dever-{}.{suffix}", platform_identity());
self.assets
.iter()
.find(|asset| asset.name == name)
.ok_or_else(|| format!("official release is missing required asset '{name}'"))
}
}
impl ReleaseSource {
@@ -35,7 +110,8 @@ impl ReleaseSource {
fn new(base: String) -> Self {
Self {
base,
base: url::Url::parse(&base).expect("release API origin is compiled in"),
releases: RefCell::new(BTreeMap::new()),
agent: ureq::Agent::new_with_config(
ureq::Agent::config_builder()
.proxy(None)
@@ -46,19 +122,33 @@ impl ReleaseSource {
}
}
fn asset(&self, requested: &str, suffix: &str) -> String {
format!(
"{}/{requested}/dever-{}.{suffix}",
self.base,
platform_identity()
)
fn resolve(&self, requested: &str, started: Instant) -> Result<Rc<ResolvedRelease>, String> {
let selection = if requested == "latest" {
"latest".to_owned()
} else {
Version::parse(requested)?;
format!("tags/v{requested}")
};
if let Some(release) = self.releases.borrow().get(requested) {
return Ok(Rc::clone(release));
}
let bytes = self.read_metadata(format!("{}/{selection}", self.base), started)?;
let release = Rc::new(ResolvedRelease::parse(&bytes, requested)?);
for asset in &release.assets {
validate_download_url(&asset.browser_download_url, &self.base)?;
}
let mut releases = self.releases.borrow_mut();
releases.insert(release.version.to_string(), Rc::clone(&release));
releases.insert(requested.to_owned(), Rc::clone(&release));
Ok(release)
}
fn response(
&self,
mut address: String,
address: String,
started: Instant,
) -> Result<ureq::http::Response<ureq::Body>, String> {
let mut address: String = validate_download_url(&address, &self.base)?.into();
for _ in 0..=5 {
let remaining = DOWNLOAD_TIMEOUT
.checked_sub(started.elapsed())
@@ -94,19 +184,13 @@ impl ReleaseSource {
if locations.next().is_some() {
return Err("release redirect has multiple Locations".into());
}
address = redirect(&address, location)?;
address = redirect(&address, location, &self.base)?;
}
Err("official release download exceeded its redirect limit".into())
}
fn metadata(
&self,
requested: &str,
suffix: &str,
path: &Path,
started: Instant,
) -> Result<(), String> {
let mut response = self.response(self.asset(requested, suffix), started)?;
fn read_metadata(&self, address: String, started: Instant) -> Result<Vec<u8>, String> {
let mut response = self.response(address, started)?;
let mut bytes = Vec::new();
response
.body_mut()
@@ -117,8 +201,27 @@ impl ReleaseSource {
if bytes.len() as u64 > METADATA_LIMIT {
return Err("official release metadata exceeds its size limit".into());
}
Ok(bytes)
}
fn metadata(&self, asset: &ReleaseAsset, path: &Path, started: Instant) -> Result<(), String> {
if asset.size > METADATA_LIMIT {
return Err("official release metadata exceeds its size limit".into());
}
let bytes = self.read_metadata(asset.browser_download_url.clone(), started)?;
if bytes.len() as u64 != asset.size {
return Err("official release asset size does not match its metadata".into());
}
write_new(path, &bytes)
}
fn open_asset(&self, asset: &ReleaseAsset, started: Instant) -> Result<Box<dyn Read>, String> {
let response = self.response(asset.browser_download_url.clone(), started)?;
Ok(Box::new(AssetReader {
reader: response.into_body().into_reader(),
remaining: asset.size,
}))
}
}
impl super::ExtensionSource for ReleaseSource {
@@ -127,30 +230,58 @@ impl super::ExtensionSource for ReleaseSource {
version: &Version,
extension: &super::ExtensionId,
) -> Result<Box<dyn Read>, String> {
Ok(Box::new(
self.response(
self.asset(version.as_str(), &extension.asset_suffix()),
Instant::now(),
)?
.into_body()
.into_reader(),
))
let started = Instant::now();
let release = self.resolve(version.as_str(), started)?;
self.open_asset(release.asset(&extension.asset_suffix())?, started)
}
}
fn redirect(current: &str, location: &str) -> Result<String, String> {
struct AssetReader<R> {
reader: R,
remaining: u64,
}
impl<R: Read> Read for AssetReader<R> {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
if buffer.is_empty() {
return Ok(0);
}
if self.remaining == 0 {
if self.reader.read(&mut [0])? != 0 {
return Err(io::Error::other("release asset exceeds its declared size"));
}
return Ok(0);
}
let limit = self.remaining.min(buffer.len() as u64) as usize;
let count = self.reader.read(&mut buffer[..limit])?;
if count == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"release asset is shorter than its declared size",
));
}
self.remaining -= count as u64;
Ok(count)
}
}
fn validate_download_url(address: &str, origin: &url::Url) -> Result<url::Url, String> {
let next = url::Url::parse(address).map_err(|_| "invalid release asset URL")?;
if next.origin() != origin.origin()
|| !next.username().is_empty()
|| next.password().is_some()
|| next.fragment().is_some()
{
return Err("release download must remain on the official HTTPS asset origin".into());
}
Ok(next)
}
fn redirect(current: &str, location: &str, origin: &url::Url) -> Result<String, String> {
let next = url::Url::parse(current)
.and_then(|base| base.join(location))
.map_err(|_| "invalid release redirect")?;
if next.scheme() != "https"
|| !next.username().is_empty()
|| next.password().is_some()
|| next.port_or_known_default() != Some(443)
|| next.fragment().is_some()
|| next.host_str() != Some("git.dever.cc")
{
return Err("release redirect must remain on the official HTTPS asset hosts".into());
}
validate_download_url(next.as_str(), origin)?;
Ok(next.into())
}
@@ -158,7 +289,7 @@ pub(super) fn prepare(layout: &Layout, requested: &str) -> Result<Version, Strin
prepare_from(layout, requested, &ReleaseSource::official())
}
fn prepare_from(
pub(super) fn prepare_from(
layout: &Layout,
requested: &str,
source: &ReleaseSource,
@@ -190,15 +321,14 @@ fn prepare_from(
let key = decode_hex_file(&key_path, 32)?;
let staging = Download::new(&layout.staging())?;
let started = Instant::now();
let release = source.resolve(requested, started)?;
source.metadata(
requested,
"manifest.json",
release.asset("manifest.json")?,
&staging.0.join("manifest.json"),
started,
)?;
source.metadata(
requested,
"manifest.sig",
release.asset("manifest.sig")?,
&staging.0.join("manifest.sig"),
started,
)?;
@@ -210,10 +340,11 @@ fn prepare_from(
{
return Err("official releases must include the matching dever-language skill".into());
}
if requested != "latest" && manifest.version.as_str() != requested {
return Err("downloaded release version does not match the requested version".into());
if manifest.version != release.version {
return Err("downloaded release version does not match the selected release tag".into());
}
validate_catalog(&manifest)?;
let archive = release.asset("tar.zst")?;
let version = manifest.version.clone();
let destination = layout.downloads().join(version.as_str());
match fs::symlink_metadata(&destination) {
@@ -226,10 +357,8 @@ fn prepare_from(
}
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
let mut response =
source.response(source.asset(version.as_str(), "tar.zst"), started)?;
super::release_archive::extract(
response.body_mut().as_reader(),
source.open_asset(archive, started)?,
&staging.0,
&manifest.artifacts,
)?;