ptx_builder/
builder.rs

1use std::env;
2use std::fmt;
3use std::fs::{read_to_string, write, File};
4use std::io::{BufReader, Read};
5use std::path::{Path, PathBuf};
6
7use failure::ResultExt;
8use lazy_static::*;
9use regex::Regex;
10
11use crate::error::*;
12use crate::executable::{Cargo, ExecutableRunner, Linker};
13use crate::source::Crate;
14
15const LAST_BUILD_CMD: &str = ".last-build-command";
16const TARGET_NAME: &str = "nvptx64-nvidia-cuda";
17
18/// Core of the crate - PTX assembly build controller.
19#[derive(Debug)]
20pub struct Builder {
21    source_crate: Crate,
22
23    profile: Profile,
24    colors: bool,
25    crate_type: Option<CrateType>,
26}
27
28/// Successful build output.
29#[derive(Debug)]
30pub struct BuildOutput<'a> {
31    builder: &'a Builder,
32    output_path: PathBuf,
33    file_suffix: String,
34}
35
36/// Non-failed build status.
37#[derive(Debug)]
38pub enum BuildStatus<'a> {
39    /// The CUDA crate building was performed without errors.
40    Success(BuildOutput<'a>),
41
42    /// The CUDA crate building is not needed. Can happend in several cases:
43    /// - `build.rs` script was called by **RLS**,
44    /// - `build.rs` was called **recursively** (e.g. `build.rs` call for device crate in single-source setup)
45    NotNeeded,
46}
47
48/// Debug / Release profile.
49///
50/// # Usage
51/// ``` no_run
52/// use ptx_builder::prelude::*;
53/// # use ptx_builder::error::Result;
54///
55/// # fn main() -> Result<()> {
56/// Builder::new(".")?
57///     .set_profile(Profile::Debug)
58///     .build()?;
59/// # Ok(())
60/// # }
61/// ```
62#[derive(PartialEq, Clone, Debug)]
63pub enum Profile {
64    /// Equivalent for `cargo-build` **without** `--release` flag.
65    Debug,
66
67    /// Equivalent for `cargo-build` **with** `--release` flag.
68    Release,
69}
70
71/// Build specified crate type.
72///
73/// Mandatory for mixed crates - that have both `lib.rs` and `main.rs`,
74/// otherwise Cargo won't know which to build:
75/// ```text
76/// error: extra arguments to `rustc` can only be passed to one target, consider filtering
77/// the package by passing e.g. `--lib` or `--bin NAME` to specify a single target
78/// ```
79///
80/// # Usage
81/// ``` no_run
82/// use ptx_builder::prelude::*;
83/// # use ptx_builder::error::Result;
84///
85/// # fn main() -> Result<()> {
86/// Builder::new(".")?
87///     .set_crate_type(CrateType::Library)
88///     .build()?;
89/// # Ok(())
90/// # }
91/// ```
92#[derive(Clone, Copy, Debug)]
93pub enum CrateType {
94    Library,
95    Binary,
96}
97
98impl Builder {
99    /// Construct a builder for device crate at `path`.
100    ///
101    /// Can also be the same crate, for single-source mode:
102    /// ``` no_run
103    /// use ptx_builder::prelude::*;
104    /// # use ptx_builder::error::Result;
105    ///
106    /// # fn main() -> Result<()> {
107    /// match Builder::new(".")?.build()? {
108    ///     BuildStatus::Success(output) => {
109    ///         // do something with the output...
110    ///     }
111    ///
112    ///     BuildStatus::NotNeeded => {
113    ///         // ...
114    ///     }
115    /// }
116    /// # Ok(())
117    /// # }
118    /// ```
119    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
120        Ok(Builder {
121            source_crate: Crate::analyse(path).context("Unable to analyse source crate")?,
122
123            profile: Profile::Release, // TODO: choose automatically, e.g.: `env::var("PROFILE").unwrap_or("release".to_string())`
124            colors: true,
125            crate_type: None,
126        })
127    }
128
129    /// Returns bool indicating whether the actual build is needed.
130    ///
131    /// Behavior is consistent with
132    /// [`BuildStatus::NotNeeded`](enum.BuildStatus.html#variant.NotNeeded).
133    pub fn is_build_needed() -> bool {
134        let cargo_env = env::var("CARGO");
135        let recursive_env = env::var("PTX_CRATE_BUILDING");
136
137        let is_rls_build = cargo_env.is_ok() && cargo_env.unwrap().ends_with("rls");
138        let is_recursive_build = recursive_env.is_ok() && recursive_env.unwrap() == "1";
139
140        !is_rls_build && !is_recursive_build
141    }
142
143    /// Disable colors for internal calls to `cargo`.
144    pub fn disable_colors(mut self) -> Self {
145        self.colors = false;
146        self
147    }
148
149    /// Set build profile.
150    pub fn set_profile(mut self, profile: Profile) -> Self {
151        self.profile = profile;
152        self
153    }
154
155    /// Set crate type that needs to be built.
156    ///
157    /// Mandatory for mixed crates - that have both `lib.rs` and `main.rs`,
158    /// otherwise Cargo won't know which to build:
159    /// ```text
160    /// error: extra arguments to `rustc` can only be passed to one target, consider filtering
161    /// the package by passing e.g. `--lib` or `--bin NAME` to specify a single target
162    /// ```
163    pub fn set_crate_type(mut self, crate_type: CrateType) -> Self {
164        self.crate_type = Some(crate_type);
165        self
166    }
167
168    /// Performs an actual build: runs `cargo` with proper flags and environment.
169    pub fn build(&self) -> Result<BuildStatus> {
170        if !Self::is_build_needed() {
171            return Ok(BuildStatus::NotNeeded);
172        }
173
174        // Verify `ptx-linker` version.
175        ExecutableRunner::new(Linker).with_args(vec!["-V"]).run()?;
176
177        let mut cargo = ExecutableRunner::new(Cargo);
178        let mut args = Vec::new();
179
180        args.push("rustc");
181
182        if self.profile == Profile::Release {
183            args.push("--release");
184        }
185
186        args.push("--color");
187        args.push(if self.colors { "always" } else { "never" });
188
189        args.push("--target");
190        args.push(TARGET_NAME);
191
192        match self.crate_type {
193            Some(CrateType::Binary) => {
194                args.push("--bin");
195                args.push(self.source_crate.get_name());
196            }
197
198            Some(CrateType::Library) => {
199                args.push("--lib");
200            }
201
202            _ => {}
203        }
204
205        args.push("-v");
206        args.push("--");
207        args.push("--crate-type");
208        args.push("cdylib");
209        args.push("-Zcrate-attr=no_main");
210
211        let output_path = {
212            self.source_crate
213                .get_output_path()
214                .context("Unable to create output path")?
215        };
216
217        cargo
218            .with_args(&args)
219            .with_cwd(self.source_crate.get_path())
220            .with_env("PTX_CRATE_BUILDING", "1")
221            .with_env("CARGO_TARGET_DIR", output_path.clone());
222
223        let cargo_output = cargo.run().map_err(|error| match error.kind() {
224            BuildErrorKind::CommandFailed { stderr, .. } => {
225                let lines = stderr
226                    .trim_matches('\n')
227                    .split('\n')
228                    .filter(Self::output_is_not_verbose)
229                    .map(String::from)
230                    .collect();
231
232                Error::from(BuildErrorKind::BuildFailed(lines))
233            }
234
235            _ => error,
236        })?;
237
238        Ok(BuildStatus::Success(
239            self.prepare_output(output_path, &cargo_output.stderr)?,
240        ))
241    }
242
243    fn prepare_output(&self, output_path: PathBuf, cargo_stderr: &str) -> Result<BuildOutput> {
244        lazy_static! {
245            static ref SUFFIX_REGEX: Regex =
246                Regex::new(r"-C extra-filename=([\S]+)").expect("Unable to parse regex...");
247        }
248
249        let crate_name = self.source_crate.get_output_file_prefix();
250
251        // We need the build command to get real output filename.
252        let build_command = {
253            cargo_stderr
254                .trim_matches('\n')
255                .split('\n')
256                .find(|line| {
257                    line.contains(&format!("--crate-name {}", crate_name))
258                        && line.contains("--crate-type cdylib")
259                })
260                .map(|line| BuildCommand::Realtime(line.to_string()))
261                .or_else(|| Self::load_cached_build_command(&output_path))
262                .ok_or_else(|| {
263                    Error::from(BuildErrorKind::InternalError(String::from(
264                        "Unable to find build command of the device crate",
265                    )))
266                })?
267        };
268
269        if let BuildCommand::Realtime(ref command) = build_command {
270            Self::store_cached_build_command(&output_path, &command)?;
271        }
272
273        let file_suffix = match SUFFIX_REGEX.captures(&build_command) {
274            Some(caps) => caps[1].to_string(),
275
276            None => {
277                bail!(BuildErrorKind::InternalError(String::from(
278                    "Unable to find `extra-filename` rustc flag",
279                )));
280            }
281        };
282
283        Ok(BuildOutput::new(self, output_path, file_suffix))
284    }
285
286    fn output_is_not_verbose(line: &&str) -> bool {
287        !line.starts_with("+ ")
288            && !line.contains("Running")
289            && !line.contains("Fresh")
290            && !line.starts_with("Caused by:")
291            && !line.starts_with("  process didn\'t exit successfully: ")
292    }
293
294    fn load_cached_build_command(output_path: &Path) -> Option<BuildCommand> {
295        match read_to_string(output_path.join(LAST_BUILD_CMD)) {
296            Ok(contents) => Some(BuildCommand::Cached(contents)),
297            Err(_) => None,
298        }
299    }
300
301    fn store_cached_build_command(output_path: &Path, command: &str) -> Result<()> {
302        write(output_path.join(LAST_BUILD_CMD), command.as_bytes())
303            .context(BuildErrorKind::OtherError)?;
304
305        Ok(())
306    }
307}
308
309impl<'a> BuildOutput<'a> {
310    fn new(builder: &'a Builder, output_path: PathBuf, file_suffix: String) -> Self {
311        BuildOutput {
312            builder,
313            output_path,
314            file_suffix,
315        }
316    }
317
318    /// Returns path to PTX assembly file.
319    ///
320    /// # Usage
321    /// Can be used from `build.rs` script to provide Rust with the path
322    /// via environment variable:
323    /// ```no_run
324    /// use ptx_builder::prelude::*;
325    /// # use ptx_builder::error::Result;
326    ///
327    /// # fn main() -> Result<()> {
328    /// if let BuildStatus::Success(output) = Builder::new(".")?.build()? {
329    ///     println!(
330    ///         "cargo:rustc-env=KERNEL_PTX_PATH={}",
331    ///         output.get_assembly_path().display()
332    ///     );
333    /// }
334    /// # Ok(())
335    /// # }
336    /// ```
337    pub fn get_assembly_path(&self) -> PathBuf {
338        self.output_path
339            .join(TARGET_NAME)
340            .join(self.builder.profile.to_string())
341            .join("deps")
342            .join(format!(
343                "{}{}.ptx",
344                self.builder.source_crate.get_output_file_prefix(),
345                self.file_suffix,
346            ))
347    }
348
349    /// Returns a list of crate dependencies.
350    ///
351    /// # Usage
352    /// Can be used from `build.rs` script to notify Cargo the dependencies,
353    /// so it can automatically rebuild on changes:
354    /// ```no_run
355    /// use ptx_builder::prelude::*;
356    /// # use ptx_builder::error::Result;
357    ///
358    /// # fn main() -> Result<()> {
359    /// if let BuildStatus::Success(output) = Builder::new(".")?.build()? {
360    ///     for path in output.dependencies()? {
361    ///         println!("cargo:rerun-if-changed={}", path.display());
362    ///     }
363    /// }
364    /// # Ok(())
365    /// # }
366    /// ```
367    pub fn dependencies(&self) -> Result<Vec<PathBuf>> {
368        let mut deps_contents = {
369            self.get_deps_file_contents()
370                .context("Unable to get crate deps")?
371        };
372
373        if deps_contents.is_empty() {
374            bail!(BuildErrorKind::InternalError(String::from(
375                "Empty deps file",
376            )));
377        }
378
379        deps_contents = deps_contents
380            .chars()
381            .skip(3) // workaround for Windows paths starts wuth "[A-Z]:\"
382            .skip_while(|c| *c != ':')
383            .skip(1)
384            .collect::<String>();
385
386        let cargo_deps = vec![
387            self.builder.source_crate.get_path().join("Cargo.toml"),
388            self.builder.source_crate.get_path().join("Cargo.lock"),
389        ];
390
391        Ok(deps_contents
392            .trim()
393            .split(' ')
394            .map(|item| PathBuf::from(item.trim()))
395            .chain(cargo_deps.into_iter())
396            .collect())
397    }
398
399    fn get_deps_file_contents(&self) -> Result<String> {
400        let crate_deps_path = self
401            .output_path
402            .join(TARGET_NAME)
403            .join(self.builder.profile.to_string())
404            .join(format!(
405                "{}.d",
406                self.builder
407                    .source_crate
408                    .get_deps_file_prefix(self.builder.crate_type)?
409            ));
410
411        let mut crate_deps_reader =
412            BufReader::new(File::open(crate_deps_path).context(BuildErrorKind::OtherError)?);
413
414        let mut crate_deps_contents = String::new();
415
416        crate_deps_reader
417            .read_to_string(&mut crate_deps_contents)
418            .context(BuildErrorKind::OtherError)?;
419
420        Ok(crate_deps_contents)
421    }
422}
423
424impl fmt::Display for Profile {
425    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
426        match self {
427            Profile::Debug => write!(f, "debug"),
428            Profile::Release => write!(f, "release"),
429        }
430    }
431}
432
433enum BuildCommand {
434    Realtime(String),
435    Cached(String),
436}
437
438impl std::ops::Deref for BuildCommand {
439    type Target = str;
440
441    fn deref(&self) -> &str {
442        match self {
443            BuildCommand::Realtime(line) => &line,
444            BuildCommand::Cached(line) => &line,
445        }
446    }
447}