Skip to content

Commit 7287b2d

Browse files
committed
Add output field to fetch blocks
Allow users to specify a custom output filename when fetching archives through an optional "output" field in fetch blocks. This provides control over the extracted filename, which is useful when the original archive filename is not suitable or when consistency across different archive sources is needed. The implementation extends the fetch block grammar to accept both the existing fetch specification and the new output field, updates the extraction logic to use the custom filename when provided, and maintains backward compatibility by falling back to the original filename when no output is specified.
1 parent 6c3ec24 commit 7287b2d

File tree

5 files changed

+142
-105
lines changed

5 files changed

+142
-105
lines changed

src/ast.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ impl ModuleBlock {
5252
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5353
pub struct FetchBlock {
5454
pub spec: FetchSpec,
55+
pub output: Option<String>,
5556
}
5657

5758
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -186,6 +187,9 @@ impl PrettyPrint for ModuleBlock {
186187
if let Some(fetch) = &self.fetch {
187188
output.push_str(" fetch {\n");
188189
output.push_str(&fetch.spec.pretty_print());
190+
if let Some(output_name) = &fetch.output {
191+
output.push_str(&format!(" output = {}\n", output_name));
192+
}
189193
output.push_str(" }\n");
190194
}
191195

src/core/deps.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -554,8 +554,14 @@ fn fetch_archive(sprout_path: &str, package: &ModuleBlock, archive: &crate::ast:
554554
}
555555
fs::create_dir_all(&source_path)?;
556556

557+
// Use custom output filename if specified
558+
let output_filename = package.fetch.as_ref()
559+
.and_then(|f| f.output.as_ref())
560+
.map(|s| s.as_str())
561+
.unwrap_or(original_filename);
562+
557563
info!("Extracting {} -> {}", original_filename, source_path.display());
558-
extract_archive(&cache_path, &source_path, original_filename)?;
564+
extract_archive_with_output(&cache_path, &source_path, original_filename, output_filename)?;
559565
Ok(())
560566
}
561567

@@ -621,7 +627,7 @@ fn compute_file_sha256(path: &Path) -> Result<String> {
621627
Ok(format!("{:x}", hasher.finalize()))
622628
}
623629

624-
fn extract_archive(cache_path: &Path, dest: &Path, filename: &str) -> Result<()> {
630+
fn extract_archive_with_output(cache_path: &Path, dest: &Path, filename: &str, output_name: &str) -> Result<()> {
625631
use indicatif::{ProgressBar, ProgressStyle};
626632
use std::time::Duration;
627633

@@ -654,19 +660,19 @@ fn extract_archive(cache_path: &Path, dest: &Path, filename: &str) -> Result<()>
654660
} else if filename.ends_with(".gz") {
655661
let gz_file = std::fs::File::open(cache_path)?;
656662
let mut decoder = flate2::read::GzDecoder::new(gz_file);
657-
let output_name = filename.strip_suffix(".gz").unwrap_or(filename);
658663
let output_path = dest.join(output_name);
659664
let mut output_file = std::fs::File::create(output_path)?;
660665
std::io::copy(&mut decoder, &mut output_file)?;
661666
} else if filename.ends_with(".xz") {
662667
let xz_file = std::fs::File::open(cache_path)?;
663668
let mut decoder = xz::read::XzDecoder::new(xz_file);
664-
let output_name = filename.strip_suffix(".xz").unwrap_or(filename);
665669
let output_path = dest.join(output_name);
666670
let mut output_file = std::fs::File::create(output_path)?;
667671
std::io::copy(&mut decoder, &mut output_file)?;
668672
} else {
669-
return Err(anyhow!("Unsupported archive format: {}", filename));
673+
// Raw file - just copy it with the specified output name
674+
let output_path = dest.join(output_name);
675+
std::fs::copy(cache_path, &output_path)?;
670676
}
671677

672678
if let Some(pb) = pb {

src/parser/parser.rs

Lines changed: 109 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -170,116 +170,127 @@ fn parse_module_block(pair: pest::iterators::Pair<Rule>) -> Result<ModuleBlock>
170170
}
171171

172172
fn parse_fetch_block(pair: pest::iterators::Pair<Rule>) -> Result<FetchBlock> {
173-
let fetch_spec = pair
173+
let mut spec = None;
174+
let mut output = None;
175+
176+
for field in pair.into_inner() {
177+
match field.as_rule() {
178+
Rule::fetch_field => {
179+
let inner = field.into_inner().next().ok_or_else(|| anyhow!("Empty fetch field"))?;
180+
match inner.as_rule() {
181+
Rule::fetch_output_field => {
182+
let value = inner.into_inner().next().ok_or_else(|| anyhow!("Missing output value"))?;
183+
output = Some(parse_value(value)?);
184+
}
185+
Rule::fetch_spec => {
186+
spec = Some(parse_fetch_spec(inner)?);
187+
}
188+
_ => {}
189+
}
190+
}
191+
_ => {}
192+
}
193+
}
194+
195+
Ok(FetchBlock {
196+
spec: spec.ok_or_else(|| anyhow!("Missing fetch spec"))?,
197+
output,
198+
})
199+
}
200+
201+
fn parse_fetch_spec(fetch_spec: pest::iterators::Pair<Rule>) -> Result<FetchSpec> {
202+
let inner_spec = fetch_spec
174203
.into_inner()
175204
.next()
176-
.ok_or_else(|| anyhow!("Missing fetch spec"))?;
177-
let fetch_rule = fetch_spec.as_rule(); // Store the rule before consuming
178-
debug!("Parsing fetch spec with rule: {:?}", fetch_rule);
179-
180-
let spec = match fetch_rule {
181-
Rule::fetch_spec => {
182-
// Handle the fetch_spec wrapper by processing its inner content
183-
let inner_spec = fetch_spec
184-
.into_inner()
185-
.next()
186-
.ok_or_else(|| anyhow!("Missing inner fetch spec"))?;
187-
let inner_rule = inner_spec.as_rule();
188-
debug!("Inner fetch spec rule: {:?}", inner_rule);
189-
190-
match inner_rule {
191-
Rule::git_spec => {
192-
let mut url = None;
193-
let mut ref_ = None;
194-
let mut recursive = false;
195-
196-
for field in inner_spec.into_inner() {
197-
if field.as_rule() == Rule::git_field {
198-
let inner_field = field.into_inner().next().unwrap();
199-
match inner_field.as_rule() {
200-
Rule::git_url_field => {
201-
let mut parts = inner_field.into_inner();
202-
let value = parts.next().unwrap();
203-
url = Some(parse_value(value)?);
204-
}
205-
Rule::git_ref_field => {
206-
let mut parts = inner_field.into_inner();
207-
let value = parts.next().unwrap();
208-
ref_ = Some(parse_value(value)?);
209-
}
210-
Rule::git_recursive_field => {
211-
let mut parts = inner_field.into_inner();
212-
let value = parts.next().unwrap();
213-
recursive = parse_value(value)? == "true";
214-
}
215-
_ => {}
216-
}
205+
.ok_or_else(|| anyhow!("Missing inner fetch spec"))?;
206+
let inner_rule = inner_spec.as_rule();
207+
debug!("Inner fetch spec rule: {:?}", inner_rule);
208+
209+
match inner_rule {
210+
Rule::git_spec => {
211+
let mut url = None;
212+
let mut ref_ = None;
213+
let mut recursive = false;
214+
215+
for field in inner_spec.into_inner() {
216+
if field.as_rule() == Rule::git_field {
217+
let inner_field = field.into_inner().next().unwrap();
218+
match inner_field.as_rule() {
219+
Rule::git_url_field => {
220+
let mut parts = inner_field.into_inner();
221+
let value = parts.next().unwrap();
222+
url = Some(parse_value(value)?);
217223
}
218-
}
219-
220-
FetchSpec::Git(GitSpec {
221-
url: url.ok_or_else(|| anyhow!("Git spec missing url"))?,
222-
ref_,
223-
recursive,
224-
})
225-
}
226-
Rule::http_spec => {
227-
let mut url = None;
228-
let mut sha256 = None;
229-
230-
for field in inner_spec.into_inner() {
231-
if field.as_rule() == Rule::http_field {
232-
let inner_field = field.into_inner().next().unwrap();
233-
match inner_field.as_rule() {
234-
Rule::http_url_field => {
235-
let mut parts = inner_field.into_inner();
236-
let value = parts.next().unwrap();
237-
url = Some(parse_value(value)?);
238-
}
239-
Rule::http_sha256_field => {
240-
let mut parts = inner_field.into_inner();
241-
let value = parts.next().unwrap();
242-
sha256 = Some(parse_value(value)?);
243-
}
244-
_ => {}
245-
}
224+
Rule::git_ref_field => {
225+
let mut parts = inner_field.into_inner();
226+
let value = parts.next().unwrap();
227+
ref_ = Some(parse_value(value)?);
228+
}
229+
Rule::git_recursive_field => {
230+
let mut parts = inner_field.into_inner();
231+
let value = parts.next().unwrap();
232+
recursive = parse_value(value)? == "true";
246233
}
234+
_ => {}
247235
}
248-
249-
FetchSpec::Http(HttpSpec {
250-
url: url.ok_or_else(|| anyhow!("HTTP spec missing url"))?,
251-
sha256,
252-
})
253236
}
254-
Rule::local_spec => {
255-
let mut path = None;
256-
257-
for field in inner_spec.into_inner() {
258-
if field.as_rule() == Rule::local_field {
259-
// local_field contains: "path" ~ "=" ~ value
260-
// field.as_str() gives us the full text like "path = /some/path"
261-
// field.into_inner() gives us only the value token
262-
let value = field.into_inner().next().unwrap(); // Only the value token
263-
path = Some(parse_value(value)?);
237+
}
238+
239+
Ok(FetchSpec::Git(GitSpec {
240+
url: url.ok_or_else(|| anyhow!("Git spec missing url"))?,
241+
ref_,
242+
recursive,
243+
}))
244+
}
245+
Rule::http_spec => {
246+
let mut url = None;
247+
let mut sha256 = None;
248+
249+
for field in inner_spec.into_inner() {
250+
if field.as_rule() == Rule::http_field {
251+
let inner_field = field.into_inner().next().unwrap();
252+
match inner_field.as_rule() {
253+
Rule::http_url_field => {
254+
let mut parts = inner_field.into_inner();
255+
let value = parts.next().unwrap();
256+
url = Some(parse_value(value)?);
257+
}
258+
Rule::http_sha256_field => {
259+
let mut parts = inner_field.into_inner();
260+
let value = parts.next().unwrap();
261+
sha256 = Some(parse_value(value)?);
264262
}
263+
_ => {}
265264
}
266-
267-
FetchSpec::Local(LocalSpec {
268-
path: path.ok_or_else(|| anyhow!("Local spec missing path"))?,
269-
})
270-
}
271-
_ => {
272-
return Err(anyhow!(
273-
"Unsupported inner fetch spec type: {:?}",
274-
inner_rule
275-
));
276265
}
277266
}
267+
268+
Ok(FetchSpec::Http(HttpSpec {
269+
url: url.ok_or_else(|| anyhow!("HTTP spec missing url"))?,
270+
sha256,
271+
}))
278272
}
279-
_ => return Err(anyhow!("Unsupported fetch spec type")),
280-
};
273+
Rule::local_spec => {
274+
let mut path = None;
275+
276+
for field in inner_spec.into_inner() {
277+
if field.as_rule() == Rule::local_field {
278+
let value = field.into_inner().next().unwrap();
279+
path = Some(parse_value(value)?);
280+
}
281+
}
281282

282-
Ok(FetchBlock { spec })
283+
Ok(FetchSpec::Local(LocalSpec {
284+
path: path.ok_or_else(|| anyhow!("Local spec missing path"))?,
285+
}))
286+
}
287+
_ => {
288+
Err(anyhow!(
289+
"Unsupported inner fetch spec type: {:?}",
290+
inner_rule
291+
))
292+
}
293+
}
283294
}
284295

285296
fn parse_script_block(pair: pest::iterators::Pair<Rule>) -> Result<ScriptBlock> {

src/parser/sprout.pest

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ depends_on_field = { "depends_on" ~ "=" ~ array }
2222
exports_field = { "exports" ~ "=" ~ exports_map }
2323

2424
// Fetch block
25-
fetch_block = { "fetch" ~ "{" ~ fetch_spec ~ "}" }
25+
fetch_block = { "fetch" ~ "{" ~ fetch_field* ~ "}" }
26+
fetch_field = {
27+
fetch_spec |
28+
fetch_output_field
29+
}
30+
31+
fetch_output_field = { "output" ~ "=" ~ value }
32+
2633
fetch_spec = {
2734
git_spec |
2835
http_spec |

tree-sitter-sprout/grammar.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,19 @@ module.exports = grammar({
5050
fetch_block: $ => seq(
5151
'fetch',
5252
'{',
53-
$._fetch_spec,
53+
repeat(choice(
54+
$._fetch_spec,
55+
$.fetch_output_field
56+
)),
5457
'}'
5558
),
5659

60+
fetch_output_field: $ => seq(
61+
alias('output', $.field_name),
62+
'=',
63+
$.value
64+
),
65+
5766
_fetch_spec: $ => choice(
5867
$.git_spec,
5968
$.archive_spec,

0 commit comments

Comments
 (0)