ognibuild 0.2.11

Detect and run any build system
Documentation
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! Context for working with Debian packages.
//!
//! This module provides a context for operations on Debian packages,
//! including editing, committing changes, and managing dependencies.

use crate::dependencies::debian::DebianDependency;
use breezyshim::commit::PyCommitReporter;
use breezyshim::debian::debcommit::debcommit;
use breezyshim::error::Error as BrzError;
use breezyshim::tree::{MutableTree, Tree};
use breezyshim::workingtree::{GenericWorkingTree, WorkingTree};
pub use buildlog_consultant::sbuild::Phase;
use debian_analyzer::abstract_control::AbstractControlEditor;
use debian_analyzer::editor::{Editor, EditorError, Marshallable, MutableTreeEdit, TreeEditor};
use std::path::{Path, PathBuf};

/// Errors that can occur when working with Debian packages.
#[derive(Debug)]
pub enum Error {
    /// Circular dependency detected.
    CircularDependency(String),
    /// No source stanza found in debian/control.
    MissingSource,
    /// Error from breezyshim.
    BrzError(BrzError),
    /// Error from debian_analyzer editor.
    EditorError(debian_analyzer::editor::EditorError),
    /// I/O error when accessing files.
    IoError(std::io::Error),
    /// Invalid field value in control file.
    InvalidField(String, String),
}

impl From<BrzError> for Error {
    fn from(e: BrzError) -> Self {
        Error::BrzError(e)
    }
}

impl From<debian_analyzer::editor::EditorError> for Error {
    fn from(e: debian_analyzer::editor::EditorError) -> Self {
        Error::EditorError(e)
    }
}

impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Error::IoError(e)
    }
}

impl From<Error> for crate::fix_build::InterimError<Error> {
    fn from(e: Error) -> crate::fix_build::InterimError<Error> {
        crate::fix_build::InterimError::Other(e)
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::CircularDependency(pkg) => write!(f, "Circular dependency on {}", pkg),
            Error::MissingSource => write!(f, "No source stanza"),
            Error::BrzError(e) => write!(f, "{}", e),
            Error::EditorError(e) => write!(f, "{}", e),
            Error::IoError(e) => write!(f, "{}", e),
            Error::InvalidField(field, e) => write!(f, "Invalid field {}: {}", field, e),
        }
    }
}

impl std::error::Error for Error {}

/// Context for working with Debian packages.
///
/// This structure provides methods for modifying Debian package files,
/// committing changes, and managing dependencies.
pub struct DebianPackagingContext {
    /// Working tree containing the package source.
    pub tree: GenericWorkingTree,
    /// Path within the tree where the package is located.
    pub subpath: PathBuf,
    /// Committer information (name, email).
    pub committer: (String, String),
    /// Whether to update the changelog during commits.
    pub update_changelog: bool,
    /// Optional reporter for commit operations.
    pub commit_reporter: Option<Box<dyn PyCommitReporter>>,
}

impl DebianPackagingContext {
    /// Create a new Debian packaging context.
    ///
    /// # Arguments
    /// * `tree` - Working tree containing the package source
    /// * `subpath` - Path within the tree where the package is located
    /// * `committer` - Optional committer information (name, email)
    /// * `update_changelog` - Whether to update the changelog during commits
    /// * `commit_reporter` - Optional reporter for commit operations
    ///
    /// # Returns
    /// A new DebianPackagingContext instance
    pub fn new(
        tree: GenericWorkingTree,
        subpath: &Path,
        committer: Option<(String, String)>,
        update_changelog: bool,
        commit_reporter: Option<Box<dyn PyCommitReporter>>,
    ) -> Self {
        Self {
            tree,
            subpath: subpath.to_path_buf(),
            committer: committer.unwrap_or_else(|| debian_changelog::get_maintainer().unwrap()),
            update_changelog,
            commit_reporter,
        }
    }

    /// Check if a file exists in the package tree.
    ///
    /// # Arguments
    /// * `path` - Path to check
    ///
    /// # Returns
    /// true if the file exists, false otherwise
    pub fn has_filename(&self, path: &Path) -> bool {
        self.tree.has_filename(&self.subpath.join(path))
    }

    /// Get the absolute path of a file in the package tree.
    ///
    /// # Arguments
    /// * `path` - Relative path within the package
    ///
    /// # Returns
    /// Absolute path to the file
    pub fn abspath(&self, path: &Path) -> PathBuf {
        self.tree.abspath(&self.subpath.join(path)).unwrap()
    }

    /// Create an editor for a file in the package tree.
    ///
    /// # Arguments
    /// * `path` - Path to the file to edit
    /// * `allow_generated` - Whether to allow editing generated files
    ///
    /// # Returns
    /// A TreeEditor for the specified file
    pub fn edit_file<P: Marshallable>(
        &self,
        path: &std::path::Path,
        allow_generated: bool,
    ) -> Result<TreeEditor<'_, P>, EditorError> {
        let path = self.subpath.join(path);
        self.tree.edit_file(&path, allow_generated, true)
    }

    /// Commit changes to the package tree.
    ///
    /// # Arguments
    /// * `summary` - Commit message summary
    /// * `update_changelog` - Whether to update the changelog (overrides context setting)
    ///
    /// # Returns
    /// Ok(true) if changes were committed, Ok(false) if no changes to commit, Error otherwise
    pub fn commit(&self, summary: &str, update_changelog: Option<bool>) -> Result<bool, Error> {
        let update_changelog = update_changelog.unwrap_or(self.update_changelog);

        let committer = format!("{} <{}>", self.committer.0, self.committer.1);

        let lock_write = self.tree.lock_write();
        let r = if update_changelog {
            let mut cl = self
                .edit_file::<debian_changelog::ChangeLog>(Path::new("debian/changelog"), false)?;
            cl.auto_add_change(&[summary], self.committer.clone(), Some(chrono::Local::now().into()), None);
            cl.commit()?;

            debcommit(
                &self.tree,
                Some(&committer),
                &self.subpath,
                None,
                self.commit_reporter.as_deref(),
                None,
            )
        } else {
            let mut builder = self
                .tree
                .build_commit()
                .message(summary)
                .committer(&committer);

            if !self.subpath.as_os_str().is_empty() {
                builder = builder.specific_files(&[&self.subpath]);
            }
            if let Some(commit_reporter) = self.commit_reporter.as_ref() {
                builder = builder.reporter(commit_reporter.as_ref());
            }
            builder.commit()
        };

        std::mem::drop(lock_write);

        match r {
            Ok(_) => Ok(true),
            Err(BrzError::PointlessCommit) => Ok(false),
            Err(e) => Err(e.into()),
        }
    }

    /// Add a dependency to the package.
    ///
    /// # Arguments
    /// * `phase` - Build phase for the dependency
    /// * `requirement` - Debian dependency to add
    ///
    /// # Returns
    /// Ok(true) if dependency was added, Ok(false) if already present, Error otherwise
    pub fn add_dependency(
        &self,
        phase: &Phase,
        requirement: &DebianDependency,
    ) -> Result<bool, Error> {
        match phase {
            Phase::AutoPkgTest(n) => self.add_test_dependency(n, requirement),
            Phase::Build => self.add_build_dependency(requirement),
            Phase::BuildEnv => {
                // TODO(jelmer): Actually, we probably just want to install it on the host system?
                log::warn!("Unknown phase {:?}", phase);
                Ok(false)
            }
            Phase::CreateSession => {
                log::warn!("Unknown phase {:?}", phase);
                Ok(false)
            }
        }
    }

    /// Create an editor for the debian/control file.
    ///
    /// # Returns
    /// An editor for the control file, or Error if not found or cannot be edited
    pub fn edit_control<'a>(&'a self) -> Result<Box<dyn AbstractControlEditor + 'a>, Error> {
        if self
            .tree
            .has_filename(&self.subpath.join("debian/debcargo.toml"))
        {
            Ok(Box::new(
                debian_analyzer::debcargo::DebcargoEditor::from_directory(
                    &self.tree.abspath(&self.subpath).unwrap(),
                )?,
            ))
        } else {
            let control_path = Path::new("debian/control");
            Ok(
                Box::new(self.edit_file::<debian_control::Control>(control_path, false)?)
                    as Box<dyn AbstractControlEditor>,
            )
        }
    }

    fn add_build_dependency(&self, requirement: &DebianDependency) -> Result<bool, Error> {
        assert!(!requirement.is_empty());
        let mut control = self.edit_control()?;

        for binary in control.binaries() {
            if requirement.touches_package(&binary.name().unwrap()) {
                return Err(Error::CircularDependency(binary.name().unwrap()));
            }
        }

        let mut source = if let Some(source) = control.source() {
            source
        } else {
            return Err(Error::MissingSource);
        };
        for rel in requirement.iter() {
            source.ensure_build_dep(rel);
        }

        std::mem::drop(source);

        let desc = requirement.relation_string();

        if !control.commit() {
            log::info!("Giving up; build dependency {} was already present.", desc);
            return Ok(false);
        }

        log::info!("Adding build dependency: {}", desc);
        self.commit(&format!("Add missing build dependency on {}.", desc), None)?;
        Ok(true)
    }

    /// Create an editor for the debian/tests/control file.
    ///
    /// # Returns
    /// An editor for the tests control file, or Error if not found or cannot be edited
    pub fn edit_tests_control(&self) -> Result<TreeEditor<'_, deb822_lossless::Deb822>, Error> {
        Ok(self.edit_file::<deb822_lossless::Deb822>(Path::new("debian/tests/control"), false)?)
    }

    /// Create an editor for the debian/rules file.
    ///
    /// # Returns
    /// An editor for the rules file, or Error if not found or cannot be edited
    pub fn edit_rules(&self) -> Result<TreeEditor<'_, makefile_lossless::Makefile>, Error> {
        Ok(self.edit_file::<makefile_lossless::Makefile>(Path::new("debian/rules"), false)?)
    }

    fn add_test_dependency(
        &self,
        testname: &str,
        requirement: &DebianDependency,
    ) -> Result<bool, Error> {
        // TODO(jelmer): If requirement is for one of our binary packages  but "@" is already
        // present then don't do anything.

        let editor = self.edit_tests_control()?;

        let mut command_counter = 1;
        for mut para in editor.paragraphs() {
            let name = para.get("Tests").unwrap_or_else(|| {
                let name = format!("command{}", command_counter);
                command_counter += 1;
                name
            });

            if name != testname {
                continue;
            }

            for rel in requirement.iter() {
                let depends = para.get("Depends").unwrap_or_default();
                let mut rels: debian_control::lossless::relations::Relations =
                    depends.parse().map_err(|e| {
                        Error::InvalidField(format!("Test Depends for {}", testname), e)
                    })?;
                debian_analyzer::relations::ensure_relation(&mut rels, rel);
                para.insert("Depends", &rels.to_string());
            }
        }

        let desc = requirement.relation_string();

        if editor.commit()?.is_empty() {
            log::info!(
                "Giving up; dependency {} for test {} was already present.",
                desc,
                testname,
            );
            return Ok(false);
        }

        log::info!("Adding dependency to test {}: {}", testname, desc);
        self.commit(
            &format!("Add missing dependency for test {} on {}.", testname, desc),
            None,
        )?;
        Ok(true)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
    pub const COMMITTER: &str = "ognibuild <ognibuild@example.com>";
    fn setup(path: &Path) -> DebianPackagingContext {
        let tree = create_standalone_workingtree(path, &ControlDirFormat::default()).unwrap();
        std::fs::create_dir_all(path.join("debian")).unwrap();
        std::fs::write(
            path.join("debian/control"),
            r###"Source: blah
Build-Depends: libc6

Package: python-blah
Depends: ${python3:Depends}
Description: A python package
 Foo
"###,
        )
        .unwrap();
        std::fs::write(
            path.join("debian/changelog"),
            r###"blah (0.1) UNRELEASED; urgency=medium

  * Initial release. (Closes: #XXXXXX)

 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 04 Apr 2020 14:12:13 +0000
"###,
        )
        .unwrap();
        tree.add(&[
            Path::new("debian"),
            Path::new("debian/control"),
            Path::new("debian/changelog"),
        ])
        .unwrap();
        tree.build_commit()
            .message("Initial commit")
            .committer(COMMITTER)
            .commit()
            .unwrap();

        DebianPackagingContext::new(
            tree,
            Path::new(""),
            Some(("ognibuild".to_owned(), "<ognibuild@jelmer.uk>".to_owned())),
            false,
            Some(Box::new(breezyshim::commit::NullCommitReporter::new())),
        )
    }

    #[test]
    fn test_already_present() {
        let td = tempfile::tempdir().unwrap();
        let context = setup(td.path());
        let dep = DebianDependency::simple("libc6");
        assert!(!context.add_build_dependency(&dep).unwrap());
    }

    #[test]
    fn test_basic() {
        let td = tempfile::tempdir().unwrap();
        let context = setup(td.path());
        let dep = DebianDependency::simple("foo");
        assert!(context.add_build_dependency(&dep).unwrap());
        let control = std::fs::read_to_string(td.path().join("debian/control")).unwrap();
        assert_eq!(
            control,
            r###"Source: blah
Build-Depends: foo, libc6

Package: python-blah
Depends: ${python3:Depends}
Description: A python package
 Foo
"###
        );
    }

    #[test]
    fn test_circular() {
        let td = tempfile::tempdir().unwrap();
        let context = setup(td.path());
        let dep = DebianDependency::simple("python-blah");
        assert!(matches!(
            context.add_build_dependency(&dep),
            Err(Error::CircularDependency(_))
        ));
    }
}