Skip to main content

wp_self_update/
lib.rs

1mod fetch;
2mod install;
3mod lock;
4mod manifest;
5mod platform;
6mod types;
7mod versioning;
8
9pub use manifest::updates_manifest_url;
10use orion_error::{ToStructError, UvsFrom};
11pub use types::{
12    CheckReport, CheckRequest, ResolvedRelease, SourceConfig, UpdateChannel, UpdateProduct,
13    UpdateReport, UpdateRequest, VersionRelation,
14};
15pub use versioning::{compare_versions_str, relation_message};
16
17use fetch::load_release;
18use install::{
19    confirm_update, create_temp_update_dir, extract_artifact, fetch_asset_bytes,
20    find_extracted_bins, install_bins, is_probably_package_managed, resolve_install_dir,
21    rollback_bins, run_health_check, validate_download_url, verify_asset_sha256,
22};
23use lock::UpdateLock;
24use std::path::PathBuf;
25use wp_error::run_error::RunResult;
26
27pub async fn check(request: CheckRequest) -> RunResult<CheckReport> {
28    let channel = request.source.channel;
29    let (release, source) = load_release(&request.source, channel).await?;
30    versioning::validate_artifact_version_consistency(&release.version, &release.artifact)?;
31
32    let relation = compare_versions_str(&request.current_version, &release.version)?;
33    Ok(CheckReport {
34        product: request.product.as_str().to_string(),
35        channel: channel.as_str().to_string(),
36        branch: request.branch,
37        source,
38        manifest_format: "v2".to_string(),
39        current_version: request.current_version,
40        latest_version: release.version.clone(),
41        update_available: relation == VersionRelation::UpdateAvailable,
42        platform_key: release.target,
43        artifact: release.artifact,
44        sha256: release.sha256,
45    })
46}
47
48pub async fn update(request: UpdateRequest) -> RunResult<UpdateReport> {
49    let channel = request.source.channel;
50    let product = request.product;
51    let selected_bins = product.bins();
52    let (release, source) = load_release(&request.source, channel).await?;
53    versioning::validate_artifact_version_consistency(&release.version, &release.artifact)?;
54    validate_download_url(&release.artifact, &request.source)?;
55
56    let relation = compare_versions_str(&request.current_version, &release.version)?;
57    let install_dir = resolve_install_dir(request.install_dir.as_deref())?;
58    let install_dir_display = install_dir.display().to_string();
59
60    if relation != VersionRelation::UpdateAvailable && !request.force {
61        return Ok(UpdateReport {
62            product: product.as_str().to_string(),
63            channel: channel.as_str().to_string(),
64            source,
65            current_version: request.current_version,
66            latest_version: release.version,
67            install_dir: install_dir_display,
68            artifact: release.artifact,
69            dry_run: request.dry_run,
70            updated: false,
71            status: relation_message(relation).to_string(),
72        });
73    }
74
75    if is_probably_package_managed(&install_dir) && !request.force {
76        return Err(wp_error::run_error::RunReason::from_conf()
77            .to_err()
78            .with_detail(format!(
79                "refusing to replace binaries under {}; looks like a package-managed install, rerun with --force if this is intentional",
80                install_dir.display()
81            )));
82    }
83
84    if request.dry_run {
85        return Ok(UpdateReport {
86            product: product.as_str().to_string(),
87            channel: channel.as_str().to_string(),
88            source,
89            current_version: request.current_version,
90            latest_version: release.version,
91            install_dir: install_dir_display,
92            artifact: release.artifact,
93            dry_run: true,
94            updated: false,
95            status: "dry-run".to_string(),
96        });
97    }
98
99    if !request.yes
100        && !confirm_update(
101            &request.current_version,
102            &release.version,
103            &install_dir,
104            &release.artifact,
105        )?
106    {
107        return Ok(UpdateReport {
108            product: product.as_str().to_string(),
109            channel: channel.as_str().to_string(),
110            source,
111            current_version: request.current_version,
112            latest_version: release.version,
113            install_dir: install_dir_display,
114            artifact: release.artifact,
115            dry_run: false,
116            updated: false,
117            status: "aborted".to_string(),
118        });
119    }
120
121    let _lock = UpdateLock::acquire(&install_dir)?;
122    let asset_bytes = fetch_asset_bytes(&release.artifact).await?;
123    verify_asset_sha256(&asset_bytes, &release.sha256)?;
124
125    let extract_root = create_temp_update_dir()?;
126    let install_result = async {
127        extract_artifact(&asset_bytes, &extract_root)?;
128        let extracted = find_extracted_bins(&extract_root, selected_bins)?;
129        let backup_dir = install_bins(&install_dir, &extracted, selected_bins)?;
130        if let Err(err) = run_health_check(&install_dir, &release.version, selected_bins) {
131            rollback_bins(&install_dir, &backup_dir, selected_bins)?;
132            return Err(err);
133        }
134        Ok::<PathBuf, wp_error::RunError>(backup_dir)
135    }
136    .await;
137
138    let _ = std::fs::remove_dir_all(&extract_root);
139    let backup_dir = install_result?;
140
141    Ok(UpdateReport {
142        product: product.as_str().to_string(),
143        channel: channel.as_str().to_string(),
144        source,
145        current_version: request.current_version,
146        latest_version: release.version,
147        install_dir: install_dir_display,
148        artifact: release.artifact,
149        dry_run: false,
150        updated: true,
151        status: format!("installed (backup: {})", backup_dir.display()),
152    })
153}
154
155#[doc(hidden)]
156pub use manifest::{parse_v2_release, updates_manifest_path};
157#[doc(hidden)]
158pub use versioning::validate_artifact_version_consistency;
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::install::{
164        create_temp_update_dir, extract_artifact, find_extracted_bins, install_bins, rollback_bins,
165        run_health_check,
166    };
167    use flate2::write::GzEncoder;
168    use flate2::Compression;
169    use std::fs;
170    #[cfg(unix)]
171    use std::os::unix::fs::PermissionsExt;
172    use std::path::Path;
173    use std::process::Command;
174    use tar::Builder;
175    use tempfile::tempdir;
176
177    fn build_artifact_tar_gz(version: &str, healthy: bool) -> Vec<u8> {
178        let mut out = Vec::new();
179        let encoder = GzEncoder::new(&mut out, Compression::default());
180        let mut builder = Builder::new(encoder);
181        for bin in UpdateProduct::Suite.bins() {
182            let body = if healthy || *bin != "wproj" {
183                format!("#!/bin/sh\necho \"{} {}\"\n", bin, version)
184            } else {
185                "#!/bin/sh\nexit 1\n".to_string()
186            };
187            let mut header = tar::Header::new_gnu();
188            header.set_size(body.len() as u64);
189            header.set_mode(0o755);
190            header.set_cksum();
191            builder
192                .append_data(&mut header, format!("artifacts/{}", bin), body.as_bytes())
193                .expect("append tar entry");
194        }
195        let encoder = builder.into_inner().expect("finish tar builder");
196        encoder.finish().expect("finish gzip");
197        out
198    }
199
200    fn write_existing_bins(dir: &Path, version: &str) {
201        for bin in UpdateProduct::Suite.bins() {
202            let path = dir.join(bin);
203            fs::write(&path, format!("#!/bin/sh\necho \"{} {}\"\n", bin, version))
204                .expect("write existing bin");
205            #[cfg(unix)]
206            {
207                let mut perms = fs::metadata(&path)
208                    .expect("stat existing bin")
209                    .permissions();
210                perms.set_mode(0o755);
211                fs::set_permissions(&path, perms).expect("chmod existing bin");
212            }
213        }
214    }
215
216    fn apply_artifact(install_dir: &Path, artifact: &[u8], version: &str) -> RunResult<PathBuf> {
217        let extract_root = create_temp_update_dir()?;
218        let install_result = (|| {
219            extract_artifact(artifact, &extract_root)?;
220            let bins = UpdateProduct::Suite.bins();
221            let extracted = find_extracted_bins(&extract_root, bins)?;
222            let backup_dir = install_bins(install_dir, &extracted, bins)?;
223            if let Err(err) = run_health_check(install_dir, version, bins) {
224                rollback_bins(install_dir, &backup_dir, bins)?;
225                return Err(err);
226            }
227            Ok(backup_dir)
228        })();
229        let _ = std::fs::remove_dir_all(&extract_root);
230        install_result
231    }
232
233    #[test]
234    fn installs_release_artifact() {
235        let artifact = build_artifact_tar_gz("0.30.0", true);
236        let install_dir = tempdir().expect("install tempdir");
237        write_existing_bins(install_dir.path(), "0.21.0");
238
239        let backup_dir =
240            apply_artifact(install_dir.path(), &artifact, "0.30.0").expect("install artifact");
241        assert!(backup_dir.exists());
242
243        let out = Command::new(install_dir.path().join("wproj"))
244            .arg("--version")
245            .output()
246            .expect("run installed wproj");
247        assert!(out.status.success());
248        assert!(String::from_utf8_lossy(&out.stdout).contains("0.30.0"));
249    }
250
251    #[test]
252    fn rolls_back_on_health_check_failure() {
253        let artifact = build_artifact_tar_gz("0.30.0", false);
254        let install_dir = tempdir().expect("install tempdir");
255        write_existing_bins(install_dir.path(), "0.21.0");
256
257        let err =
258            apply_artifact(install_dir.path(), &artifact, "0.30.0").expect_err("expected failure");
259        assert!(format!("{}", err).contains("health check failed"));
260
261        let out = Command::new(install_dir.path().join("wproj"))
262            .arg("--version")
263            .output()
264            .expect("run rolled back wproj");
265        assert!(out.status.success());
266        assert!(String::from_utf8_lossy(&out.stdout).contains("0.21.0"));
267    }
268}