/ build.rs
build.rs
  1  // Copyright (c) 2019-2025 Alpha-Delta Network Inc.
  2  // This file is part of the alphavm library.
  3  
  4  // Licensed under the Apache License, Version 2.0 (the "License");
  5  // you may not use this file except in compliance with the License.
  6  // You may obtain a copy of the License at:
  7  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  use std::{
 17      ffi::OsStr,
 18      fs::{self, File},
 19      io::Read,
 20      path::Path,
 21  };
 22  
 23  use walkdir::WalkDir;
 24  
 25  // The following license text that should be present at the beginning of every source file.
 26  const EXPECTED_LICENSE_TEXT: &[u8] = include_bytes!(".license_header");
 27  
 28  // The following directories will be excluded from the license scan.
 29  const DIRS_TO_SKIP: [&str; 5] = [".cargo", ".circleci", ".git", ".github", "target"];
 30  
 31  #[derive(Clone, Copy, PartialEq, Eq)]
 32  enum ImportOfInterest {
 33      Locktick,
 34      ParkingLot,
 35      Tokio,
 36  }
 37  
 38  fn check_locktick_imports<P: AsRef<Path>>(path: P) {
 39      let mut iter = WalkDir::new(path).into_iter();
 40      while let Some(entry) = iter.next() {
 41          let entry = entry.unwrap();
 42          let entry_type = entry.file_type();
 43  
 44          // Skip the specified directories.
 45          if entry_type.is_dir() && DIRS_TO_SKIP.contains(&entry.file_name().to_str().unwrap_or("")) {
 46              iter.skip_current_dir();
 47  
 48              continue;
 49          }
 50  
 51          let path = entry.path();
 52  
 53          // Ignore non-rs
 54          if path.extension() != Some(OsStr::new("rs")) {
 55              continue;
 56          }
 57  
 58          // Read the entire file.
 59          let file = fs::read_to_string(path).unwrap();
 60  
 61          // Prepare a filtered line iterator.
 62          let lines = file
 63              .lines()
 64              .filter(|l| !l.is_empty()) // Ignore empty lines.
 65              .skip_while(|l| !l.starts_with("use")) // Skip the license etc.
 66              .take_while(|l| { // Process the section containing import statements.
 67                  l.starts_with("use")
 68                      || l.starts_with("#[cfg")
 69                      || l.starts_with("//")
 70                      || *l == "};"
 71                      || l.starts_with(|c: char| c.is_ascii_whitespace())
 72              });
 73  
 74          // The currently processed import of interest.
 75          let mut import_of_interest: Option<ImportOfInterest> = None;
 76          // This value not being zero at the end of the imports suggests a missing locktick import.
 77          let mut lock_balance: i8 = 0;
 78  
 79          // Process the filtered lines.
 80          for line in lines {
 81              // Check if this is a lock-related import.
 82              if import_of_interest.is_none() {
 83                  if line.starts_with("use locktick::") {
 84                      import_of_interest = Some(ImportOfInterest::Locktick);
 85                  } else if line.starts_with("use parking_lot::") {
 86                      import_of_interest = Some(ImportOfInterest::ParkingLot);
 87                  } else if line.starts_with("use tokio::") {
 88                      import_of_interest = Some(ImportOfInterest::Tokio);
 89                  }
 90              }
 91  
 92              // Skip irrelevant imports.
 93              let Some(ioi) = import_of_interest else {
 94                  continue;
 95              };
 96  
 97              // Modify the lock balance based on the type of the relevant import.
 98              if [ImportOfInterest::ParkingLot, ImportOfInterest::Tokio].contains(&ioi) {
 99                  if line.contains("Mutex") {
100                      lock_balance += 1;
101                  }
102                  if line.contains("RwLock") {
103                      lock_balance += 1;
104                  }
105              } else if ioi == ImportOfInterest::Locktick {
106                  // Use `matches` instead of just `contains` here, as more than a single
107                  // lock type entry is possible in a locktick import.
108                  for _hit in line.matches("Mutex") {
109                      lock_balance -= 1;
110                  }
111                  for _hit in line.matches("RwLock") {
112                      lock_balance -= 1;
113                  }
114                  // A correction in case of the `use tokio::Mutex as TMutex` convention.
115                  if line.contains("TMutex") {
116                      lock_balance += 1;
117                  }
118              }
119  
120              // Register the end of an import statement.
121              if line.ends_with(";") {
122                  import_of_interest = None;
123              }
124          }
125  
126          // If the file has a lock import "imbalance", print it out and increment the counter.
127          assert!(
128              lock_balance == 0,
129              "The locks in \"{}\" don't seem to have `locktick` counterparts!",
130              entry.path().display()
131          );
132      }
133  }
134  
135  fn check_file_licenses<P: AsRef<Path>>(path: P) {
136      let path = path.as_ref();
137  
138      let mut iter = WalkDir::new(path).into_iter();
139      while let Some(entry) = iter.next() {
140          let entry = entry.unwrap();
141          let entry_type = entry.file_type();
142  
143          // Skip the specified directories.
144          if entry_type.is_dir() && DIRS_TO_SKIP.contains(&entry.file_name().to_str().unwrap_or("")) {
145              iter.skip_current_dir();
146  
147              continue;
148          }
149  
150          // Check all files with the ".rs" extension.
151          if entry_type.is_file() && entry.file_name().to_str().unwrap_or("").ends_with(".rs") {
152              let file = File::open(entry.path()).unwrap();
153              let mut contents = Vec::with_capacity(EXPECTED_LICENSE_TEXT.len());
154              file.take(EXPECTED_LICENSE_TEXT.len() as u64).read_to_end(&mut contents).unwrap();
155  
156              assert!(
157                  contents == EXPECTED_LICENSE_TEXT,
158                  "The license in \"{}\" is either missing or it doesn't match the expected string!",
159                  entry.path().display()
160              );
161          }
162      }
163  }
164  
165  // The build script; it currently only checks the licenses.
166  fn main() {
167      // Check licenses in the current folder.
168      check_file_licenses(".");
169      // Ensure that lock imports have locktick counterparts.
170      check_locktick_imports(".");
171  }