-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Automatically run Django's
collectstatic
command (#108)
The classic Heroku Python buildpack automatically runs the Django `collectstatic` command: https://github.com/heroku/heroku-buildpack-python/blob/main/bin/steps/collectstatic This adds equivalent support, with a couple of improvements: - This implementation performs more checks to see whether the app is actually using the static files feature before trying to run it (reducing the number of cases where users would need to manually disable it). - The collectstatic symlink feature has been enabled, as requested in heroku/heroku-buildpack-python#1060. - Symlinked `manage.py` files are now supported, as requested in heroku/heroku-buildpack-python#972. - The error messages are finer grained/more useful. - There are many more tests (including now testing legacy vs latest Django versions, to check the CLI arguments used work for both ends of the spectrum). There is currently no way to force disable the feature (beyond removing `django.contrib.staticfiles` from `INSTALLED_APPS` in the app's Django config, or removing the `manage.py` script). Supporting this depends on us deciding how best to handle buildpack options, so will be added later, in #109. The build log output and error messages are fairly reasonable already (and a significant improvement over the classic buildpack), and will be further polished as part of the future build output overhaul. The implementation uses the new `utils::run_command_and_capture_output` added in #106. See: * https://docs.djangoproject.com/en/4.2/howto/static-files/ * https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/ * https://docs.djangoproject.com/en/4.2/ref/settings/#settings-staticfiles Fixes #5. GUS-W-9538294.
- Loading branch information
Showing
33 changed files
with
548 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
use crate::utils::{self, CapturedCommandError, StreamedCommandError}; | ||
use indoc::indoc; | ||
use libcnb::Env; | ||
use libherokubuildpack::log::log_info; | ||
use std::io; | ||
use std::path::Path; | ||
use std::process::Command; | ||
|
||
const MANAGEMENT_SCRIPT_NAME: &str = "manage.py"; | ||
|
||
pub(crate) fn is_django_installed(dependencies_layer_dir: &Path) -> io::Result<bool> { | ||
dependencies_layer_dir.join("bin/django-admin").try_exists() | ||
} | ||
|
||
pub(crate) fn run_django_collectstatic( | ||
app_dir: &Path, | ||
command_env: &Env, | ||
) -> Result<(), DjangoCollectstaticError> { | ||
if !has_management_script(app_dir) | ||
.map_err(DjangoCollectstaticError::CheckManagementScriptExists)? | ||
{ | ||
log_info(indoc! {" | ||
Skipping automatic static file generation since no Django 'manage.py' | ||
script (or symlink to one) was found in the root directory of your | ||
application." | ||
}); | ||
return Ok(()); | ||
} | ||
|
||
if !has_collectstatic_command(app_dir, command_env) | ||
.map_err(DjangoCollectstaticError::CheckCollectstaticCommandExists)? | ||
{ | ||
log_info(indoc! {" | ||
Skipping automatic static file generation since the 'django.contrib.staticfiles' | ||
feature is not enabled in your app's Django configuration." | ||
}); | ||
return Ok(()); | ||
} | ||
|
||
log_info("Running 'manage.py collectstatic'"); | ||
utils::run_command_and_stream_output( | ||
Command::new("python") | ||
.args([ | ||
MANAGEMENT_SCRIPT_NAME, | ||
"collectstatic", | ||
"--link", | ||
// Using `--noinput` instead of `--no-input` since the latter requires Django 1.9+. | ||
"--noinput", | ||
]) | ||
.current_dir(app_dir) | ||
.env_clear() | ||
.envs(command_env), | ||
) | ||
.map_err(DjangoCollectstaticError::CollectstaticCommand) | ||
} | ||
|
||
fn has_management_script(app_dir: &Path) -> io::Result<bool> { | ||
app_dir.join(MANAGEMENT_SCRIPT_NAME).try_exists() | ||
} | ||
|
||
fn has_collectstatic_command( | ||
app_dir: &Path, | ||
command_env: &Env, | ||
) -> Result<bool, CapturedCommandError> { | ||
utils::run_command_and_capture_output( | ||
Command::new("python") | ||
.args([MANAGEMENT_SCRIPT_NAME, "help", "collectstatic"]) | ||
.current_dir(app_dir) | ||
.env_clear() | ||
.envs(command_env), | ||
) | ||
.map_or_else( | ||
|error| match error { | ||
// We need to differentiate between the command not existing (due to the staticfiles app | ||
// not being installed) and the Django config or mange.py script being broken. Ideally | ||
// we'd inspect the output of `manage.py help --commands` but that command unhelpfully | ||
// exits zero even if the app's `DJANGO_SETTINGS_MODULE` wasn't a valid module. | ||
CapturedCommandError::NonZeroExitStatus(output) | ||
if String::from_utf8_lossy(&output.stderr).contains("Unknown command") => | ||
{ | ||
Ok(false) | ||
} | ||
_ => Err(error), | ||
}, | ||
|_| Ok(true), | ||
) | ||
} | ||
|
||
/// Errors that can occur when running the Django collectstatic command. | ||
#[derive(Debug)] | ||
pub(crate) enum DjangoCollectstaticError { | ||
CheckCollectstaticCommandExists(CapturedCommandError), | ||
CheckManagementScriptExists(io::Error), | ||
CollectstaticCommand(StreamedCommandError), | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn has_management_script_django_project() { | ||
assert!(has_management_script(Path::new( | ||
"tests/fixtures/django_staticfiles_latest_django" | ||
)) | ||
.unwrap()); | ||
} | ||
|
||
#[test] | ||
fn has_management_script_empty() { | ||
assert!(!has_management_script(Path::new("tests/fixtures/empty")).unwrap()); | ||
} | ||
|
||
#[test] | ||
fn has_management_script_io_error() { | ||
assert!(has_management_script(Path::new("tests/fixtures/empty/.gitkeep")).is_err()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.