1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Copyright (C) 2019 Sajeer Ahamed <ahamedsajeer.15.15@cse.mrt.ac.lk>
// Copyright (C) 2019 Sebastian Dröge <sebastian@centricular.com>
//
// Licensed under the MIT license, see the LICENSE file or <http://opensource.org/licenses/MIT>
//
// SPDX-License-Identifier: MIT

//! Extracts release for [GStreamer](https://gstreamer.freedesktop.org) plugin metadata
//!
//! See [`info`](fn.info.html) for details.
//!
//! This function is supposed to be used as follows in the `build.rs` of a crate that implements a
//! plugin:
//!
//! ```rust,ignore
//! gst_plugin_version_helper::info();
//! ```
//!
//! Inside `lib.rs` of the plugin, the information provided by `info` are usable as follows:
//!
//! ```rust,ignore
//! gst::plugin_define!(
//!     the_plugin_name,
//!     env!("CARGO_PKG_DESCRIPTION"),
//!     plugin_init,
//!     concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
//!     "The Plugin's License",
//!     env!("CARGO_PKG_NAME"),
//!     env!("CARGO_PKG_NAME"),
//!     env!("CARGO_PKG_REPOSITORY"),
//!     env!("BUILD_REL_DATE")
//! );
//! ```

mod git;

use chrono::{Datelike, TimeZone};
use std::convert::TryInto;
use std::time::SystemTime;
use std::{env, fs, path};

/// Extracts release for GStreamer plugin metadata
///
/// Release information is first tried to be extracted from a git repository at the same
/// place as the `Cargo.toml`, or one directory up to allow for Cargo workspaces. If no
/// git repository is found, we assume this is a release.
///
/// - If extracted from a git repository, sets the `COMMIT_ID` environment variable to the short
///   commit id of the latest commit and the `BUILD_REL_DATE` environment variable to the date of the
///   commit.
///
/// - If not, `COMMIT_ID` will be set to the string `RELEASE` and the
///   `BUILD_REL_DATE` variable will be set to the `package.metadata.gstreamer.release_date` key of
///   `Cargo.toml`, if it exists.
///
/// - If not, `COMMIT_ID` will be set to the string `RELEASE` and the `BUILD_REL_DATE` variable
///   will be set to the mtime of `Cargo.toml`. Note that the crates created by `cargo package` and
///   `cargo publish` have bogus mtimes for all files and won't be used.
///
/// - If neither is possible, `COMMIT_ID` is set to the string `UNKNOWN` and `BUILD_REL_DATE` to the
///   current date.
///
pub fn info() {
    let crate_dir =
        path::PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
    let mut repo_dir = crate_dir.clone();

    // First check for a git repository in the manifest directory and if there
    // is none try one directory up in case we're in a Cargo workspace
    let git_info = git::repo_hash(&repo_dir).or_else(move || {
        repo_dir.pop();
        git::repo_hash(&repo_dir)
    });

    // If there is a git repository, extract the version information from there.
    // Otherwise assume this is a release and use Cargo.toml mtime as date.
    let (commit_id, commit_date) = git_info.unwrap_or_else(|| {
        let date = cargo_metadata_release_date(&crate_dir)
            .or_else(|| cargo_mtime_date(&crate_dir))
            .unwrap_or_else(chrono::Utc::now);
        ("RELEASE".into(), date.format("%Y-%m-%d").to_string())
    });

    println!("cargo:rustc-env=COMMIT_ID={commit_id}");
    println!("cargo:rustc-env=BUILD_REL_DATE={commit_date}");
}

fn cargo_metadata_release_date(crate_dir: &path::Path) -> Option<chrono::DateTime<chrono::Utc>> {
    use std::io::prelude::*;

    let mut cargo_toml = path::PathBuf::from(crate_dir);
    cargo_toml.push("Cargo.toml");

    let mut file = fs::File::open(&cargo_toml).ok()?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).ok()?;

    let doc = contents.parse::<toml_edit::DocumentMut>().ok()?;
    let release_date = doc
        .get("package")
        .and_then(|package| package.as_table_like())
        .and_then(|package| package.get("metadata"))
        .and_then(|metadata| metadata.as_table_like())
        .and_then(|metadata| metadata.get("gstreamer"))
        .and_then(|gstreamer| gstreamer.as_table_like())
        .and_then(|gstreamer| gstreamer.get("release_date"))
        .and_then(|release_date| release_date.as_str())?;

    let release_date = release_date.parse::<chrono::NaiveDate>().ok()?;
    Some(chrono::DateTime::from_naive_utc_and_offset(
        release_date.and_hms_opt(0, 0, 0)?,
        chrono::Utc,
    ))
}

fn cargo_mtime_date(crate_dir: &path::Path) -> Option<chrono::DateTime<chrono::Utc>> {
    let mut cargo_toml = path::PathBuf::from(crate_dir);
    cargo_toml.push("Cargo.toml");

    let metadata = fs::metadata(&cargo_toml).ok()?;
    let mtime = metadata.modified().ok()?;
    let unix_time = mtime.duration_since(SystemTime::UNIX_EPOCH).ok()?;
    let dt = chrono::Utc
        .timestamp_opt(unix_time.as_secs().try_into().ok()?, 0)
        .latest()?;

    // FIXME: Work around https://github.com/rust-lang/cargo/issues/10285
    if dt.date_naive().year() < 2015 {
        return None;
    }

    Some(dt)
}